Repository: olliethedev/dnd-dashboard Branch: main Commit: 9addc26c3f07 Files: 47 Total size: 96.7 KB Directory structure: gitextract_4y0fo0n3/ ├── .eslintrc.json ├── .gitignore ├── README.md ├── app/ │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components/ │ ├── date-range-picker.tsx │ ├── edit-switch.tsx │ ├── main-nav.tsx │ ├── overview-loader.tsx │ ├── overview.tsx │ ├── recent-sales-loader.tsx │ ├── recent-sales.tsx │ ├── search.tsx │ ├── stats-loader.tsx │ ├── stats.tsx │ ├── swap-layout-loader.tsx │ ├── swap-layout.tsx │ ├── team-switcher.tsx │ ├── transactions-loader.tsx │ ├── transactions.tsx │ ├── ui/ │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── select.tsx │ │ ├── skeleton.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ └── toggle.tsx │ └── user-nav.tsx ├── components.json ├── lib/ │ └── utils.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── tailwind.config.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: README.md ================================================ # Next.js Drag-and-Drop Dashboard Demo This is a beautiful, performant dashboard with drop-to-swap layouts built using Next.js, shadcn/ui, and swapy. ![Demo GIF](./demo.gif) ## Features - Drag-and-drop layout customization - Real-time layout updates - Responsive design - Beautiful UI components from shadcn/ui - Server-side rendering with Next.js ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More To learn more about the technologies used in this project, check out the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - [shadcn/ui](https://ui.shadcn.com/) - beautifully designed components built with Radix UI and Tailwind CSS. - [swapy](https://swapy.tahazsh.com/) - a lightweight JavaScript library for creating drag and drop interfaces. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ================================================ FILE: app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ================================================ FILE: app/layout.tsx ================================================ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ================================================ FILE: app/page.tsx ================================================ import React, { Suspense } from "react"; import dynamic from "next/dynamic"; // navigation components import { UserNav } from "@/components/user-nav"; import { MainNav } from "@/components/main-nav"; import { CalendarDateRangePicker } from "@/components/date-range-picker"; // basic components import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; // high level components import { Search } from "@/components/search"; import { TeamSwitcher } from "@/components/team-switcher"; import { RecentSales } from "@/components/recent-sales"; import { Transactions } from "@/components/transactions"; import { Stats } from "@/components/stats"; import { Overview } from "@/components/overview"; // Skeleton loaders import { StatsLoader } from "@/components/stats-loader"; import { SwapLayoutLoader } from "@/components/swap-layout-loader"; import { OverviewLoader } from "@/components/overview-loader"; import { RecentSalesLoader } from "@/components/recent-sales-loader"; import { TransactionsLoader } from "@/components/transactions-loader"; // swap layout is a client side component, since it uses local storage for this demo. // In production you might want to save the layout order on server via api call const SwapLayout = dynamic(() => import("@/components/swap-layout"), { ssr: false, loading: () => , }); // This is the main page of the app. export default function Home() { return (

Dashboard

); } // this is the initial layout of the swap layout. const initialSwapSections = { top: ( Stats }> ), center_left: ( Overview }> ), center_right: ( Recent Sales You made 265 sales this month. }> ), bottom: (
Transactions Recent transactions from your store.
}>
), }; // this is the class names for the sections of the swap layout. const sectionSlotClassNames = { "1": "col-span-2 row-span-1 h-full w-full flex flex-col", "2": "col-span-1 row-span-2 h-full w-full flex flex-col", "3": "col-span-1 row-span-2 h-full w-full flex flex-col", "4": "col-span-2 row-span-2 h-full w-full flex flex-col", }; ================================================ FILE: components/date-range-picker.tsx ================================================ "use client" import * as React from "react" import { CalendarIcon } from "@radix-ui/react-icons" import { addDays, format } from "date-fns" import { DateRange } from "react-day-picker" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Calendar } from "@/components/ui/calendar" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" export function CalendarDateRangePicker({ className, }: React.HTMLAttributes) { const [date, setDate] = React.useState({ from: new Date(2023, 0, 20), to: addDays(new Date(2023, 0, 20), 20), }) return (
) } ================================================ FILE: components/edit-switch.tsx ================================================ "use client"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useCallback } from "react"; export function EditSwitch({ defaultEditing, onCheckedChange, }: { defaultEditing: boolean; onCheckedChange: (editing: boolean) => void; }) { const setEditing = useCallback( (editing: boolean) => { onCheckedChange(editing); }, [onCheckedChange] ); return (
); } ================================================ FILE: components/main-nav.tsx ================================================ import Link from "next/link" import { cn } from "@/lib/utils" export function MainNav({ className, ...props }: React.HTMLAttributes) { return ( ) } ================================================ FILE: components/overview-loader.tsx ================================================ import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; export function OverviewLoader() { return ( ); } ================================================ FILE: components/overview.tsx ================================================ "use client"; import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; const data = [ { name: "Jan", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Feb", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Mar", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Apr", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "May", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Jun", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Jul", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Aug", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Sep", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Oct", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Nov", total: Math.floor(Math.random() * 5000) + 1000, }, { name: "Dec", total: Math.floor(Math.random() * 5000) + 1000, }, ]; export async function Overview() { // simulate a delay await new Promise((resolve) => setTimeout(resolve, 2000)); return ( `$${value}`} /> ); } ================================================ FILE: components/recent-sales-loader.tsx ================================================ import { Card, CardContent, CardHeader, CardDescription } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; export function RecentSalesLoader() { return (
{[...Array(5)].map((_, index) => (
))}
); } ================================================ FILE: components/recent-sales.tsx ================================================ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; export async function RecentSales() { // simulate a delay await new Promise((resolve) => setTimeout(resolve, 2000)); return (
OM

Olivia Martin

olivia.martin@email.com

+$1,999.00
JL

Jackson Lee

jackson.lee@email.com

+$39.00
IN

Isabella Nguyen

isabella.nguyen@email.com

+$299.00
WK

William Kim

will@email.com

+$99.00
SD

Sofia Davis

sofia.davis@email.com

+$39.00
); } ================================================ FILE: components/search.tsx ================================================ import { Input } from "@/components/ui/input" export function Search() { return (
) } ================================================ FILE: components/stats-loader.tsx ================================================ import { Skeleton } from "@/components/ui/skeleton"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; export function StatsLoader() { return (
{[...Array(4)].map((_, index) => ( ))}
); } ================================================ FILE: components/stats.tsx ================================================ import React from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; export async function Stats() { // simulate a delay await new Promise((resolve) => setTimeout(resolve, 2000)); return (
Total Revenue
$45,231.89

+20.1% from last month

Subscriptions
+2350

+180.1% from last month

Sales
+12,234

+19% from last month

Active Now
+573

+201 since last hour

); } ================================================ FILE: components/swap-layout-loader.tsx ================================================ import { Skeleton } from "@/components/ui/skeleton"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; export function SwapLayoutLoader() { return (
); } ================================================ FILE: components/swap-layout.tsx ================================================ "use client"; import { useEffect, ReactNode, useState, useRef } from "react"; import { createSwapy } from "swapy"; import { EditSwitch } from "./edit-switch"; // this is the initial order of the layout of the swap layout. const DEFAULT = { "1": "top" as SectionKey, "2": "center_left" as SectionKey, "3": "center_right" as SectionKey, "4": "bottom" as SectionKey, }; // this is the type of the sections of the swap layout. type SectionKey = "top" | "center_left" | "center_right" | "bottom"; interface SwapLayoutProps { sections: { [key in SectionKey]: ReactNode; }; sectionSlotClassNames: { [key in keyof typeof DEFAULT]: string; }; defaultEditing: boolean; } // SwapLayout is the main component of the swap layout. export default function SwapLayout({ sections, defaultEditing, sectionSlotClassNames, ...rest }: SwapLayoutProps & React.HTMLProps) { // load the layout from local storage if it exists, otherwise use the default layout. const slotItems: Record = JSON.parse( localStorage.getItem("dashSlotItems") || JSON.stringify(DEFAULT) ); // this is the function that is called when the layout items are swapped. const onSwap = (object: Record) => { localStorage.setItem("dashSlotItems", JSON.stringify(object)); }; const [isEditing, setIsEditing] = useState(defaultEditing); return ( <> {Object.entries(slotItems).map(([slotId, sectionKey]) => { const section = sections[sectionKey]; return ( {isEditing ? ( ) : null} {section} ); })} ); } interface ContainerProps { id: string; children: React.ReactNode; className?: string; enable?: boolean; onSwap?: (record: Record) => void; config?: object; } // Container is the main container of the swap layout. export const Container = ({ id, enable = true, onSwap = () => {}, config = undefined, children, ...rest }: ContainerProps & React.HTMLProps) => { const swapy = useRef>(); useEffect(() => { swapy.current = createSwapy(document.querySelector(`#${id}`), config); swapy.current.enable(enable); swapy.current.onSwap((event) => { onSwap(event.data.object); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { swapy.current?.enable(enable); }, [enable]); useEffect(() => { swapy.current?.onSwap((event) => { onSwap(event.data.object); }); }, [onSwap]); return (
{children}
); }; interface SlotProps { id: number | string; className?: string; name?: string; children?: React.ReactNode; } // Slots are used to wrap the items of the swap layout. export const Slot = ({ id, name, children, ...rest }: SlotProps & React.HTMLProps) => { return (
{children}
); }; interface ItemProps { className?: string; name: string; children?: React.ReactNode; } // Item is the item inside the slot of the swap layout. export const Item = ({ name, children, ...rest }: ItemProps & React.HTMLProps) => { return (
{children}
); }; export const Handle = ({ children, ...rest }: React.HTMLProps) => { return ( {children} ); }; ================================================ FILE: components/team-switcher.tsx ================================================ "use client" import * as React from "react" import { CaretSortIcon, CheckIcon, PlusCircledIcon, } from "@radix-ui/react-icons" import { cn } from "@/lib/utils" import { Avatar, AvatarFallback, AvatarImage, } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@/components/ui/command" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" const groups = [ { label: "Personal Account", teams: [ { label: "Alicia Koch", value: "personal", }, ], }, { label: "Teams", teams: [ { label: "Acme Inc.", value: "acme-inc", }, { label: "Monsters Inc.", value: "monsters", }, ], }, ] type Team = (typeof groups)[number]["teams"][number] type PopoverTriggerProps = React.ComponentPropsWithoutRef interface TeamSwitcherProps extends PopoverTriggerProps {} export function TeamSwitcher({ className }: TeamSwitcherProps) { const [open, setOpen] = React.useState(false) const [showNewTeamDialog, setShowNewTeamDialog] = React.useState(false) const [selectedTeam, setSelectedTeam] = React.useState( groups[0].teams[0] ) return ( No team found. {groups.map((group) => ( {group.teams.map((team) => ( { setSelectedTeam(team) setOpen(false) }} className="text-sm" > SC {team.label} ))} ))} { setOpen(false) setShowNewTeamDialog(true) }} > Create Team Create team Add a new team to manage products and customers.
) } ================================================ FILE: components/transactions-loader.tsx ================================================ import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; export function TransactionsLoader() { return (
{['Customer', 'Type', 'Status', 'Date', 'Amount'].map((_, index) => ( ))}
{[...Array(4)].map((_, rowIndex) => (
))}
); } ================================================ FILE: components/transactions.tsx ================================================ import React from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "./ui/table"; import { Badge } from "./ui/badge"; export async function Transactions() { // simulate a delay await new Promise((resolve) => setTimeout(resolve, 2000)); return ( Customer Type Status Date Amount
Liam Johnson
liam@example.com
Sale Approved 2023-06-23 $250.00
Olivia Smith
olivia@example.com
Refund Declined 2023-06-24 $150.00
Noah Williams
noah@example.com
Subscription Approved 2023-06-25 $350.00
Emma Brown
emma@example.com
Sale Approved 2023-06-26 $450.00
); } ================================================ FILE: components/ui/avatar.tsx ================================================ "use client" import * as React from "react" import * as AvatarPrimitive from "@radix-ui/react-avatar" import { cn } from "@/lib/utils" const Avatar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) Avatar.displayName = AvatarPrimitive.Root.displayName const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AvatarImage.displayName = AvatarPrimitive.Image.displayName const AvatarFallback = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName export { Avatar, AvatarImage, AvatarFallback } ================================================ FILE: components/ui/badge.tsx ================================================ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const badgeVariants = cva( "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", outline: "text-foreground", }, }, defaultVariants: { variant: "default", }, } ) export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { return (
) } export { Badge, badgeVariants } ================================================ FILE: components/ui/button.tsx ================================================ "use client" import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( ) } ) Button.displayName = "Button" export { Button, buttonVariants } ================================================ FILE: components/ui/calendar.tsx ================================================ "use client" import * as React from "react" import { ChevronLeft, ChevronRight } from "lucide-react" import { DayPicker } from "react-day-picker" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" export type CalendarProps = React.ComponentProps function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { return ( , IconRight: ({ ...props }) => , }} {...props} /> ) } Calendar.displayName = "Calendar" export { Calendar } ================================================ FILE: components/ui/card.tsx ================================================ "use client" import * as React from "react" import { cn } from "@/lib/utils" const Card = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) Card.displayName = "Card" const CardHeader = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardHeader.displayName = "CardHeader" const CardTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardTitle.displayName = "CardTitle" const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardDescription.displayName = "CardDescription" const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (

)) CardContent.displayName = "CardContent" const CardFooter = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)) CardFooter.displayName = "CardFooter" export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } ================================================ FILE: components/ui/chart.tsx ================================================ "use client" import * as React from "react" import * as RechartsPrimitive from "recharts" import { cn } from "@/lib/utils" // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: "", dark: ".dark" } as const export type ChartConfig = { [k in string]: { label?: React.ReactNode icon?: React.ComponentType } & ( | { color?: string; theme?: never } | { color?: never; theme: Record } ) } type ChartContextProps = { config: ChartConfig } const ChartContext = React.createContext(null) function useChart() { const context = React.useContext(ChartContext) if (!context) { throw new Error("useChart must be used within a ") } return context } const ChartContainer = React.forwardRef< HTMLDivElement, React.ComponentProps<"div"> & { config: ChartConfig children: React.ComponentProps< typeof RechartsPrimitive.ResponsiveContainer >["children"] } >(({ id, className, children, config, ...props }, ref) => { const uniqueId = React.useId() const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` return (
{children}
) }) ChartContainer.displayName = "Chart" const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( ([_, config]) => config.theme || config.color ) if (!colorConfig.length) { return null } return (