feat(v4): dashboard demo

This commit is contained in:
shadcn
2025-03-17 22:07:02 +04:00
parent 28d97b7fc1
commit 911c52022a
21 changed files with 758 additions and 144 deletions

View File

@@ -2,16 +2,21 @@
import * as React from "react"
import {
IconCloud,
IconCamera,
IconChartBar,
IconDashboard,
IconDatabase,
IconFileAi,
IconFileDescription,
IconFileWord,
IconFolder,
IconHelp,
IconInnerShadowTop,
IconListDetails,
IconReport,
IconSearch,
IconSettings,
IconUsers,
} from "@tabler/icons-react"
import {
@@ -23,7 +28,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar"
import { NavClouds } from "@/app/(examples)/dashboard-02/components/nav-clouds"
// import { NavClouds } from "@/app/(examples)/dashboard-02/components/nav-clouds"
import { NavDocuments } from "@/app/(examples)/dashboard-02/components/nav-documents"
import { NavMain } from "@/app/(examples)/dashboard-02/components/nav-main"
import { NavSecondary } from "@/app/(examples)/dashboard-02/components/nav-secondary"
@@ -46,13 +51,26 @@ const data = {
url: "#",
icon: IconListDetails,
},
{
title: "Analytics",
url: "#",
icon: IconChartBar,
},
{
title: "Projects",
url: "#",
icon: IconFolder,
},
{
title: "Team",
url: "#",
icon: IconUsers,
},
],
navClouds: [
{
title: "Capture",
icon: IconCloud,
iconClassName:
"fill-amber-500 text-amber-500 dark:fill-amber-400 dark:text-amber-400",
icon: IconCamera,
isActive: true,
url: "#",
items: [
@@ -68,9 +86,7 @@ const data = {
},
{
title: "Proposal",
icon: IconCloud,
iconClassName:
"fill-blue-500 text-blue-500 dark:fill-blue-400 dark:text-blue-400",
icon: IconFileDescription,
url: "#",
items: [
{
@@ -84,10 +100,8 @@ const data = {
],
},
{
title: "Contract",
icon: IconCloud,
iconClassName:
"fill-teal-500 text-teal-500 dark:fill-teal-400 dark:text-teal-400",
title: "Prompts",
icon: IconFileAi,
url: "#",
items: [
{
@@ -157,7 +171,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavClouds items={data.navClouds} />
{/* <NavClouds items={data.navClouds} /> */}
<NavDocuments items={data.documents} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>

View File

@@ -0,0 +1,292 @@
"use client"
import * as React from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { useIsMobile } from "@/registry/new-york-v4/hooks/use-mobile"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/registry/new-york-v4/ui/chart"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/registry/new-york-v4/ui/toggle-group"
export const description = "An interactive area chart"
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
]
const chartConfig = {
visitors: {
label: "Visitors",
},
desktop: {
label: "Desktop",
color: "var(--primary)",
},
mobile: {
label: "Mobile",
color: "var(--primary)",
},
} satisfies ChartConfig
export function ChartAreaInteractive() {
const isMobile = useIsMobile()
const [timeRange, setTimeRange] = React.useState("90d")
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d")
}
}, [isMobile])
const filteredData = chartData.filter((item) => {
const date = new Date(item.date)
const referenceDate = new Date("2024-06-30")
let daysToSubtract = 90
if (timeRange === "30d") {
daysToSubtract = 30
} else if (timeRange === "7d") {
daysToSubtract = 7
}
const startDate = new Date(referenceDate)
startDate.setDate(startDate.getDate() - daysToSubtract)
return date >= startDate
})
return (
<Card className="@container/card">
<CardHeader>
<CardTitle>Total Visitors</CardTitle>
<CardDescription>
<span className="hidden @[540px]/card:block">
Total for the last 3 months
</span>
<span className="@[540px]/card:hidden">Last 3 months</span>
</CardDescription>
<CardAction>
<ToggleGroup
type="single"
value={timeRange}
onValueChange={setTimeRange}
variant="outline"
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
>
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
<ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
</ToggleGroup>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger
className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
size="sm"
aria-label="Select a value"
>
<SelectValue placeholder="Last 3 months" />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="90d" className="rounded-lg">
Last 3 months
</SelectItem>
<SelectItem value="30d" className="rounded-lg">
Last 30 days
</SelectItem>
<SelectItem value="7d" className="rounded-lg">
Last 7 days
</SelectItem>
</SelectContent>
</Select>
</CardAction>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={filteredData}>
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-desktop)"
stopOpacity={1.0}
/>
<stop
offset="95%"
stopColor="var(--color-desktop)"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-mobile)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-mobile)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value)
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
/>
<ChartTooltip
cursor={false}
defaultIndex={isMobile ? -1 : 10}
content={
<ChartTooltipContent
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
indicator="dot"
/>
}
/>
<Area
dataKey="mobile"
type="natural"
fill="url(#fillMobile)"
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="url(#fillDesktop)"
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}

View File

@@ -256,7 +256,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
>
<SelectValue placeholder="Assign reviewer" />
</SelectTrigger>
<SelectContent>
<SelectContent align="end">
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
<SelectItem value="Jamik Tashpulatov">Jamik Tashpulatov</SelectItem>
</SelectContent>
@@ -447,55 +447,57 @@ export function DataTable({
</div>
</div>
<div className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
<DndContext
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}
id={sortableId}
>
<Table>
<TableHeader className="bg-muted sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody className="**:data-[slot=table-cell]:first:w-8">
{table.getRowModel().rows?.length ? (
<SortableContext
items={dataIds}
strategy={verticalListSortingStrategy}
>
{table.getRowModel().rows.map((row) => (
<DraggableRow key={row.id} row={row} />
))}
</SortableContext>
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
<div className="overflow-hidden rounded-lg border">
<DndContext
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}
id={sortableId}
>
<Table>
<TableHeader className="bg-muted sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody className="**:data-[slot=table-cell]:first:w-8">
{table.getRowModel().rows?.length ? (
<SortableContext
items={dataIds}
strategy={verticalListSortingStrategy}
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DndContext>
{table.getRowModel().rows.map((row) => (
<DraggableRow key={row.id} row={row} />
))}
</SortableContext>
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DndContext>
</div>
<div className="flex items-center justify-between px-4">
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
@@ -606,7 +608,7 @@ function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
{item.header}
</Button>
</DrawerTrigger>
<DrawerContent className="theme-custom font-sans">
<DrawerContent>
<DrawerHeader className="gap-1">
<DrawerTitle>{item.header}</DrawerTitle>
<DrawerDescription>

View File

@@ -0,0 +1,27 @@
"use client"
import * as React from "react"
import { IconBrightness } from "@tabler/icons-react"
import { useTheme } from "next-themes"
import { Button } from "@/registry/new-york-v4/ui/button"
export function ModeToggle() {
const { setTheme, resolvedTheme } = useTheme()
const toggleTheme = React.useCallback(() => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}, [resolvedTheme, setTheme])
return (
<Button
variant="secondary"
size="icon"
className="group/toggle size-8"
onClick={toggleTheme}
>
<IconBrightness />
<span className="sr-only">Toggle theme</span>
</Button>
)
}

View File

@@ -47,7 +47,7 @@ export function NavClouds({
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon className={item.iconClassName} />}
{item.icon && <item.icon />}
<span>{item.title}</span>
<IconChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>

View File

@@ -50,7 +50,10 @@ export function NavDocuments({
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<SidebarMenuAction
showOnHover
className="data-[state=open]:bg-accent rounded-sm"
>
<IconDots />
<span className="sr-only">More</span>
</SidebarMenuAction>

View File

@@ -49,7 +49,7 @@ export function NavUser({
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<Avatar className="h-8 w-8 rounded-lg grayscale">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>

View File

@@ -0,0 +1,34 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { SidebarTrigger } from "@/registry/new-york-v4/ui/sidebar"
import { ModeToggle } from "@/app/(examples)/dashboard-02/components/mode-toggle"
import { ThemeSelector } from "@/app/(examples)/dashboard-02/components/theme-selector"
export function SiteHeader() {
return (
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mx-2 data-[orientation=vertical]:h-4"
/>
<h1 className="text-base font-medium">Documents</h1>
<div className="ml-auto flex items-center gap-2">
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
<a
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard-02"
rel="noopener noreferrer"
target="_blank"
className="dark:text-foreground"
>
GitHub
</a>
</Button>
<ThemeSelector />
<ModeToggle />
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,98 @@
"use client"
import { useThemeConfig } from "@/components/active-theme"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
const DEFAULT_THEMES = [
{
name: "Default",
value: "default",
},
{
name: "Blue",
value: "blue",
},
{
name: "Green",
value: "green",
},
{
name: "Amber",
value: "amber",
},
]
const SCALED_THEMES = [
{
name: "Default",
value: "default-scaled",
},
{
name: "Blue",
value: "blue-scaled",
},
]
const MONO_THEMES = [
{
name: "Mono",
value: "mono-scaled",
},
]
export function ThemeSelector() {
const { activeTheme, setActiveTheme } = useThemeConfig()
return (
<div className="flex items-center gap-2">
<Select value={activeTheme} onValueChange={setActiveTheme}>
<SelectTrigger
size="sm"
className="justify-start *:data-[slot=select-value]:w-12"
>
<span className="text-muted-foreground hidden sm:block">
Select a theme:
</span>
<span className="text-muted-foreground block sm:hidden">Theme</span>
<SelectValue placeholder="Select a theme" />
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
<SelectLabel>Default</SelectLabel>
{DEFAULT_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}>
{theme.name}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Scaled</SelectLabel>
{SCALED_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}>
{theme.name}
</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>Monospaced</SelectLabel>
{MONO_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}>
{theme.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
)
}

View File

@@ -1,17 +1,13 @@
import { cookies } from "next/headers"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/registry/new-york-v4/ui/sidebar"
import { AppSidebar } from "@/app/(examples)/dashboard-02/components/app-sidebar"
import { SiteHeader } from "@/app/(examples)/dashboard-02/components/site-header"
import "./theme.css"
import { IconExternalLink } from "@tabler/icons-react"
import { Button } from "@/registry/new-york-v4/ui/button"
export default async function DashboardLayout({
children,
@@ -24,7 +20,6 @@ export default async function DashboardLayout({
return (
<SidebarProvider
defaultOpen={defaultOpen}
className="theme-custom font-sans"
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
@@ -33,28 +28,7 @@ export default async function DashboardLayout({
>
<AppSidebar variant="inset" />
<SidebarInset>
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<div className="flex w-full items-center gap-2 px-4 lg:px-6">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mx-2 data-[orientation=vertical]:h-4"
/>
<h1 className="text-lg font-semibold">Documents</h1>
<div className="ml-auto">
<Button variant="link" asChild size="sm">
<a
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard-02"
rel="noopener noreferrer"
target="_blank"
>
<IconExternalLink />
GitHub
</a>
</Button>
</div>
</div>
</header>
<SiteHeader />
<div className="flex flex-1 flex-col">{children}</div>
</SidebarInset>
</SidebarProvider>

View File

@@ -4,11 +4,13 @@ import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import {
Card,
CardAction,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { ChartAreaInteractive } from "@/app/(examples)/dashboard-02/components/chart-area-interactive"
import { DataTable } from "@/app/(examples)/dashboard-02/components/data-table"
import data from "./data.json"
@@ -20,58 +22,102 @@ export const metadata: Metadata = {
export default async function Dashboard02() {
return (
<div className="flex flex-1 flex-col gap-2">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<div className="grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:shadow-xs md:grid-cols-2 lg:grid-cols-4 lg:px-6">
<Card>
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
<Card className="@container/card">
<CardHeader>
<CardTitle>Total Revenue</CardTitle>
<CardDescription>$1,250.00 in the last 30 days</CardDescription>
<CardDescription>Total Revenue</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
$1,250.00
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter>
<Badge variant="outline">
<IconTrendingUp />
+12.5%
</Badge>
<CardFooter className="flex-col items-start gap-2.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Trending up this month <IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
Visitors for the last 6 months
</div>
</CardFooter>
</Card>
<Card>
<Card className="@container/card">
<CardHeader>
<CardTitle>New Customers</CardTitle>
<CardDescription>-12 customers from last month</CardDescription>
<CardDescription>New Customers</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
1,234
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingDown />
-20%
</Badge>
</CardAction>
</CardHeader>
<CardFooter>
<Badge variant="outline">
<IconTrendingDown />
-20%
</Badge>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Down 20% this period <IconTrendingDown className="size-4" />
</div>
<div className="text-muted-foreground">
Acquisition needs attention
</div>
</CardFooter>
</Card>
<Card>
<Card className="@container/card">
<CardHeader>
<CardTitle>Active Accounts</CardTitle>
<CardDescription>+2,345 users from last month</CardDescription>
<CardDescription>Active Accounts</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
45,678
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter>
<Badge variant="outline">
<IconTrendingUp />
+12.5%
</Badge>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Strong user retention <IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
Engagement exceed targets
</div>
</CardFooter>
</Card>
<Card>
<Card className="@container/card">
<CardHeader>
<CardTitle>Growth Rate</CardTitle>
<CardDescription>+12.5% increase per month</CardDescription>
<CardDescription>Growth Rate</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
4.5%
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingUp />
+4.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter>
<Badge variant="outline">
<IconTrendingUp />
+4.5%
</Badge>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Steady performance increase{" "}
<IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
Meets growth projections
</div>
</CardFooter>
</Card>
</div>
<div className="px-4 lg:px-6">
<ChartAreaInteractive />
</div>
<DataTable data={data} />
</div>
</div>

View File

@@ -1,10 +1,13 @@
:root,
.theme-custom {
body {
@apply overscroll-none bg-transparent;
}
:root {
--font-sans: var(--font-inter);
--header-height: calc(var(--spacing) * 12 + 1px);
--primary: oklch(0.546 0.245 262.881);
--primary-foreground: oklch(0.97 0.014 254.604);
}
.theme-scaled {
@media (min-width: 1024px) {
--radius: 0.6rem;
--text-lg: 1.05rem;
@@ -16,8 +19,87 @@
[data-slot="card"] {
--spacing: 0.16rem;
}
[data-slot="select-trigger"],
[data-slot="toggle-group-item"] {
--spacing: 0.222222rem;
}
}
.dark {
--primary: oklch(0.623 0.214 259.815);
.theme-default,
.theme-default-scaled {
--primary: var(--color-neutral-600);
--primary-foreground: var(--color-neutral-50);
@variant dark {
--primary: var(--color-neutral-500);
--primary-foreground: var(--color-neutral-50);
}
}
.theme-blue,
.theme-blue-scaled {
--primary: var(--color-blue-600);
--primary-foreground: var(--color-blue-50);
@variant dark {
--primary: var(--color-blue-500);
--primary-foreground: var(--color-blue-50);
}
}
.theme-green,
.theme-green-scaled {
--primary: var(--color-lime-600);
--primary-foreground: var(--color-lime-50);
@variant dark {
--primary: var(--color-lime-600);
--primary-foreground: var(--color-lime-50);
}
}
.theme-amber,
.theme-amber-scaled {
--primary: var(--color-amber-600);
--primary-foreground: var(--color-amber-50);
@variant dark {
--primary: var(--color-amber-500);
--primary-foreground: var(--color-amber-50);
}
}
.theme-mono,
.theme-mono-scaled {
--font-sans: var(--font-mono);
--primary: var(--color-neutral-600);
--primary-foreground: var(--color-neutral-50);
@variant dark {
--primary: var(--color-neutral-500);
--primary-foreground: var(--color-neutral-50);
}
.rounded-xs,
.rounded-sm,
.rounded-md,
.rounded-lg,
.rounded-xl {
@apply !rounded-none;
border-radius: 0;
}
.shadow-xs,
.shadow-sm,
.shadow-md,
.shadow-lg,
.shadow-xl {
@apply !shadow-none;
}
[data-slot="toggle-group"],
[data-slot="toggle-group-item"] {
@apply !rounded-none !shadow-none;
}
}

View File

@@ -7,6 +7,8 @@ import {
import { AppSidebar } from "@/app/(examples)/dashboard/components/app-sidebar"
import { SiteHeader } from "@/app/(examples)/dashboard/components/site-header"
import "../../themes.css"
export default async function DashboardLayout({
children,
}: {

View File

@@ -1,11 +1,9 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@import "./themes.css";
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
@@ -111,6 +109,26 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
@layer base {

View File

@@ -79,6 +79,7 @@ export default async function RootLayout({
}>) {
const cookieStore = await cookies()
const activeThemeValue = cookieStore.get("active_theme")?.value
const isScaled = activeThemeValue?.endsWith("-scaled")
return (
<html lang="en" suppressHydrationWarning>
@@ -99,6 +100,7 @@ export default async function RootLayout({
className={cn(
"bg-background overscroll-none font-sans antialiased",
activeThemeValue ? `theme-${activeThemeValue}` : "",
isScaled ? "theme-scaled" : "",
fontVariables
)}
>
@@ -107,6 +109,7 @@ export default async function RootLayout({
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
>
<ActiveThemeProvider initialTheme={activeThemeValue}>
{children}

View File

@@ -38,12 +38,15 @@ export function ActiveThemeProvider({
useEffect(() => {
setThemeCookie(activeTheme)
document.body.classList.forEach((className) => {
if (className.startsWith("theme-")) {
Array.from(document.body.classList)
.filter((className) => className.startsWith("theme-"))
.forEach((className) => {
document.body.classList.remove(className)
}
})
})
document.body.classList.add(`theme-${activeTheme}`)
if (activeTheme.endsWith("-scaled")) {
document.body.classList.add("theme-scaled")
}
}, [activeTheme])
return (

View File

@@ -4,14 +4,21 @@ import * as React from "react"
import { MoonIcon, SunIcon } from "lucide-react"
import { useTheme } from "next-themes"
import { META_THEME_COLORS, useMetaColor } from "@/hooks/use-meta-color"
import { Button } from "@/registry/new-york-v4/ui/button"
export function ModeToggle() {
const { setTheme, resolvedTheme } = useTheme()
const { setMetaColor } = useMetaColor()
const toggleTheme = React.useCallback(() => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}, [resolvedTheme, setTheme])
setMetaColor(
resolvedTheme === "dark"
? META_THEME_COLORS.light
: META_THEME_COLORS.dark
)
}, [resolvedTheme, setTheme, setMetaColor])
return (
<Button

View File

@@ -79,6 +79,7 @@
"tailwindcss": "^4.0.7",
"tailwindcss-animate": "^1.0.7",
"ts-morph": "^22.0.0",
"tw-animate-css": "^1.2.2",
"vaul": "1.1.2",
"zod": "^3.24.1"
},

View File

@@ -92,7 +92,7 @@ function SelectLabel({
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-2 py-1.5 text-sm font-medium", className)}
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)

File diff suppressed because one or more lines are too long

8
pnpm-lock.yaml generated
View File

@@ -300,6 +300,9 @@ importers:
ts-morph:
specifier: ^22.0.0
version: 22.0.0
tw-animate-css:
specifier: ^1.2.2
version: 1.2.2
vaul:
specifier: 1.1.2
version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -7744,6 +7747,9 @@ packages:
resolution: {integrity: sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ==}
hasBin: true
tw-animate-css@1.2.2:
resolution: {integrity: sha512-TdSaQcV+V8MypECoXr6Nnv3EQFz05kxfTUaOHtpWTQZp8r5Bx2OmTnkSZVlOTaRiS20a6wYw0RaAnNX2D8ywrQ==}
typanion@3.14.0:
resolution: {integrity: sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==}
@@ -17557,6 +17563,8 @@ snapshots:
turbo-windows-64: 1.13.4
turbo-windows-arm64: 1.13.4
tw-animate-css@1.2.2: {}
typanion@3.14.0: {}
type-check@0.4.0: