replace contents with tweakcn-next

This commit is contained in:
Sahaj Jain
2025-04-17 14:18:28 +05:30
parent 8521b6a356
commit b3f5de8a3d
340 changed files with 33366 additions and 3383 deletions
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## 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/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at 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.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

+105
View File
@@ -0,0 +1,105 @@
"use client";
import { getEditorConfig } from "@/config/editors";
import Link from "next/link";
import { Moon, Sun, Heart } from "lucide-react";
import GitHubIcon from "@/assets/github.svg";
import TwitterIcon from "@/assets/twitter.svg";
import DiscordIcon from "@/assets/discord.svg";
import { cn } from "@/lib/utils";
import { useTheme } from "@/components/theme-provider";
import Logo from "@/assets/logo.svg";
import { useGithubStars } from "@/hooks/use-github-stars";
import { SocialLink } from "@/components/social-link";
import { Separator } from "@/components/ui/separator";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { Suspense } from "react";
import { Loading } from "@/components/loading";
import Editor from "@/components/editor/editor";
export function meta() {
return [
{ title: "tweakcn — Theme Generator for shadcn/ui" },
{
name: "description",
content:
"Easily customize and preview your shadcn/ui theme with tweakcn. Modify colors, fonts, and styles in real-time.",
},
];
}
export default function Component() {
const { theme, toggleTheme } = useTheme();
const { stargazersCount } = useGithubStars("jnsahaj", "tweakcn");
const handleThemeToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
const { clientX: x, clientY: y } = event;
toggleTheme({ x, y });
};
return (
<>
<div
className={cn(
"h-screen flex flex-col text-foreground bg-background transition-colors"
)}
>
<header className="border-b">
<div className="px-2 md:px-4 py-4 flex items-center gap-2 justify-between">
<div className="flex items-center gap-1">
<Link href="/" className="flex items-center gap-2">
<Logo className="size-6" title="tweakcn" />
<span className="font-bold hidden md:block">tweakcn</span>
</Link>
</div>
<div className="flex items-center gap-3.5">
<SocialLink
href="https://github.com/jnsahaj/tweakcn"
className="flex items-center gap-2 text-sm font-bold"
>
<GitHubIcon className="size-4" />
{stargazersCount > 0 && stargazersCount.toLocaleString()}
</SocialLink>
<Separator orientation="vertical" className="h-5" />
<div className="hidden md:flex items-center gap-3.5">
<SocialLink
href="https://github.com/sponsors/jnsahaj"
className="flex items-center gap-1.5 px-2 py-1 rounded-md border hover:border-pink-500 hover:text-pink-500 transition-colors"
>
<Heart className="size-4" strokeWidth={2.5} />
<span className="text-sm font-medium">Support</span>
</SocialLink>
<SocialLink href="https://discord.gg/Phs4u2NM3n">
<DiscordIcon className="size-5" />
</SocialLink>
</div>
<SocialLink href="https://x.com/iamsahaj_xyz">
<TwitterIcon className="size-4" />
</SocialLink>
<Separator orientation="vertical" className="h-5" />
<SwitchPrimitives.Root
checked={theme === "dark"}
onClick={handleThemeToggle}
className="peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-accent data-[state=unchecked]:bg-input"
>
<SwitchPrimitives.Thumb className="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 flex items-center justify-center">
{theme === "dark" ? (
<Moon className="size-3" />
) : (
<Sun className="size-3" />
)}
</SwitchPrimitives.Thumb>
</SwitchPrimitives.Root>
</div>
</div>
</header>
<main className="flex-1 overflow-hidden">
<Suspense fallback={<Loading />}>
<Editor config={getEditorConfig("theme")} />
</Suspense>
</main>
</div>
</>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+116
View File
@@ -0,0 +1,116 @@
@import "tailwindcss";
@import "tw-animate-css";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Computed Shadow Variants */
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
--tracking-tighter: calc(var(--letter-spacing) - 0.05em);
--tracking-tight: calc(var(--letter-spacing) - 0.025em);
--tracking-normal: var(--letter-spacing);
--tracking-wide: calc(var(--letter-spacing) + 0.025em);
--tracking-wider: calc(var(--letter-spacing) + 0.05em);
--tracking-widest: calc(var(--letter-spacing) + 0.1em);
}
* {
border-color: var(--color-border);
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
-webkit-font-smoothing: antialiased;
letter-spacing: var(--letter-spacing);
}
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
/* View Transition Wave Effect */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
/* Ensure the outgoing view (old theme) is beneath */
z-index: 0;
}
::view-transition-new(root) {
/* Ensure the incoming view (new theme) is always on top */
z-index: 1;
}
@keyframes reveal {
from {
/* Use CSS variables for the origin, defaulting to center if not set */
clip-path: circle(0% at var(--x, 50%) var(--y, 50%));
opacity: 0.7;
}
to {
/* Use CSS variables for the origin, defaulting to center if not set */
clip-path: circle(150% at var(--x, 50%) var(--y, 50%));
opacity: 1;
}
}
::view-transition-new(root) {
/* Apply the reveal animation */
animation: reveal 0.4s ease-in-out forwards;
}
+84
View File
@@ -0,0 +1,84 @@
import { NuqsAdapter } from "nuqs/adapters/next/app";
import type { Metadata } from "next";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ThemeScript } from "@/components/theme-script";
import "./globals.css";
import { PostHogInit } from "@/components/posthog-init";
export const metadata: Metadata = {
title: "Beautiful themes for shadcn/ui — tweakcn | Theme Editor & Generator",
description:
"Customize theme for shadcn/ui with tweakcn's interactive editor. Supports Tailwind CSS v4, Shadcn UI, and custom styles. Modify properties, preview changes, and get the code in real time.",
keywords:
"theme editor, theme generator, shadcn, ui, components, react, tailwind, button, editor, visual editor, component editor, web development, frontend, design system, UI components, React components, Tailwind CSS, shadcn/ui themes",
authors: [{ name: "Sahaj Jain" }],
openGraph: {
title: "Beautiful themes for shadcn/ui — tweakcn | Theme Editor & Generator",
description:
"Customize theme for shadcn/ui with tweakcn's interactive editor. Supports Tailwind CSS v4, Shadcn UI, and custom styles. Modify properties, preview changes, and get the code in real time.",
url: "https://tweakcn.com/",
siteName: "tweakcn",
images: [
{
url: "https://tweakcn.com/og-image.png",
width: 1200,
height: 630,
},
],
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Beautiful themes for shadcn/ui — tweakcn | Theme Editor & Generator",
description:
"Customize theme for shadcn/ui with tweakcn's interactive editor. Supports Tailwind CSS v4, Shadcn UI, and custom styles. Modify properties, preview changes, and get the code in real time.",
images: ["https://tweakcn.com/og-image.png"],
},
robots: "index, follow",
viewport: "width=device-width, initial-scale=1.0",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<ThemeScript />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link
rel="apple-touch-icon"
href="/apple-touch-icon.png"
type="image/png"
sizes="180x180"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fira+Code:wght@300..700&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:ital,wght@0,400..700;1,400..700&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&family=Oxanium:wght@200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<NuqsAdapter>
<ThemeProvider defaultTheme="light">
<TooltipProvider>
<Toaster />
{children}
</TooltipProvider>
</ThemeProvider>
</NuqsAdapter>
<PostHogInit />
</body>
</html>
);
}
+8
View File
@@ -0,0 +1,8 @@
export default function NotFound() {
return (
<div className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-4xl font-bold">404</h1>
<p className="mt-4 text-lg">Page not found</p>
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
"use client";
import { useEffect, useState } from "react";
import { Header } from "@/components/home/header";
import { Hero } from "@/components/home/hero";
import { ThemePresetSelector } from "@/components/home/theme-preset-selector";
import { Features } from "@/components/home/features";
import { HowItWorks } from "@/components/home/how-it-works";
import { Roadmap } from "@/components/home/roadmap";
import { FAQ } from "@/components/home/faq";
import { CTA } from "@/components/home/cta";
import { Footer } from "@/components/home/footer";
export default function Home() {
const [isScrolled, setIsScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 10) {
setIsScrolled(true);
} else {
setIsScrolled(false);
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div className="flex min-h-[100dvh] justify-items-center items-center flex-col bg-background text-foreground">
<Header
isScrolled={isScrolled}
mobileMenuOpen={mobileMenuOpen}
setMobileMenuOpen={setMobileMenuOpen}
/>
<main className="flex-1">
<Hero />
<ThemePresetSelector />
<Features />
<HowItWorks />
<Roadmap />
<FAQ />
<CTA />
</main>
<Footer />
</div>
);
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 256 199" width="256" height="199" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<path d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg
viewBox="0 0 256 250"
width="256"
height="250"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
>
<path
d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-heart anim-hover-grow mr-1 color-fg-sponsors">
<path d="m8 14.25.345.666a.75.75 0 0 1-.69 0l-.008-.004-.018-.01a7.152 7.152 0 0 1-.31-.17 22.055 22.055 0 0 1-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.066 22.066 0 0 1-3.744 2.584l-.018.01-.006.003h-.002ZM4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.58 20.58 0 0 0 8 13.393a20.58 20.58 0 0 0 3.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.749.749 0 0 1-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 801 B

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<!-- Line 1 split to avoid circle at (188, 148) -->
<line x1="208" y1="128" x2="207.8" y2="128.2" stroke="currentColor" stroke-linecap="round" stroke-width="24" />
<line x1="168.2" y1="167.8" x2="128" y2="208" stroke="currentColor" stroke-linecap="round" stroke-width="24" />
<!-- Line 2 split to avoid circle at (96, 136) -->
<line x1="192" y1="40" x2="115.8" y2="116.2" stroke="currentColor" stroke-linecap="round" stroke-width="24" />
<line x1="76.2" y1="155.8" x2="40" y2="192" stroke="currentColor" stroke-linecap="round" stroke-width="24" />
<!-- Hollow circles with stroke width = 24 and radius = 24 -->
<circle cx="188" cy="148" r="24" fill="none" stroke="currentColor" stroke-width="24" />
<circle cx="96" cy="136" r="24" fill="none" stroke="currentColor" stroke-width="24" />
</svg>

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1227" fill="none" viewBox="0 0 1200 1227"><path fill="currentColor" d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z"/></svg>

After

Width:  |  Height:  |  Size: 426 B

+230
View File
@@ -0,0 +1,230 @@
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Copy, Check, PanelRight } from "lucide-react";
import { EditorConfig, ThemeEditorState } from "@/types/editor";
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
import { ColorFormat } from "../../types";
import {
Select,
SelectContent,
SelectTrigger,
SelectValue,
SelectItem,
} from "../ui/select";
import { usePostHog } from "posthog-js/react";
import { useEditorStore } from "@/store/editor-store";
import { usePreferencesStore } from "@/store/preferences-store";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
interface CodePanelProps {
config: EditorConfig;
themeEditorState: ThemeEditorState;
onCodePanelToggle: () => void;
}
const CodePanel: React.FC<CodePanelProps> = ({
config,
themeEditorState,
onCodePanelToggle,
}) => {
const [registryCopied, setRegistryCopied] = useState(false);
const [copied, setCopied] = useState(false);
const posthog = usePostHog();
const preset = useEditorStore((state) => state.themeState.preset);
const colorFormat = usePreferencesStore((state) => state.colorFormat);
const tailwindVersion = usePreferencesStore((state) => state.tailwindVersion);
const packageManager = usePreferencesStore((state) => state.packageManager);
const setColorFormat = usePreferencesStore((state) => state.setColorFormat);
const setTailwindVersion = usePreferencesStore((state) => state.setTailwindVersion);
const setPackageManager = usePreferencesStore((state) => state.setPackageManager);
const code = config.codeGenerator.generateComponentCode(
themeEditorState,
colorFormat,
tailwindVersion
);
const getRegistryCommand = (preset: string) => {
const url = `https://tweakcn.com/r/themes/${preset}.json`;
switch (packageManager) {
case "pnpm":
return `pnpm dlx shadcn@latest add ${url}`;
case "npm":
return `npx shadcn@latest add ${url}`;
case "yarn":
return `yarn dlx shadcn@latest add ${url}`;
case "bun":
return `bunx shadcn@latest add ${url}`;
}
};
const copyRegistryCommand = async () => {
try {
await navigator.clipboard.writeText(getRegistryCommand(preset));
setRegistryCopied(true);
setTimeout(() => setRegistryCopied(false), 2000);
captureCopyEvent("COPY_REGISTRY_COMMAND");
} catch (err) {
console.error("Failed to copy text:", err);
}
};
const captureCopyEvent = (event: string) => {
posthog.capture(event, {
editorType: "theme",
preset,
colorFormat,
tailwindVersion,
});
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
captureCopyEvent("COPY_CODE");
} catch (err) {
console.error("Failed to copy text:", err);
}
};
return (
<div className="h-full flex flex-col p-4">
<div className="flex-none mb-4">
<div className="flex items-center justify-between gap-2">
<h2 className="text-lg font-semibold">Code</h2>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onCodePanelToggle}
className="h-8 invisible md:visible group"
>
<PanelRight className="size-4 group-hover:scale-120 transition-all" />
</Button>
</TooltipTrigger>
<TooltipContent>Collapse Code Panel</TooltipContent>
</Tooltip>
</div>
{preset && preset !== "default" && (
<div className="mt-4 rounded-md overflow-hidden border">
<div className="flex border-b">
{(["pnpm", "npm", "yarn", "bun"] as const).map((pm) => (
<button
key={pm}
onClick={() => setPackageManager(pm)}
className={`px-3 py-1.5 text-sm font-medium ${
packageManager === pm
? "bg-muted text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{pm}
</button>
))}
<Button
variant="ghost"
size="sm"
onClick={copyRegistryCommand}
className="h-8 ml-auto"
aria-label={
registryCopied ? "Copied to clipboard" : "Copy to clipboard"
}
>
{registryCopied ? (
<Check className="size-4" />
) : (
<Copy className="size-4" />
)}
</Button>
</div>
<div className="p-2 bg-muted/50 flex justify-between items-center">
<ScrollArea className="w-full">
<div className="whitespace-nowrap overflow-y-hidden pb-2">
<code className="text-sm font-mono">
{getRegistryCommand(preset)}
</code>
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
</div>
)}
</div>
<div className="flex items-center gap-2 mb-4 ">
<Select
value={tailwindVersion}
onValueChange={(value: "3" | "4") => {
setTailwindVersion(value);
if (value === "4" && colorFormat === "hsl") {
setColorFormat("oklch");
}
}}
>
<SelectTrigger className="w-fit focus:ring-transparent focus:border-none bg-muted/50 outline-hidden border-none gap-1">
<SelectValue className="focus:ring-transparent" />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">Tailwind v3</SelectItem>
<SelectItem value="4">Tailwind v4</SelectItem>
</SelectContent>
</Select>
<Select
value={colorFormat}
onValueChange={(value: ColorFormat) => setColorFormat(value)}
>
<SelectTrigger className="w-fit focus:ring-transparent focus:border-none bg-muted/50 outline-hidden border-none gap-1">
<SelectValue className="focus:ring-transparent" />
</SelectTrigger>
<SelectContent>
<SelectItem value="hsl">hsl</SelectItem>
<SelectItem value="oklch">oklch</SelectItem>
<SelectItem value="rgb">rgb</SelectItem>
<SelectItem value="hex">hex</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex-1 min-h-0 flex flex-col rounded-lg border overflow-hidden">
<div className="flex-none flex justify-between items-center px-4 py-2 border-b bg-muted/50">
<span className="text-sm font-medium">index.css</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => copyToClipboard(code)}
className="h-8"
aria-label={copied ? "Copied to clipboard" : "Copy to clipboard"}
>
{copied ? (
<>
<Check className="size-4" />
<span className="sr-only md:not-sr-only">Copied</span>
</>
) : (
<>
<Copy className="size-4" />
<span className="sr-only md:not-sr-only">Copy</span>
</>
)}
</Button>
</div>
</div>
<ScrollArea className="flex-1 relative">
<pre className="h-full p-4 text-sm">
<code>{code}</code>
</pre>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
</div>
);
};
export default CodePanel;
+65
View File
@@ -0,0 +1,65 @@
import React, { useState, useCallback, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { ColorPickerProps } from "@/types";
import { debounce } from "@/utils/debounce";
const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => {
const [isOpen, setIsOpen] = useState(false);
const [localColor, setLocalColor] = useState(color);
// Update localColor if the prop changes externally
useEffect(() => {
setLocalColor(color);
}, [color]);
// Create debounced onChange handler with useCallback to maintain reference
const debouncedOnChange = useCallback(
debounce((value) => {
onChange(value);
}, 10),
[onChange]
);
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newColor = e.target.value;
setLocalColor(newColor);
debouncedOnChange(newColor);
};
return (
<div className="mb-3">
<div className="flex items-center justify-between mb-1.5">
<Label
htmlFor={`color-${label.replace(/\s+/g, "-").toLowerCase()}`}
className="text-xs font-medium"
>
{label}
</Label>
<div className="text-xs text-muted-foreground">{localColor}</div>
</div>
<div className="flex items-center gap-2">
<div
className="h-8 w-8 rounded border cursor-pointer overflow-hidden relative flex items-center justify-center"
style={{ backgroundColor: localColor }}
onClick={() => setIsOpen(!isOpen)}
>
<input
type="color"
id={`color-${label.replace(/\s+/g, "-").toLowerCase()}`}
value={localColor}
onChange={handleColorChange}
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
/>
</div>
<input
type="text"
value={localColor}
onChange={handleColorChange}
className="flex-1 h-8 px-2 text-sm rounded-md border bg-background"
/>
</div>
</div>
);
};
export default ColorPicker;
+47
View File
@@ -0,0 +1,47 @@
import React, { useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
import { ControlSectionProps } from "@/types";
const ControlSection = ({
title,
children,
expanded = false,
className,
id,
}: ControlSectionProps) => {
const [isExpanded, setIsExpanded] = useState(expanded);
return (
<div id={id} className={cn("mb-4 border rounded-lg overflow-hidden", className)}>
<div
className="flex items-center justify-between p-3 cursor-pointer bg-background hover:bg-muted"
onClick={() => setIsExpanded(!isExpanded)}
>
<h3 className="font-medium text-sm">{title}</h3>
<button
type="button"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label={isExpanded ? "Collapse section" : "Expand section"}
>
{isExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
</div>
<div
className={cn(
"overflow-hidden transition-all duration-200",
isExpanded ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0",
)}
>
<div className="p-3 bg-background border-t">{children}</div>
</div>
</div>
);
};
export default ControlSection;
+117
View File
@@ -0,0 +1,117 @@
import React, { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
interface CssImportDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onImport: (css: string) => void;
}
const CssImportDialog: React.FC<CssImportDialogProps> = ({
open,
onOpenChange,
onImport,
}) => {
const [cssText, setCssText] = useState("");
const [error, setError] = useState<string | null>(null);
const handleImport = () => {
// Basic validation - check if the CSS contains some expected variables
if (!cssText.trim()) {
setError("Please enter CSS content");
return;
}
try {
// Here you would add more sophisticated CSS parsing validation
// For now we'll just do a simple check
if (!cssText.includes("--") || !cssText.includes(":")) {
setError(
"Invalid CSS format. CSS should contain variable definitions like --primary: #color"
);
return;
}
onImport(cssText);
setCssText("");
setError(null);
onOpenChange(false);
} catch (err) {
setError("Failed to parse CSS. Please check your syntax.");
}
};
const handleClose = () => {
setCssText("");
setError(null);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh]">
<DialogHeader>
<DialogTitle className="text-foreground">Import Custom CSS</DialogTitle>
<DialogDescription>
Paste your CSS file below to customize the theme colors. Make sure
to include variables like --primary, --background, etc.
</DialogDescription>
</DialogHeader>
{error && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4 mr-2" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="grid gap-4 py-4">
<Textarea
placeholder={`:root {
--background: 0 0% 100%;
--foreground: oklch(0.52 0.13 144.17);
--primary: #3e2723;
/* And more */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: hsl(37.50 36.36% 95.69%);
--primary: rgb(46, 125, 50);
/* And more */
}
`}
className="min-h-[300px] font-mono text-sm text-foreground"
value={cssText}
onChange={(e) => {
setCssText(e.target.value);
if (error) setError(null);
}}
/>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={handleClose}
className="text-foreground"
>
Cancel
</Button>
<Button onClick={handleImport}>Import</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default CssImportDialog;
+127
View File
@@ -0,0 +1,127 @@
import React, { useState } from "react";
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EditorConfig, BaseEditorState, ThemeEditorState } from "@/types/editor";
import { ThemeStyles } from "@/types/theme";
import CodePanel from "./code-panel";
import { Sliders } from "lucide-react";
import { useEditorStore } from "@/store/editor-store";
interface EditorProps {
config: EditorConfig;
initialState?: BaseEditorState;
}
const isThemeStyles = (styles: any): styles is ThemeStyles => {
return !!styles && "light" in styles && "dark" in styles;
};
const Editor: React.FC<EditorProps> = ({ config }) => {
const { themeState, setThemeState } = useEditorStore();
const Controls = config.controls;
const Preview = config.preview;
const [isCodePanelOpen, setIsCodePanelOpen] = useState(true);
const handleStyleChange = (newStyles: ThemeStyles) => {
setThemeState({ ...themeState, styles: newStyles });
};
// Ensure we have valid theme styles
const styles = !isThemeStyles(themeState.styles)
? (config.defaultState as ThemeEditorState).styles
: themeState.styles;
return (
<div className="h-full overflow-hidden">
{/* Desktop Layout */}
<div className="h-full hidden md:block">
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel defaultSize={30} minSize={20} maxSize={30}>
<div className="h-full p-4">
<Controls
styles={styles}
onChange={handleStyleChange}
currentMode={themeState.currentMode}
/>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={45} minSize={20}>
<div className="h-full flex flex-col">
<div className="flex-1 min-h-0 p-4">
<Preview
styles={styles}
currentMode={themeState.currentMode}
isCodePanelOpen={isCodePanelOpen}
onCodePanelToggle={() => setIsCodePanelOpen(!isCodePanelOpen)}
/>
</div>
</div>
</ResizablePanel>
{isCodePanelOpen && (
<>
<ResizableHandle />
<ResizablePanel defaultSize={25} minSize={10}>
<CodePanel
config={config}
themeEditorState={themeState}
onCodePanelToggle={() => setIsCodePanelOpen(!isCodePanelOpen)}
/>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</div>
{/* Mobile Layout */}
<div className="h-full md:hidden">
<Tabs defaultValue="controls" className="h-full">
<TabsList className="w-full">
<TabsTrigger value="controls" className="flex-1">
<Sliders className="h-4 w-4 mr-2" />
Controls
</TabsTrigger>
<TabsTrigger value="preview" className="flex-1">
Preview
</TabsTrigger>
<TabsTrigger value="code" className="flex-1">
Code
</TabsTrigger>
</TabsList>
<TabsContent value="controls" className="h-[calc(100%-2.5rem)]">
<div className="h-full p-4">
<Controls
styles={styles}
onChange={handleStyleChange}
currentMode={themeState.currentMode}
/>
</div>
</TabsContent>
<TabsContent value="preview" className="h-[calc(100%-2.5rem)]">
<div className="h-full p-4">
<Preview
styles={styles}
currentMode={themeState.currentMode}
isCodePanelOpen={isCodePanelOpen}
onCodePanelToggle={() => setIsCodePanelOpen(!isCodePanelOpen)}
/>
</div>
</TabsContent>
<TabsContent value="code" className="h-[calc(100%-2.5rem)]">
<CodePanel
config={config}
themeEditorState={themeState}
onCodePanelToggle={() => setIsCodePanelOpen(!isCodePanelOpen)}
/>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default Editor;
+101
View File
@@ -0,0 +1,101 @@
import React from "react";
import { Label } from "../ui/label";
import { SliderWithInput } from "./slider-with-input";
import ColorPicker from "./color-picker";
import ControlSection from "./control-section";
interface ShadowControlProps {
shadowColor: string;
shadowOpacity: number;
shadowBlur: number;
shadowSpread: number;
shadowOffsetX: number;
shadowOffsetY: number;
onChange: (key: string, value: any) => void;
}
const ShadowControl: React.FC<ShadowControlProps> = ({
shadowColor,
shadowOpacity,
shadowBlur,
shadowSpread,
shadowOffsetX,
shadowOffsetY,
onChange,
}) => {
return (
<ControlSection title="Shadow" expanded>
<div className="space-y-4">
<div>
<ColorPicker
color={shadowColor}
onChange={(color) => onChange("shadow-color", color)}
label="Shadow Color"
/>
</div>
<div>
<SliderWithInput
value={shadowOpacity}
onChange={(value) => onChange("shadow-opacity", value)}
min={0}
max={1}
step={0.01}
unit=""
label="Shadow Opacity"
/>
</div>
<div>
<SliderWithInput
value={shadowBlur}
onChange={(value) => onChange("shadow-blur", value)}
min={0}
max={50}
step={0.5}
unit="px"
label="Blur Radius"
/>
</div>
<div>
<SliderWithInput
value={shadowSpread}
onChange={(value) => onChange("shadow-spread", value)}
min={-50}
max={50}
step={0.5}
unit="px"
label="Spread"
/>
</div>
<div>
<SliderWithInput
value={shadowOffsetX}
onChange={(value) => onChange("shadow-offset-x", value)}
min={-50}
max={50}
step={0.5}
unit="px"
label="Offset X"
/>
</div>
<div>
<SliderWithInput
value={shadowOffsetY}
onChange={(value) => onChange("shadow-offset-y", value)}
min={-50}
max={50}
step={0.5}
unit="px"
label="Offset Y"
/>
</div>
</div>
</ControlSection>
);
};
export default ShadowControl;
+70
View File
@@ -0,0 +1,70 @@
import { useEffect, useState } from "react";
import { Label } from "../ui/label";
import { Input } from "../ui/input";
import { Slider } from "../ui/slider";
export const SliderWithInput = ({
value,
onChange,
min,
max,
step = 1,
label,
unit = "px",
}: {
value: number;
onChange: (value: number) => void;
min: number;
max: number;
step?: number;
label: string;
unit?: string;
}) => {
const [localValue, setLocalValue] = useState(value);
useEffect(() => {
setLocalValue(value);
}, [value]);
return (
<div className="mb-3">
<div className="flex items-center justify-between mb-1.5">
<Label
htmlFor={`slider-${label.replace(/\s+/g, "-").toLowerCase()}`}
className="text-xs font-medium"
>
{label}
</Label>
<div className="flex items-center gap-1">
<Input
id={`input-${label.replace(/\s+/g, "-").toLowerCase()}`}
type="number"
value={localValue}
onChange={(e) => {
const newValue = Number(e.target.value);
setLocalValue(newValue);
onChange(newValue);
}}
min={min}
max={max}
step={step}
className="h-6 w-18 text-xs px-2"
/>
<span className="text-xs text-muted-foreground">{unit}</span>
</div>
</div>
<Slider
id={`slider-${label.replace(/\s+/g, "-").toLowerCase()}`}
value={[localValue]}
min={min}
max={max}
step={step}
onValueChange={(values) => {
setLocalValue(values[0]);
onChange(values[0]);
}}
className="py-1"
/>
</div>
);
};
+102
View File
@@ -0,0 +1,102 @@
import { FileCode, Palette, RefreshCw, LucideIcon, Undo2 } from "lucide-react";
import { Button } from "../ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { cn } from "@/lib/utils";
interface MenuItemProps {
icon: LucideIcon;
label: string;
onClick: () => void;
disabled?: boolean;
title?: string;
}
const MenuItem = ({
icon: Icon,
label,
onClick,
disabled,
title,
}: MenuItemProps) => {
return (
<DropdownMenuItem
onClick={onClick}
disabled={disabled}
className={cn(
"flex items-center gap-2 px-3 py-2 rounded-md transition-colors",
disabled
? "opacity-50 cursor-not-allowed"
: "hover:bg-accent/50 cursor-pointer"
)}
title={title}
>
<Icon className="h-4 w-4 text-muted-foreground" />
<span>{label}</span>
</DropdownMenuItem>
);
};
interface ThemeControlActionsProps {
hasChanges: boolean;
hasPresetChanges: boolean;
onReset: () => void;
onResetToPreset: () => void;
onImportClick: () => void;
}
const ThemeControlActions = ({
hasChanges,
hasPresetChanges,
onReset,
onResetToPreset,
onImportClick,
}: ThemeControlActionsProps) => {
const menuItems: MenuItemProps[] = [
{
icon: FileCode,
label: "Import from CSS file",
onClick: onImportClick,
},
{
icon: RefreshCw,
label: "Reset to Current Preset",
onClick: onResetToPreset,
disabled: !hasPresetChanges,
title: hasPresetChanges ? "Reset to current preset" : "No changes from preset",
},
{
icon: Undo2,
label: "Reset to Default Theme",
onClick: onReset,
disabled: !hasChanges,
title: hasChanges ? "Reset to base theme" : "No changes to reset",
},
];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 px-2 gap-1.5 text-muted-foreground hover:text-foreground hover:bg-accent/50"
>
<Palette className="size-3.5" />
<span className="text-sm">Options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-fit p-2">
{menuItems.map((item, index) => (
<MenuItem key={index} {...item} />
))}
</DropdownMenuContent>
</DropdownMenu>
);
};
export default ThemeControlActions;
+471
View File
@@ -0,0 +1,471 @@
import React, { useState } from "react";
import { ThemeEditorControlsProps, ThemeStyleProps } from "@/types/theme";
import ControlSection from "./control-section";
import ColorPicker from "./color-picker";
import { ScrollArea } from "../ui/scroll-area";
import ThemePresetSelect from "./theme-preset-select";
import { presets } from "../../utils/theme-presets";
import {
getAppliedThemeFont,
monoFonts,
sansSerifFonts,
serifFonts,
} from "../../utils/theme-fonts";
import { useEditorStore } from "../../store/editor-store";
import { Label } from "../ui/label";
import { SliderWithInput } from "./slider-with-input";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import ThemeFontSelect from "./theme-font-select";
import {
DEFAULT_FONT_MONO,
DEFAULT_FONT_SANS,
DEFAULT_FONT_SERIF,
COMMON_STYLES,
defaultThemeState,
} from "../../config/theme";
import { Separator } from "../ui/separator";
import { AlertCircle } from "lucide-react";
import CssImportDialog from "./css-import-dialog";
import { toast } from "../ui/use-toast";
import { parseCssInput } from "../../utils/parse-css-input";
import ShadowControl from "./shadow-control";
import ThemeControlActions from "./theme-control-actions";
const ThemeControlPanel = ({
styles,
currentMode,
onChange,
}: ThemeEditorControlsProps) => {
const {
applyThemePreset,
themeState,
resetToCurrentPreset,
resetToDefault,
hasDefaultThemeChanged,
hasCurrentPresetChanged,
} = useEditorStore();
const [cssImportOpen, setCssImportOpen] = useState(false);
const currentStyles = {
...defaultThemeState.styles.light,
...defaultThemeState.styles[currentMode],
...styles?.[currentMode],
};
const updateStyle = React.useCallback(
<K extends keyof typeof currentStyles>(
key: K,
value: (typeof currentStyles)[K]
) => {
// apply common styles to both light and dark modes
if (COMMON_STYLES.includes(key)) {
onChange({
...styles,
light: { ...styles.light, [key]: value },
dark: { ...styles.dark, [key]: value },
});
return;
}
onChange({
...styles,
[currentMode]: {
...currentStyles,
[key]: value,
},
});
},
[onChange, styles, currentMode, currentStyles]
);
const handleCssImport = (css: string) => {
// This just shows a success toast for now
const { lightColors, darkColors } = parseCssInput(css);
onChange({
...styles,
light: { ...styles.light, ...lightColors },
dark: { ...styles.dark, ...darkColors },
});
// The actual CSS parsing and theme application logic would be implemented later
toast({
title: "CSS imported",
description: "Your custom CSS has been imported successfully",
});
};
// Ensure we have valid styles for the current mode
if (!currentStyles) {
return null; // Or some fallback UI
}
const radius = parseFloat(currentStyles.radius.replace("rem", ""));
return (
<div className="space-y-4 h-full">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold">Theme Editor</h2>
</div>
<ThemeControlActions
hasChanges={hasDefaultThemeChanged()}
hasPresetChanges={hasCurrentPresetChanged()}
onReset={resetToDefault}
onResetToPreset={resetToCurrentPreset}
onImportClick={() => setCssImportOpen(true)}
/>
</div>
<div className="mb-6 ml-1">
<ThemePresetSelect
presets={presets}
currentPreset={themeState.preset}
onPresetChange={applyThemePreset}
/>
</div>
<Tabs defaultValue="colors" className="w-full h-full">
<TabsList className="grid grid-cols-3 mb-3 w-full">
<TabsTrigger value="colors">Colors</TabsTrigger>
<TabsTrigger value="typography">Typography</TabsTrigger>
<TabsTrigger value="other">Other</TabsTrigger>
</TabsList>
<ScrollArea className="h-full pb-40">
<TabsContent value="colors">
<ControlSection title="Primary Colors" id="primary-colors" expanded>
<ColorPicker
color={currentStyles.primary}
onChange={(color) => updateStyle("primary", color)}
label="Primary"
/>
<ColorPicker
color={currentStyles["primary-foreground"]}
onChange={(color) => updateStyle("primary-foreground", color)}
label="Primary Foreground"
/>
</ControlSection>
<ControlSection title="Secondary Colors" expanded>
<ColorPicker
color={currentStyles.secondary}
onChange={(color) => updateStyle("secondary", color)}
label="Secondary"
/>
<ColorPicker
color={currentStyles["secondary-foreground"]}
onChange={(color) => updateStyle("secondary-foreground", color)}
label="Secondary Foreground"
/>
</ControlSection>
<ControlSection title="Accent Colors" expanded>
<ColorPicker
color={currentStyles.accent}
onChange={(color) => updateStyle("accent", color)}
label="Accent"
/>
<ColorPicker
color={currentStyles["accent-foreground"]}
onChange={(color) => updateStyle("accent-foreground", color)}
label="Accent Foreground"
/>
</ControlSection>
<ControlSection title="Base Colors">
<ColorPicker
color={currentStyles.background}
onChange={(color) => updateStyle("background", color)}
label="Background"
/>
<ColorPicker
color={currentStyles.foreground}
onChange={(color) => updateStyle("foreground", color)}
label="Foreground"
/>
</ControlSection>
<ControlSection title="Card Colors">
<ColorPicker
color={currentStyles.card}
onChange={(color) => updateStyle("card", color)}
label="Card Background"
/>
<ColorPicker
color={currentStyles["card-foreground"]}
onChange={(color) => updateStyle("card-foreground", color)}
label="Card Foreground"
/>
</ControlSection>
<ControlSection title="Popover Colors">
<ColorPicker
color={currentStyles.popover}
onChange={(color) => updateStyle("popover", color)}
label="Popover Background"
/>
<ColorPicker
color={currentStyles["popover-foreground"]}
onChange={(color) => updateStyle("popover-foreground", color)}
label="Popover Foreground"
/>
</ControlSection>
<ControlSection title="Muted Colors">
<ColorPicker
color={currentStyles.muted}
onChange={(color) => updateStyle("muted", color)}
label="Muted"
/>
<ColorPicker
color={currentStyles["muted-foreground"]}
onChange={(color) => updateStyle("muted-foreground", color)}
label="Muted Foreground"
/>
</ControlSection>
<ControlSection title="Destructive Colors">
<ColorPicker
color={currentStyles.destructive}
onChange={(color) => updateStyle("destructive", color)}
label="Destructive"
/>
<ColorPicker
color={currentStyles["destructive-foreground"]}
onChange={(color) => updateStyle("destructive-foreground", color)}
label="Destructive Foreground"
/>
</ControlSection>
<ControlSection title="Border & Input Colors">
<ColorPicker
color={currentStyles.border}
onChange={(color) => updateStyle("border", color)}
label="Border"
/>
<ColorPicker
color={currentStyles.input}
onChange={(color) => updateStyle("input", color)}
label="Input"
/>
<ColorPicker
color={currentStyles.ring}
onChange={(color) => updateStyle("ring", color)}
label="Ring"
/>
</ControlSection>
<ControlSection title="Chart Colors">
<ColorPicker
color={currentStyles["chart-1"]}
onChange={(color) => updateStyle("chart-1", color)}
label="Chart 1"
/>
<ColorPicker
color={currentStyles["chart-2"]}
onChange={(color) => updateStyle("chart-2", color)}
label="Chart 2"
/>
<ColorPicker
color={currentStyles["chart-3"]}
onChange={(color) => updateStyle("chart-3", color)}
label="Chart 3"
/>
<ColorPicker
color={currentStyles["chart-4"]}
onChange={(color) => updateStyle("chart-4", color)}
label="Chart 4"
/>
<ColorPicker
color={currentStyles["chart-5"]}
onChange={(color) => updateStyle("chart-5", color)}
label="Chart 5"
/>
</ControlSection>
<ControlSection title="Sidebar Colors">
<ColorPicker
color={currentStyles.sidebar}
onChange={(color) => updateStyle("sidebar", color)}
label="Sidebar Background"
/>
<ColorPicker
color={currentStyles["sidebar-foreground"]}
onChange={(color) => updateStyle("sidebar-foreground", color)}
label="Sidebar Foreground"
/>
<ColorPicker
color={currentStyles["sidebar-primary"]}
onChange={(color) => updateStyle("sidebar-primary", color)}
label="Sidebar Primary"
/>
<ColorPicker
color={currentStyles["sidebar-primary-foreground"]}
onChange={(color) =>
updateStyle("sidebar-primary-foreground", color)
}
label="Sidebar Primary Foreground"
/>
<ColorPicker
color={currentStyles["sidebar-accent"]}
onChange={(color) => updateStyle("sidebar-accent", color)}
label="Sidebar Accent"
/>
<ColorPicker
color={currentStyles["sidebar-accent-foreground"]}
onChange={(color) => updateStyle("sidebar-accent-foreground", color)}
label="Sidebar Accent Foreground"
/>
<ColorPicker
color={currentStyles["sidebar-border"]}
onChange={(color) => updateStyle("sidebar-border", color)}
label="Sidebar Border"
/>
<ColorPicker
color={currentStyles["sidebar-ring"]}
onChange={(color) => updateStyle("sidebar-ring", color)}
label="Sidebar Ring"
/>
</ControlSection>
</TabsContent>
<TabsContent value="typography" className="flex flex-col gap-4">
<div className="p-3 bg-muted/50 rounded-md border mb-2 flex items-start gap-2.5">
<AlertCircle className="h-5 w-5 text-muted-foreground shrink-0 mt-0.5" />
<div className="text-sm text-muted-foreground">
<p>
To use custom fonts, embed them in your project. <br />
See{" "}
<a
href="https://tailwindcss.com/docs/font-family"
target="_blank"
className="underline underline-offset-2 hover:text-muted-foreground/90"
>
Tailwind docs
</a>{" "}
for details.
</p>
</div>
</div>
<ControlSection title="Font Family" expanded>
<div className="mb-4">
<Label htmlFor="font-sans" className="text-xs mb-1.5 block">
Sans-Serif Font
</Label>
<ThemeFontSelect
fonts={{ ...sansSerifFonts, ...serifFonts, ...monoFonts }}
defaultValue={DEFAULT_FONT_SANS}
currentFont={getAppliedThemeFont(themeState, "font-sans")}
onFontChange={(value) => updateStyle("font-sans", value)}
/>
</div>
<Separator className="my-4" />
<div className="mb-4">
<Label htmlFor="font-serif" className="text-xs mb-1.5 block">
Serif Font
</Label>
<ThemeFontSelect
fonts={{ ...serifFonts, ...sansSerifFonts, ...monoFonts }}
defaultValue={DEFAULT_FONT_SERIF}
currentFont={getAppliedThemeFont(themeState, "font-serif")}
onFontChange={(value) => updateStyle("font-serif", value)}
/>
</div>
<Separator className="my-4" />
<div>
<Label htmlFor="font-mono" className="text-xs mb-1.5 block">
Monospace Font
</Label>
<ThemeFontSelect
fonts={{ ...monoFonts, ...sansSerifFonts, ...serifFonts }}
defaultValue={DEFAULT_FONT_MONO}
currentFont={getAppliedThemeFont(themeState, "font-mono")}
onFontChange={(value) => updateStyle("font-mono", value)}
/>
</div>
</ControlSection>
<ControlSection title="Letter Spacing" expanded>
<SliderWithInput
value={parseFloat(
currentStyles["letter-spacing"]?.replace("em", "")
)}
onChange={(value) => updateStyle("letter-spacing", `${value}em`)}
min={-0.5}
max={0.5}
step={0.025}
unit="em"
label="Letter Spacing"
/>
</ControlSection>
</TabsContent>
<TabsContent value="other">
<ControlSection title="Radius" expanded>
<SliderWithInput
value={radius}
onChange={(value) => updateStyle("radius", `${value}rem`)}
min={0}
max={5}
step={0.025}
unit="rem"
label="Radius"
/>
</ControlSection>
<ControlSection title="Spacing" expanded>
<SliderWithInput
value={parseFloat(currentStyles.spacing?.replace("rem", ""))}
onChange={(value) => updateStyle("spacing", `${value}rem`)}
min={0.15}
max={0.35}
step={0.01}
unit="rem"
label="Spacing"
/>
</ControlSection>
<div className="mt-6">
<ShadowControl
shadowColor={currentStyles["shadow-color"]}
shadowOpacity={parseFloat(currentStyles["shadow-opacity"])}
shadowBlur={parseFloat(
currentStyles["shadow-blur"]?.replace("px", "")
)}
shadowSpread={parseFloat(
currentStyles["shadow-spread"]?.replace("px", "")
)}
shadowOffsetX={parseFloat(
currentStyles["shadow-offset-x"]?.replace("px", "")
)}
shadowOffsetY={parseFloat(
currentStyles["shadow-offset-y"]?.replace("px", "")
)}
onChange={(key, value) => {
if (key === "shadow-color") {
updateStyle(key, value);
} else if (key === "shadow-opacity") {
updateStyle(key, value.toString());
} else {
updateStyle(key as keyof ThemeStyleProps, `${value}px`);
}
}}
/>
</div>
</TabsContent>
</ScrollArea>
</Tabs>
<CssImportDialog
open={cssImportOpen}
onOpenChange={setCssImportOpen}
onImport={handleCssImport}
/>
</div>
);
};
export default ThemeControlPanel;
+53
View File
@@ -0,0 +1,53 @@
import React, { useMemo } from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface ThemeFontSelectProps {
fonts: Record<string, string>;
defaultValue: string;
currentFont: string | null;
onFontChange: (font: string) => void;
}
const ThemeFontSelect: React.FC<ThemeFontSelectProps> = ({
fonts,
defaultValue,
currentFont,
onFontChange,
}) => {
const fontNames = useMemo(() => ["System", ...Object.keys(fonts)], [fonts]);
const value = fonts[currentFont] ?? defaultValue;
return (
<Select value={value || ""} onValueChange={onFontChange}>
<div className="flex gap-1 items-center w-full">
<SelectTrigger className="w-full bg-secondary text-secondary-foreground">
<SelectValue placeholder="Select theme font" />
</SelectTrigger>
</div>
<SelectContent className="max-h-[400px]">
<SelectGroup>
{fontNames.map((fontName) => (
<SelectItem key={fontName} value={fonts[fontName] ?? defaultValue}>
<span
style={{
fontFamily: fonts[fontName] ?? defaultValue,
}}
>
{fontName}
</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
);
};
export default ThemeFontSelect;
+276
View File
@@ -0,0 +1,276 @@
import React, { useCallback, useMemo, useState } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ThemePreset } from "../../types/theme";
import { useEditorStore } from "../../store/editor-store";
import { getPresetThemeStyles } from "../../utils/theme-presets";
import { Button } from "../ui/button";
import {
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
Moon,
Search,
Shuffle,
Sun,
} from "lucide-react";
import { useTheme } from "@/components/theme-provider";
import { Separator } from "../ui/separator";
import { ScrollArea } from "../ui/scroll-area";
import { Command, CommandEmpty, CommandGroup, CommandItem } from "../ui/command";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { Input } from "../ui/input";
import { Badge } from "../ui/badge";
import { cn } from "@/lib/utils";
interface ThemePresetSelectProps {
presets: Record<string, ThemePreset>;
currentPreset: string | null;
onPresetChange: (preset: string) => void;
}
const ColorBox = ({ color }: { color: string }) => {
return (
<div
className="h-3 w-3 rounded-sm border border-muted"
style={{ backgroundColor: color }}
/>
);
};
const isThemeNew = (preset: ThemePreset) => {
if (!preset.createdAt) return false;
const createdAt = new Date(preset.createdAt);
const timePeriod = new Date();
timePeriod.setDate(timePeriod.getDate() - 5);
return createdAt > timePeriod;
};
const ThemePresetSelect: React.FC<ThemePresetSelectProps> = ({
presets,
currentPreset,
onPresetChange,
}) => {
const { themeState } = useEditorStore();
const { hasChangedThemeFromDefault } = useEditorStore();
const { theme, toggleTheme } = useTheme();
const mode = themeState.currentMode;
const [search, setSearch] = useState("");
const presetNames = useMemo(() => ["default", ...Object.keys(presets)], [presets]);
const value = presetNames?.find((name) => name === currentPreset);
const currentIndex = useMemo(
() => presetNames.indexOf(value || "default"),
[presetNames, value]
);
const randomize = useCallback(() => {
const random = Math.floor(Math.random() * presetNames.length);
onPresetChange(presetNames[random]);
}, [onPresetChange, presetNames]);
const cycleTheme = useCallback(
(direction: "prev" | "next") => {
const newIndex =
direction === "next"
? (currentIndex + 1) % presetNames.length
: (currentIndex - 1 + presetNames.length) % presetNames.length;
onPresetChange(presetNames[newIndex]);
},
[currentIndex, presetNames, onPresetChange]
);
const filteredPresets = useMemo(() => {
const filteredList =
search.trim() === ""
? presetNames
: presetNames.filter((name) =>
name.toLowerCase().includes(search.toLowerCase())
);
return filteredList.sort((a, b) => {
// Sort alphabetically
const labelA = presets[a]?.label || a;
const labelB = presets[b]?.label || b;
return labelA.localeCompare(labelB);
});
}, [presetNames, search, presets]);
const handleThemeToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
const { clientX: x, clientY: y } = event;
toggleTheme({ x, y });
};
return (
<div className="flex items-center gap-1">
<TooltipProvider>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full md:min-w-64 h-10 justify-between group relative",
(!value || value === "default") &&
!hasChangedThemeFromDefault &&
"ring-2 ring-offset-1 ring-offset-background ring-primary/30 animate-pulse"
)}
>
<div className="flex items-center gap-3">
<div className="flex gap-0.5">
<ColorBox
color={getPresetThemeStyles(value || "default")[mode].primary}
/>
<ColorBox
color={getPresetThemeStyles(value || "default")[mode].accent}
/>
<ColorBox
color={getPresetThemeStyles(value || "default")[mode].secondary}
/>
<ColorBox
color={getPresetThemeStyles(value || "default")[mode].border}
/>
</div>
<span className="capitalize font-medium">
{presets[value || "default"]?.label || "default"}
</span>
</div>
<ChevronDown className="size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[300px]" align="start">
<Command className="rounded-lg border shadow-md w-full">
<div className="flex items-center w-full">
<div className="flex items-center w-full border-b px-3 py-1">
<Search className="size-4 shrink-0 opacity-50" />
<Input
placeholder="Search themes..."
className="shadow-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="flex items-center justify-between px-4 py-2">
<div className="text-xs text-muted-foreground">
{filteredPresets.length} theme
{filteredPresets.length !== 1 ? "s" : ""}
</div>
<div className="flex gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={handleThemeToggle}
>
{theme === "light" ? (
<Sun className="h-3.5 w-3.5" />
) : (
<Moon className="h-3.5 w-3.5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p className="text-xs">Toggle theme</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={randomize}
>
<Shuffle className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p className="text-xs">Random theme</p>
</TooltipContent>
</Tooltip>
</div>
</div>
<Separator />
<ScrollArea className="h-[500px] max-h-[70vh]">
<CommandEmpty>No themes found.</CommandEmpty>
<CommandGroup>
{filteredPresets.map((presetName) => (
<CommandItem
key={presetName}
onSelect={() => {
onPresetChange(presetName);
setSearch("");
}}
className="flex items-center gap-2 py-2 hover:bg-secondary/50"
>
<div className="flex gap-0.5 mr-2">
<ColorBox
color={getPresetThemeStyles(presetName)[mode].primary}
/>
<ColorBox
color={getPresetThemeStyles(presetName)[mode].accent}
/>
<ColorBox
color={getPresetThemeStyles(presetName)[mode].secondary}
/>
<ColorBox
color={getPresetThemeStyles(presetName)[mode].border}
/>
</div>
<div className="flex items-center gap-2 flex-1">
<span className="capitalize text-sm font-medium">
{presets[presetName]?.label || presetName}
</span>
{presets[presetName] && isThemeNew(presets[presetName]) && (
<Badge
variant="secondary"
className="text-xs rounded-full"
>
New
</Badge>
)}
</div>
{presetName === value && (
<Check className="h-4 w-4 shrink-0 opacity-70" />
)}
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
</TooltipProvider>
<Button
variant="outline"
size="icon"
className="h-10 w-10 shrink-0"
title="Previous theme"
onClick={() => cycleTheme("prev")}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-10 w-10 shrink-0"
title="Next theme"
onClick={() => cycleTheme("next")}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
};
export default ThemePresetSelect;
+173
View File
@@ -0,0 +1,173 @@
import { ThemeEditorPreviewProps } from "@/types/theme";
import { Tabs, TabsContent, TabsList } from "@/components/ui/tabs";
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
import ColorPreview from "./theme-preview/color-preview";
import TabsTriggerPill from "./theme-preview/tabs-trigger-pill";
import ExamplesPreviewContainer from "./theme-preview/examples-preview-container";
import { lazy } from "react";
import { Button } from "@/components/ui/button";
import { Maximize, Minimize, PanelRight, Moon, Sun } from "lucide-react";
import { useFullscreen } from "@/hooks/use-fullscreen";
import { cn } from "@/lib/utils";
import { useTheme } from "@/components/theme-provider";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
const DemoCards = lazy(() => import("@/components/examples/demo-cards"));
const DemoMail = lazy(() => import("@/components/examples/mail"));
const DemoTasks = lazy(() => import("@/components/examples/tasks"));
const DemoMusic = lazy(() => import("@/components/examples/music"));
const DemoDashboard = lazy(() => import("@/components/examples/dashboard"));
const ThemePreviewPanel = ({
styles,
currentMode,
isCodePanelOpen,
onCodePanelToggle,
}: ThemeEditorPreviewProps) => {
const { isFullscreen, toggleFullscreen } = useFullscreen();
const { theme, toggleTheme } = useTheme();
if (!styles || !styles[currentMode]) {
return null;
}
const handleThemeToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
const { clientX: x, clientY: y } = event;
toggleTheme({ x, y });
};
return (
<div
className={cn(
"max-h-full flex flex-col",
isFullscreen && "fixed inset-0 z-50 bg-background p-4"
)}
>
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Theme Preview</h2>
<div className="flex items-center gap-0">
{isFullscreen && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleThemeToggle}
className="h-8 group"
>
{theme === "light" ? (
<Sun className="size-4 group-hover:scale-120 transition-all" />
) : (
<Moon className="size-4 group-hover:scale-120 transition-all" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>Toggle Theme</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={toggleFullscreen}
className="h-8 group"
>
{isFullscreen ? (
<Minimize className="size-4 group-hover:scale-120 transition-all" />
) : (
<Maximize className="size-4 group-hover:scale-120 transition-all" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
{isFullscreen ? "Exit full screen" : "Full screen"}
</TooltipContent>
</Tooltip>
{!isCodePanelOpen && !isFullscreen && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => onCodePanelToggle(!isCodePanelOpen)}
className="h-8 invisible md:visible group"
aria-label="Show Code Panel"
>
<PanelRight className="size-4 group-hover:scale-120 transition-all" />
</Button>
</TooltipTrigger>
<TooltipContent>Hide Code Panel</TooltipContent>
</Tooltip>
)}
</div>
</div>
<div className="flex flex-col flex-1 overflow-hidden">
<Tabs defaultValue="cards" className="flex flex-col overflow-hidden">
<TabsList className="inline-flex w-fit h-9 items-center justify-center rounded-full bg-background px-0 text-muted-foreground">
<TabsTriggerPill value="cards">Cards</TabsTriggerPill>
<div className="hidden md:flex">
<TabsTriggerPill value="mail">Mail</TabsTriggerPill>
<TabsTriggerPill value="tasks">Tasks</TabsTriggerPill>
<TabsTriggerPill value="music">Music</TabsTriggerPill>
<TabsTriggerPill value="dashboard">Dashboard</TabsTriggerPill>
</div>
<TabsTriggerPill value="colors">Color Palette</TabsTriggerPill>
</TabsList>
<ScrollArea className="rounded-lg border mt-2 flex flex-col flex-1">
<div className="flex flex-col flex-1">
<TabsContent value="cards" className="space-y-6 mt-0 py-4 px-4 h-full">
<ExamplesPreviewContainer>
<DemoCards />
</ExamplesPreviewContainer>
</TabsContent>
<TabsContent value="mail" className="space-y-6 mt-0 h-full @container">
<ExamplesPreviewContainer className="min-w-[1300px]">
<DemoMail />
</ExamplesPreviewContainer>
</TabsContent>
<TabsContent
value="tasks"
className="space-y-6 mt-0 h-full @container"
>
<ExamplesPreviewContainer className="min-w-[1300px]">
<DemoTasks />
</ExamplesPreviewContainer>
</TabsContent>
<TabsContent
value="music"
className="space-y-6 mt-0 h-full @container"
>
<ExamplesPreviewContainer className="min-w-[1300px]">
<DemoMusic />
</ExamplesPreviewContainer>
</TabsContent>
<TabsContent
value="dashboard"
className="space-y-6 mt-0 h-full @container relative"
>
<ExamplesPreviewContainer className="min-w-[1400px]">
<DemoDashboard />
</ExamplesPreviewContainer>
</TabsContent>
<TabsContent value="colors" className="p-4 space-y-6">
<ColorPreview styles={styles} currentMode={currentMode} />
</TabsContent>
<ScrollBar orientation="horizontal" />
</div>
</ScrollArea>
</Tabs>
</div>
</div>
);
};
export default ThemePreviewPanel;
@@ -0,0 +1,160 @@
import { ThemeEditorPreviewProps } from "@/types/theme";
interface ColorPreviewProps {
styles: ThemeEditorPreviewProps["styles"];
currentMode: ThemeEditorPreviewProps["currentMode"];
}
const renderColorPreview = (label: string, color: string) => (
<div className="flex items-center gap-4">
<div
className="w-12 h-12 rounded-md border"
style={{ backgroundColor: color }}
/>
<div className="flex-1">
<p className="text-sm font-medium">{label}</p>
<p className="text-xs text-muted-foreground">{color}</p>
</div>
</div>
);
const ColorPreview = ({ styles, currentMode }: ColorPreviewProps) => {
if (!styles || !styles[currentMode]) {
return null;
}
return (
<div className="grid grid-cols-1 gap-8">
{/* Primary Colors */}
<div className="space-y-4">
<h3 className="text-sm font-medium border-b pb-2">Primary Theme Colors</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderColorPreview("Background", styles[currentMode].background)}
{renderColorPreview("Foreground", styles[currentMode].foreground)}
{renderColorPreview("Primary", styles[currentMode].primary)}
{renderColorPreview(
"Primary Foreground",
styles[currentMode]["primary-foreground"]
)}
</div>
</div>
{/* Secondary & Accent Colors */}
<div className="space-y-4">
<h3 className="text-sm font-medium border-b pb-2">
Secondary & Accent Colors
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderColorPreview("Secondary", styles[currentMode].secondary)}
{renderColorPreview(
"Secondary Foreground",
styles[currentMode]["secondary-foreground"]
)}
{renderColorPreview("Accent", styles[currentMode].accent)}
{renderColorPreview(
"Accent Foreground",
styles[currentMode]["accent-foreground"]
)}
</div>
</div>
{/* UI Component Colors */}
<div className="space-y-4">
<h3 className="text-sm font-medium border-b pb-2">UI Component Colors</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderColorPreview("Card", styles[currentMode].card)}
{renderColorPreview(
"Card Foreground",
styles[currentMode]["card-foreground"]
)}
{renderColorPreview("Popover", styles[currentMode].popover)}
{renderColorPreview(
"Popover Foreground",
styles[currentMode]["popover-foreground"]
)}
{renderColorPreview("Muted", styles[currentMode].muted)}
{renderColorPreview(
"Muted Foreground",
styles[currentMode]["muted-foreground"]
)}
</div>
</div>
{/* Utility & Form Colors */}
<div className="space-y-4">
<h3 className="text-sm font-medium border-b pb-2">Utility & Form Colors</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderColorPreview("Border", styles[currentMode].border)}
{renderColorPreview("Input", styles[currentMode].input)}
{renderColorPreview("Ring", styles[currentMode].ring)}
{renderColorPreview("Radius", styles[currentMode].radius)}
</div>
</div>
{/* Status & Feedback Colors */}
<div className="space-y-4">
<h3 className="text-sm font-medium border-b pb-2">
Status & Feedback Colors
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderColorPreview("Destructive", styles[currentMode].destructive)}
{renderColorPreview(
"Destructive Foreground",
styles[currentMode]["destructive-foreground"]
)}
</div>
</div>
{/* Chart & Data Visualization Colors */}
<div className="space-y-4">
<h3 className="text-sm font-medium border-b pb-2">
Chart & Visualization Colors
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderColorPreview("Chart 1", styles[currentMode]["chart-1"])}
{renderColorPreview("Chart 2", styles[currentMode]["chart-2"])}
{renderColorPreview("Chart 3", styles[currentMode]["chart-3"])}
{renderColorPreview("Chart 4", styles[currentMode]["chart-4"])}
{renderColorPreview("Chart 5", styles[currentMode]["chart-5"])}
</div>
</div>
{/* Sidebar Colors */}
<div className="space-y-4">
<h3 className="text-sm font-medium border-b pb-2">
Sidebar & Navigation Colors
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderColorPreview("Sidebar Background", styles[currentMode].sidebar)}
{renderColorPreview(
"Sidebar Foreground",
styles[currentMode]["sidebar-foreground"]
)}
{renderColorPreview(
"Sidebar Primary",
styles[currentMode]["sidebar-primary"]
)}
{renderColorPreview(
"Sidebar Primary Foreground",
styles[currentMode]["sidebar-primary-foreground"]
)}
{renderColorPreview(
"Sidebar Accent",
styles[currentMode]["sidebar-accent"]
)}
{renderColorPreview(
"Sidebar Accent Foreground",
styles[currentMode]["sidebar-accent-foreground"]
)}
{renderColorPreview(
"Sidebar Border",
styles[currentMode]["sidebar-border"]
)}
{renderColorPreview("Sidebar Ring", styles[currentMode]["sidebar-ring"])}
</div>
</div>
</div>
);
};
export default ColorPreview;
@@ -0,0 +1,218 @@
import { ThemeEditorPreviewProps } from "@/types/theme";
import { Settings, Info, AlertTriangle, Star } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Table,
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell,
} from "@/components/ui/table";
interface ComponentsShowcaseProps {
styles: ThemeEditorPreviewProps["styles"];
currentMode: ThemeEditorPreviewProps["currentMode"];
}
const ComponentsShowcase = ({ styles, currentMode }: ComponentsShowcaseProps) => {
if (!styles || !styles[currentMode]) {
return null;
}
return (
<div className="space-y-6">
{/* Button showcase */}
<section className="space-y-3">
<h3 className="text-sm font-medium border-b pb-2">
Buttons & Interactive Elements
</h3>
<div className="space-y-4">
<div className="flex flex-wrap gap-3">
<Button variant="default">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Delete</Button>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Switch id="notifications" />
<label htmlFor="notifications">Notifications</label>
</div>
<div className="flex items-center space-x-2">
<Switch id="darkmode" />
<label htmlFor="darkmode">Dark Mode</label>
</div>
</div>
</div>
</section>
{/* Cards & Containers */}
<section className="space-y-3">
<h3 className="text-sm font-medium border-b pb-2">Cards & Containers</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Feature Card</CardTitle>
<CardDescription>
Card description with muted foreground color
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm">
This card demonstrates the card background and foreground colors,
with content showing regular text.
</p>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="ghost">Cancel</Button>
<Button>Continue</Button>
</CardFooter>
</Card>
<div className="space-y-3">
<div
className="rounded-lg p-4"
style={{
backgroundColor: styles[currentMode].popover,
color: styles[currentMode]["popover-foreground"],
border: `1px solid ${styles[currentMode].border}`,
}}
>
<h4 className="text-sm font-medium mb-2">Popover Container</h4>
<p className="text-xs">
This container shows popover colors and styling.
</p>
</div>
<div
className="rounded-lg p-4"
style={{
backgroundColor: styles[currentMode].muted,
color: styles[currentMode]["muted-foreground"],
}}
>
<h4 className="text-sm font-medium mb-2">Muted Container</h4>
<p className="text-xs">
Container with muted background and foreground colors.
</p>
</div>
</div>
</div>
</section>
{/* Status Indicators */}
<section className="space-y-3">
<h3 className="text-sm font-medium border-b pb-2">
Status Indicators & Alerts
</h3>
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Badge>Default Badge</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="destructive">Error</Badge>
<Badge className="bg-blue-500 hover:bg-blue-600">Custom</Badge>
</div>
<div className="space-y-3">
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>Information</AlertTitle>
<AlertDescription>
Standard alert with default styling.
</AlertDescription>
</Alert>
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Destructive alert showcasing error state colors.
</AlertDescription>
</Alert>
<div
className="rounded-lg border p-4 flex items-start gap-3"
style={{
borderColor: styles[currentMode].border,
backgroundColor: `${styles[currentMode].accent}20`,
}}
>
<Star className="h-5 w-5 text-yellow-500 shrink-0" />
<div>
<h5 className="font-medium text-sm">Success Alert</h5>
<p className="text-xs mt-1">
Custom alert using accent colors with an opacity modifier.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Data Display */}
<section className="space-y-3">
<h3 className="text-sm font-medium border-b pb-2">Data Display</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Status</TableHead>
<TableHead>Role</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">Alex Johnson</TableCell>
<TableCell>
<Badge variant="outline" className="bg-green-500/10 text-green-600">
Active
</Badge>
</TableCell>
<TableCell>Admin</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
<Settings className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Sarah Chen</TableCell>
<TableCell>
<Badge
variant="outline"
className="bg-destructive/10 text-destructive"
>
Inactive
</Badge>
</TableCell>
<TableCell>User</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
<Settings className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</section>
</div>
);
};
export default ComponentsShowcase;
@@ -0,0 +1,30 @@
import { Suspense } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
const LoadingSkeleton = () => (
<div className="flex w-fit p-4 flex-col space-y-3 min-h-full">
<Skeleton className="h-[225px] w-full rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
);
const ExamplesPreviewContainer = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<div className={cn("space-y-6", className)}>
<div className="space-y-6 mt-0 h-full">
<Suspense fallback={<LoadingSkeleton />}>{children}</Suspense>
</div>
</div>
);
};
export default ExamplesPreviewContainer;
@@ -0,0 +1,15 @@
import { TabsTrigger } from "@/components/ui/tabs";
import { TabsTriggerProps } from "@radix-ui/react-tabs";
const TabsTriggerPill = ({ children, ...props }: TabsTriggerProps) => {
return (
<TabsTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-full px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-secondary data-[state=active]:text-secondary-foreground hover:text-muted-foreground/70"
{...props}
>
{children}
</TabsTrigger>
);
};
export default TabsTriggerPill;
+246
View File
@@ -0,0 +1,246 @@
import * as React from "react";
import { Check, Plus, Send } from "lucide-react";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const users = [
{
name: "Olivia Martin",
email: "m@example.com",
avatar: "/avatars/01.png",
},
{
name: "Isabella Nguyen",
email: "isabella.nguyen@email.com",
avatar: "/avatars/03.png",
},
{
name: "Emma Wilson",
email: "emma@example.com",
avatar: "/avatars/05.png",
},
{
name: "Jackson Lee",
email: "lee@example.com",
avatar: "/avatars/02.png",
},
{
name: "William Kim",
email: "will@email.com",
avatar: "/avatars/04.png",
},
] as const;
type User = (typeof users)[number];
export function DemoChat() {
const [open, setOpen] = React.useState(false);
const [selectedUsers, setSelectedUsers] = React.useState<User[]>([]);
const [messages, setMessages] = React.useState([
{
role: "agent",
content: "Hi, how can I help you today?",
},
{
role: "user",
content: "Hey, I'm having trouble with my account.",
},
{
role: "agent",
content: "What seems to be the problem?",
},
{
role: "user",
content: "I can't log in.",
},
]);
const [input, setInput] = React.useState("");
const inputLength = input.trim().length;
return (
<>
<Card>
<CardHeader className="flex flex-row items-center">
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src="/avatars/01.png" alt="Image" />
<AvatarFallback>OM</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium leading-none">Sofia Davis</p>
<p className="text-sm text-muted-foreground">m@example.com</p>
</div>
</div>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="outline"
className="ml-auto rounded-full"
onClick={() => setOpen(true)}
>
<Plus />
<span className="sr-only">New message</span>
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={10}>New message</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardHeader>
<CardContent>
<div className="space-y-4">
{messages.map((message, index) => (
<div
key={index}
className={cn(
"flex w-max max-w-[75%] flex-col gap-2 rounded-lg px-3 py-2 text-sm",
message.role === "user"
? "ml-auto bg-primary text-primary-foreground"
: "bg-muted",
)}
>
{message.content}
</div>
))}
</div>
</CardContent>
<CardFooter>
<form
onSubmit={(event) => {
event.preventDefault();
if (inputLength === 0) return;
setMessages([
...messages,
{
role: "user",
content: input,
},
]);
setInput("");
}}
className="flex w-full items-center space-x-2"
>
<Input
id="message"
placeholder="Type your message..."
className="flex-1"
autoComplete="off"
value={input}
onChange={(event) => setInput(event.target.value)}
/>
<Button type="submit" size="icon" disabled={inputLength === 0}>
<Send />
<span className="sr-only">Send</span>
</Button>
</form>
</CardFooter>
</Card>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="gap-0 p-0 outline-hidden">
<DialogHeader className="px-4 pb-4 pt-5">
<DialogTitle>New message</DialogTitle>
<DialogDescription>
Invite a user to this thread. This will create a new group message.
</DialogDescription>
</DialogHeader>
<Command className="overflow-hidden rounded-t-none border-t bg-transparent">
<CommandInput placeholder="Search user..." />
<CommandList>
<CommandEmpty>No users found.</CommandEmpty>
<CommandGroup className="p-2">
{users.map((user) => (
<CommandItem
key={user.email}
className="flex items-center px-2"
onSelect={() => {
if (selectedUsers.includes(user)) {
return setSelectedUsers(
selectedUsers.filter(
(selectedUser) => selectedUser !== user,
),
);
}
return setSelectedUsers(
[...users].filter((u) =>
[...selectedUsers, user].includes(u),
),
);
}}
>
<Avatar>
<AvatarImage src={user.avatar} alt="Image" />
<AvatarFallback>{user.name[0]}</AvatarFallback>
</Avatar>
<div className="ml-2">
<p className="text-sm font-medium leading-none">{user.name}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
{selectedUsers.includes(user) ? (
<Check className="ml-auto flex h-5 w-5 text-primary" />
) : null}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
<DialogFooter className="flex items-center border-t p-4 sm:justify-between">
{selectedUsers.length > 0 ? (
<div className="flex -space-x-2 overflow-hidden">
{selectedUsers.map((user) => (
<Avatar
key={user.email}
className="inline-block border-2 border-background"
>
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.name[0]}</AvatarFallback>
</Avatar>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">
Select users to add to this thread.
</p>
)}
<Button
disabled={selectedUsers.length < 2}
onClick={() => {
setOpen(false);
}}
>
Continue
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
@@ -0,0 +1,57 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
export function DemoCookieSettings() {
return (
<Card>
<CardHeader>
<CardTitle>Cookie Settings</CardTitle>
<CardDescription>Manage your cookie settings here.</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="necessary" className="flex flex-col space-y-1">
<span>Strictly Necessary</span>
<span className="font-normal leading-snug text-muted-foreground">
These cookies are essential in order to use the website and use its
features.
</span>
</Label>
<Switch id="necessary" defaultChecked />
</div>
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="functional" className="flex flex-col space-y-1">
<span>Functional Cookies</span>
<span className="font-normal leading-snug text-muted-foreground">
These cookies allow the website to provide personalized functionality.
</span>
</Label>
<Switch id="functional" />
</div>
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="performance" className="flex flex-col space-y-1">
<span>Performance Cookies</span>
<span className="font-normal leading-snug text-muted-foreground">
These cookies help to improve the performance of the website.
</span>
</Label>
<Switch id="performance" />
</div>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full">
Save preferences
</Button>
</CardFooter>
</Card>
);
}
@@ -0,0 +1,58 @@
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function DemoCreateAccount() {
return (
<Card>
<CardHeader className="space-y-1">
<CardTitle className="text-2xl">Create an account</CardTitle>
<CardDescription>
Enter your email below to create your account
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid grid-cols-2 gap-6">
<Button variant="outline">
<Icons.gitHub className="mr-2 h-4 w-4" />
Github
</Button>
<Button variant="outline">
<Icons.google className="mr-2 h-4 w-4" />
Google
</Button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="m@example.com" />
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" />
</div>
</CardContent>
<CardFooter>
<Button className="w-full">Create account</Button>
</CardFooter>
</Card>
);
}
@@ -0,0 +1,58 @@
import * as React from "react";
import { addDays, format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
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 default function DatePickerWithRange({
className,
}: React.HTMLAttributes<HTMLDivElement>) {
const [date, setDate] = React.useState<DateRange | undefined>({
from: new Date(2022, 0, 20),
to: addDays(new Date(2022, 0, 20), 20),
});
return (
<div className={cn("grid gap-2", className)}>
<Popover>
<PopoverTrigger asChild>
<Button
id="date"
variant={"outline"}
className={cn(
"w-[300px] justify-start text-left font-normal",
!date && "text-muted-foreground",
)}
>
<CalendarIcon />
{date?.from ? (
date.to ? (
<>
{format(date.from, "LLL dd, y")} - {format(date.to, "LLL dd, y")}
</>
) : (
format(date.from, "LLL dd, y")
)
) : (
<span>Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
initialFocus
mode="range"
defaultMonth={date?.from}
selected={date}
onSelect={setDate}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import DatePickerWithRange from "./date-picker-with-range";
export function DemoDatePicker() {
return (
<Card>
<CardContent className="pt-6">
<div className="space-y-2">
<Label htmlFor="date" className="shrink-0">
Pick a date
</Label>
<DatePickerWithRange className="[&>button]:w-[260px]" />
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,52 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export function DemoFontShowcase() {
return (
<Card>
<CardHeader>
<CardTitle>Font Showcase</CardTitle>
<CardDescription>View theme fonts in different styles</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<div>
<h3 className="text-lg font-semibold mb-2">Sans-Serif</h3>
<div className="font-sans space-y-1">
<div className="text-xl font-light">Light Weight Text</div>
<div className="text-xl">Regular Weight Text</div>
<div className="text-xl font-medium">Medium Weight Text</div>
<div className="text-xl font-semibold">Semibold Weight Text</div>
<div className="text-xl font-bold">Bold Weight Text</div>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">Serif</h3>
<div className="font-serif space-y-1">
<div className="text-xl font-light">Light Weight Text</div>
<div className="text-xl">Regular Weight Text</div>
<div className="text-xl font-medium">Medium Weight Text</div>
<div className="text-xl font-semibold">Semibold Weight Text</div>
<div className="text-xl font-bold">Bold Weight Text</div>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">Monospace</h3>
<div className="font-mono space-y-1">
<div className="text-xl font-light">Light Weight Text</div>
<div className="text-xl">Regular Weight Text</div>
<div className="text-xl font-medium">Medium Weight Text</div>
<div className="text-xl font-semibold">Semibold Weight Text</div>
<div className="text-xl font-bold">Bold Weight Text</div>
</div>
</div>
</CardContent>
</Card>
);
}
+84
View File
@@ -0,0 +1,84 @@
import { ChevronDown, Circle, Plus, Star } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import Link from "next/link";
export function DemoGithub() {
return (
<Card>
<CardHeader className="grid grid-cols-[1fr_110px] items-start gap-4 space-y-0">
<div className="space-y-1">
<CardTitle>tweakcn</CardTitle>
<CardDescription>
A visual editor for shadcn/ui components with beautiful themes.
Accessible. Customizable. Open Source.
</CardDescription>
</div>
<div className="flex items-center space-x-1 rounded-md bg-secondary text-secondary-foreground">
<Link href="https://github.com/jnsahaj/tweakcn">
<Button variant="secondary" className="px-3 shadow-none">
<Star />
Star
</Button>
</Link>
<Separator orientation="vertical" className="h-[20px]" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" className="px-2 shadow-none">
<ChevronDown className="text-secondary-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
alignOffset={-5}
className="w-[200px]"
forceMount
>
<DropdownMenuLabel>Suggested Lists</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>
Future Ideas
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>My Stack</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Inspiration</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Plus /> Create List
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent>
<div className="flex space-x-4 text-sm text-muted-foreground">
<div className="flex items-center">
<Circle className="mr-1 h-3 w-3 fill-sky-400 text-sky-400" />
TypeScript
</div>
<div className="flex items-center">
<Star className="mr-1 h-3 w-3" />
20k
</div>
<div>Updated April 2023</div>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,47 @@
import { Bell, EyeOff, User } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export function DemoNotifications() {
return (
<Card>
<CardHeader className="pb-3">
<CardTitle>Notifications</CardTitle>
<CardDescription>Choose what you want to be notified about.</CardDescription>
</CardHeader>
<CardContent className="grid gap-1">
<div className="-mx-2 flex items-start space-x-4 rounded-md p-2 transition-all hover:bg-accent hover:text-accent-foreground">
<Bell className="mt-px h-5 w-5" />
<div className="space-y-1">
<p className="text-sm font-medium leading-none">Everything</p>
<p className="text-sm text-muted-foreground">
Email digest, mentions & all activity.
</p>
</div>
</div>
<div className="-mx-2 flex items-start space-x-4 rounded-md bg-accent p-2 text-accent-foreground transition-all">
<User className="mt-px h-5 w-5" />
<div className="space-y-1">
<p className="text-sm font-medium leading-none">Available</p>
<p className="text-sm">Only mentions and comments.</p>
</div>
</div>
<div className="-mx-2 flex items-start space-x-4 rounded-md p-2 transition-all hover:bg-accent hover:text-accent-foreground">
<EyeOff className="mt-px h-5 w-5" />
<div className="space-y-1">
<p className="text-sm font-medium leading-none">Ignoring</p>
<p className="text-sm text-muted-foreground">
Turn off all notifications.
</p>
</div>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,131 @@
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export function DemoPaymentMethod() {
return (
<Card>
<CardHeader>
<CardTitle>Payment Method</CardTitle>
<CardDescription>Add a new payment method to your account.</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<RadioGroup defaultValue="card" className="grid grid-cols-3 gap-4">
<div>
<RadioGroupItem value="card" id="card" className="peer sr-only" />
<Label
htmlFor="card"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="mb-3 h-6 w-6"
>
<rect width="20" height="14" x="2" y="5" rx="2" />
<path d="M2 10h20" />
</svg>
Card
</Label>
</div>
<div>
<RadioGroupItem value="paypal" id="paypal" className="peer sr-only" />
<Label
htmlFor="paypal"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<Icons.paypal className="mb-3 h-6 w-6" />
Paypal
</Label>
</div>
<div>
<RadioGroupItem value="apple" id="apple" className="peer sr-only" />
<Label
htmlFor="apple"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<Icons.apple className="mb-3 h-6 w-6" />
Apple
</Label>
</div>
</RadioGroup>
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="First Last" />
</div>
<div className="grid gap-2">
<Label htmlFor="number">Card number</Label>
<Input id="number" placeholder="" />
</div>
<div className="grid grid-cols-3 gap-4">
<div className="grid gap-2">
<Label htmlFor="month">Expires</Label>
<Select>
<SelectTrigger id="month">
<SelectValue placeholder="Month" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">January</SelectItem>
<SelectItem value="2">February</SelectItem>
<SelectItem value="3">March</SelectItem>
<SelectItem value="4">April</SelectItem>
<SelectItem value="5">May</SelectItem>
<SelectItem value="6">June</SelectItem>
<SelectItem value="7">July</SelectItem>
<SelectItem value="8">August</SelectItem>
<SelectItem value="9">September</SelectItem>
<SelectItem value="10">October</SelectItem>
<SelectItem value="11">November</SelectItem>
<SelectItem value="12">December</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="year">Year</Label>
<Select>
<SelectTrigger id="year">
<SelectValue placeholder="Year" />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 10 }, (_, i) => (
<SelectItem key={i} value={`${new Date().getFullYear() + i}`}>
{new Date().getFullYear() + i}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="cvc">CVC</Label>
<Input id="cvc" placeholder="CVC" />
</div>
</div>
</CardContent>
<CardFooter>
<Button className="w-full">Continue</Button>
</CardFooter>
</Card>
);
}
@@ -0,0 +1,78 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
export function DemoReportAnIssue() {
return (
<Card>
<CardHeader>
<CardTitle>Report an issue</CardTitle>
<CardDescription>What area are you having problems with?</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="area">Area</Label>
<Select defaultValue="billing">
<SelectTrigger id="area">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="team">Team</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
<SelectItem value="account">Account</SelectItem>
<SelectItem value="deployments">Deployments</SelectItem>
<SelectItem value="support">Support</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="security-level">Security Level</Label>
<Select defaultValue="2">
<SelectTrigger id="security-level">
<SelectValue placeholder="Select level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Severity 1 (Highest)</SelectItem>
<SelectItem value="2">Severity 2</SelectItem>
<SelectItem value="3">Severity 3</SelectItem>
<SelectItem value="4">Severity 4 (Lowest)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="subject">Subject</Label>
<Input id="subject" placeholder="I need help with..." />
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Please include all information relevant to your issue."
/>
</div>
</CardContent>
<CardFooter className="justify-between space-x-2">
<Button variant="ghost">Cancel</Button>
<Button>Submit</Button>
</CardFooter>
</Card>
);
}
@@ -0,0 +1,108 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
export function DemoShareDocument() {
return (
<Card>
<CardHeader>
<CardTitle>Share this document</CardTitle>
<CardDescription>
Anyone with the link can view this document.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex space-x-2">
<Input value="http://example.com/link/to/document" readOnly />
<Button variant="secondary" className="shrink-0">
Copy Link
</Button>
</div>
<Separator className="my-4" />
<div className="space-y-4">
<div className="text-sm font-medium">People with access</div>
<div className="grid gap-6">
<div className="flex items-center justify-between space-x-4">
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src="/avatars/03.png" />
<AvatarFallback>OM</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium leading-none">Olivia Martin</p>
<p className="text-sm text-muted-foreground">m@example.com</p>
</div>
</div>
<Select defaultValue="edit">
<SelectTrigger className="ml-auto w-[110px]">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="edit">Can edit</SelectItem>
<SelectItem value="view">Can view</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between space-x-4">
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src="/avatars/05.png" />
<AvatarFallback>IN</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium leading-none">Isabella Nguyen</p>
<p className="text-sm text-muted-foreground">b@example.com</p>
</div>
</div>
<Select defaultValue="view">
<SelectTrigger className="ml-auto w-[110px]">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="edit">Can edit</SelectItem>
<SelectItem value="view">Can view</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between space-x-4">
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src="/avatars/01.png" />
<AvatarFallback>SD</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium leading-none">Sofia Davis</p>
<p className="text-sm text-muted-foreground">p@example.com</p>
</div>
</div>
<Select defaultValue="view">
<SelectTrigger className="ml-auto w-[110px]">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="edit">Can edit</SelectItem>
<SelectItem value="view">Can view</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</CardContent>
</Card>
);
}
+105
View File
@@ -0,0 +1,105 @@
import { Bar, BarChart, Line, LineChart } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
const data = [
{
revenue: 10400,
subscription: 240,
},
{
revenue: 14405,
subscription: 300,
},
{
revenue: 9400,
subscription: 200,
},
{
revenue: 8200,
subscription: 278,
},
{
revenue: 7000,
subscription: 189,
},
{
revenue: 9600,
subscription: 239,
},
{
revenue: 11244,
subscription: 278,
},
{
revenue: 26475,
subscription: 189,
},
];
const chartConfig = {
revenue: {
label: "Revenue",
color: "var(--primary)",
},
subscription: {
label: "Subscriptions",
color: "var(--primary)",
},
} satisfies ChartConfig;
export function DemoStats() {
return (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-2">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-normal">Total Revenue</CardTitle>
</CardHeader>
<CardContent className="pb-0">
<div className="text-2xl font-bold">$15,231.89</div>
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
<ChartContainer config={chartConfig} className="h-[80px] w-full">
<LineChart
data={data}
margin={{
top: 5,
right: 10,
left: 10,
bottom: 0,
}}
>
<Line
type="monotone"
strokeWidth={2}
dataKey="revenue"
stroke="var(--color-revenue)"
activeDot={{
r: 6,
}}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-normal">Subscriptions</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">+2350</div>
<p className="text-xs text-muted-foreground">+180.1% from last month</p>
<ChartContainer config={chartConfig} className="mt-2 h-[80px] w-full">
<BarChart data={data}>
<Bar
dataKey="subscription"
fill="var(--color-subscription)"
radius={4}
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
</div>
);
}
+139
View File
@@ -0,0 +1,139 @@
import { ChevronDown } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export function DemoTeamMembers() {
return (
<Card>
<CardHeader>
<CardTitle>Team Members</CardTitle>
<CardDescription>Invite your team members to collaborate.</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<div className="flex items-center justify-between space-x-4">
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src="/avatars/01.png" />
<AvatarFallback>OM</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium leading-none">Sofia Davis</p>
<p className="text-sm text-muted-foreground">m@example.com</p>
</div>
</div>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="ml-auto">
Owner <ChevronDown className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="end">
<Command>
<CommandInput placeholder="Select new role..." />
<CommandList>
<CommandEmpty>No roles found.</CommandEmpty>
<CommandGroup>
<CommandItem className="teamaspace-y-1 flex flex-col items-start px-4 py-2">
<p>Viewer</p>
<p className="text-sm text-muted-foreground">
Can view and comment.
</p>
</CommandItem>
<CommandItem className="teamaspace-y-1 flex flex-col items-start px-4 py-2">
<p>Developer</p>
<p className="text-sm text-muted-foreground">
Can view, comment and edit.
</p>
</CommandItem>
<CommandItem className="teamaspace-y-1 flex flex-col items-start px-4 py-2">
<p>Billing</p>
<p className="text-sm text-muted-foreground">
Can view, comment and manage billing.
</p>
</CommandItem>
<CommandItem className="teamaspace-y-1 flex flex-col items-start px-4 py-2">
<p>Owner</p>
<p className="text-sm text-muted-foreground">
Admin-level access to all resources.
</p>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center justify-between space-x-4">
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src="/avatars/02.png" />
<AvatarFallback>JL</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium leading-none">Jackson Lee</p>
<p className="text-sm text-muted-foreground">p@example.com</p>
</div>
</div>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="ml-auto">
Member <ChevronDown className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="end">
<Command>
<CommandInput placeholder="Select new role..." />
<CommandList>
<CommandEmpty>No roles found.</CommandEmpty>
<CommandGroup className="p-1.5">
<CommandItem className="teamaspace-y-1 flex flex-col items-start px-4 py-2">
<p>Viewer</p>
<p className="text-sm text-muted-foreground">
Can view and comment.
</p>
</CommandItem>
<CommandItem className="teamaspace-y-1 flex flex-col items-start px-4 py-2">
<p>Developer</p>
<p className="text-sm text-muted-foreground">
Can view, comment and edit.
</p>
</CommandItem>
<CommandItem className="teamaspace-y-1 flex flex-col items-start px-4 py-2">
<p>Billing</p>
<p className="text-sm text-muted-foreground">
Can view, comment and manage billing.
</p>
</CommandItem>
<CommandItem className="teamaspace-y-1 flex flex-col items-start px-4 py-2">
<p>Owner</p>
<p className="text-sm text-muted-foreground">
Admin-level access to all resources.
</p>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,179 @@
import * as React from "react";
import {
ArrowUpCircleIcon,
BarChartIcon,
CameraIcon,
ClipboardListIcon,
DatabaseIcon,
FileCodeIcon,
FileIcon,
FileTextIcon,
FolderIcon,
HelpCircleIcon,
LayoutDashboardIcon,
ListIcon,
SearchIcon,
SettingsIcon,
UsersIcon,
} from "lucide-react";
import { NavDocuments } from "@/components/examples/dashboard/components/nav-documents";
import { NavMain } from "@/components/examples/dashboard/components/nav-main";
import { NavSecondary } from "@/components/examples/dashboard/components/nav-secondary";
import { NavUser } from "@/components/examples/dashboard/components/nav-user";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Dashboard",
url: "#",
icon: LayoutDashboardIcon,
},
{
title: "Lifecycle",
url: "#",
icon: ListIcon,
},
{
title: "Analytics",
url: "#",
icon: BarChartIcon,
},
{
title: "Projects",
url: "#",
icon: FolderIcon,
},
{
title: "Team",
url: "#",
icon: UsersIcon,
},
],
navClouds: [
{
title: "Capture",
icon: CameraIcon,
isActive: true,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Proposal",
icon: FileTextIcon,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Prompts",
icon: FileCodeIcon,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
],
navSecondary: [
{
title: "Settings",
url: "#",
icon: SettingsIcon,
},
{
title: "Get Help",
url: "#",
icon: HelpCircleIcon,
},
{
title: "Search",
url: "#",
icon: SearchIcon,
},
],
documents: [
{
name: "Data Library",
url: "#",
icon: DatabaseIcon,
},
{
name: "Reports",
url: "#",
icon: ClipboardListIcon,
},
{
name: "Word Assistant",
url: "#",
icon: FileIcon,
},
],
};
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="offcanvas" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
asChild
className="data-[slot=sidebar-menu-button]:!p-1.5"
>
<a href="#">
<ArrowUpCircleIcon className="h-5 w-5" />
<span className="text-base font-semibold">Acme Inc.</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavDocuments items={data.documents} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
);
}
@@ -0,0 +1,290 @@
import * as React from "react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import { useIsMobile } from "@/hooks/use-mobile";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ToggleGroup, ToggleGroupItem } from "@/components/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(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
} satisfies ChartConfig;
export function ChartAreaInteractive() {
const isMobile = useIsMobile();
const [timeRange, setTimeRange] = React.useState("30d");
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 className="relative">
<CardTitle>Total Visitors</CardTitle>
<CardDescription>
<span className="@[540px]/card:block hidden">
Total for the last 3 months
</span>
<span className="@[540px]/card:hidden">Last 3 months</span>
</CardDescription>
<div className="absolute right-4 top-4">
<ToggleGroup
type="single"
value={timeRange}
onValueChange={setTimeRange}
variant="outline"
className="@[767px]/card:flex hidden"
>
<ToggleGroupItem value="90d" className="h-8 px-2.5">
Last 3 months
</ToggleGroupItem>
<ToggleGroupItem value="30d" className="h-8 px-2.5">
Last 30 days
</ToggleGroupItem>
<ToggleGroupItem value="7d" className="h-8 px-2.5">
Last 7 days
</ToggleGroupItem>
</ToggleGroup>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger
className="@[767px]/card:hidden flex w-40"
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>
</div>
</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}
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>
);
}
@@ -0,0 +1,760 @@
import * as React from "react";
import {
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
closestCenter,
useSensor,
useSensors,
type DragEndEvent,
type UniqueIdentifier,
} from "@dnd-kit/core";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import {
SortableContext,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
ColumnDef,
ColumnFiltersState,
Row,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
CheckCircle2Icon,
CheckCircleIcon,
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
ChevronsLeftIcon,
ChevronsRightIcon,
ColumnsIcon,
GripVerticalIcon,
LoaderIcon,
MoreVerticalIcon,
PlusIcon,
TrendingUpIcon,
} from "lucide-react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import { toast } from "sonner";
import { z } from "zod";
import { useIsMobile } from "@/hooks/use-mobile";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export const schema = z.object({
id: z.number(),
header: z.string(),
type: z.string(),
target: z.string(),
limit: z.string(),
reviewer: z.string(),
});
// Create a separate component for the drag handle
function DragHandle({ id }: { id: number }) {
const { attributes, listeners } = useSortable({
id,
});
return (
<Button
{...attributes}
{...listeners}
variant="ghost"
size="icon"
className="size-7 text-muted-foreground hover:bg-transparent"
>
<GripVerticalIcon className="size-3 text-muted-foreground" />
<span className="sr-only">Drag to reorder</span>
</Button>
);
}
const columns: ColumnDef<z.infer<typeof schema>>[] = [
{
id: "drag",
header: () => null,
cell: ({ row }) => <DragHandle id={row.original.id} />,
},
{
id: "select",
header: ({ table }) => (
<div className="flex items-center justify-center">
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() ? "indeterminate" : false)
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
</div>
),
cell: ({ row }) => (
<div className="flex items-center justify-center">
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
</div>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "header",
header: "Header",
cell: ({ row }) => {
return <TableCellViewer item={row.original} />;
},
enableHiding: false,
},
{
accessorKey: "type",
header: "Section Type",
cell: ({ row }) => (
<div className="w-32">
<Badge variant="outline" className="px-1.5 text-muted-foreground">
{row.original.type}
</Badge>
</div>
),
},
{
accessorKey: "target",
header: () => <div className="w-full text-right">Target</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault();
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
});
}}
>
<Label htmlFor={`${row.original.id}-target`} className="sr-only">
Target
</Label>
<Input
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background"
defaultValue={row.original.target}
id={`${row.original.id}-target`}
/>
</form>
),
},
{
accessorKey: "limit",
header: () => <div className="w-full text-right">Limit</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault();
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
});
}}
>
<Label htmlFor={`${row.original.id}-limit`} className="sr-only">
Limit
</Label>
<Input
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background"
defaultValue={row.original.limit}
id={`${row.original.id}-limit`}
/>
</form>
),
},
{
accessorKey: "reviewer",
header: "Reviewer",
cell: ({ row }) => {
const isAssigned = row.original.reviewer !== "Assign reviewer";
if (isAssigned) {
return row.original.reviewer;
}
return (
<>
<Label htmlFor={`${row.original.id}-reviewer`} className="sr-only">
Reviewer
</Label>
<Select>
<SelectTrigger className="h-8 w-40" id={`${row.original.id}-reviewer`}>
<SelectValue placeholder="Assign reviewer" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
<SelectItem value="Jamik Tashpulatov">Jamik Tashpulatov</SelectItem>
</SelectContent>
</Select>
</>
);
},
},
{
id: "actions",
cell: () => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
size="icon"
>
<MoreVerticalIcon />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({
id: row.original.id,
});
return (
<TableRow
data-state={row.getIsSelected() && "selected"}
data-dragging={isDragging}
ref={setNodeRef}
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
style={{
transform: CSS.Transform.toString(transform),
transition: transition,
}}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
);
}
export function DataTable({
data: initialData,
}: {
data: z.infer<typeof schema>[];
}) {
const [data, setData] = React.useState(() => initialData);
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
{}
);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
});
const sortableId = React.useId();
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
);
const dataIds = React.useMemo<UniqueIdentifier[]>(
() => data?.map(({ id }) => id) || [],
[data]
);
const table = useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
pagination,
},
getRowId: (row) => row.id.toString(),
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
});
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (active && over && active.id !== over.id) {
setData((data) => {
const oldIndex = dataIds.indexOf(active.id);
const newIndex = dataIds.indexOf(over.id);
return arrayMove(data, oldIndex, newIndex);
});
}
}
return (
<Tabs
defaultValue="outline"
className="flex w-full flex-col justify-start gap-6"
>
<div className="flex items-center justify-between px-4 lg:px-6">
<Label htmlFor="view-selector" className="sr-only">
View
</Label>
<Select defaultValue="outline">
<SelectTrigger className="@4xl/main:hidden flex w-fit" id="view-selector">
<SelectValue placeholder="Select a view" />
</SelectTrigger>
<SelectContent>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="past-performance">Past Performance</SelectItem>
<SelectItem value="key-personnel">Key Personnel</SelectItem>
<SelectItem value="focus-documents">Focus Documents</SelectItem>
</SelectContent>
</Select>
<TabsList className="@4xl/main:flex hidden">
<TabsTrigger value="outline">Outline</TabsTrigger>
<TabsTrigger value="past-performance" className="gap-1">
Past Performance{" "}
<Badge
variant="secondary"
className="flex h-5 w-5 items-center justify-center rounded-full bg-muted-foreground/30"
>
3
</Badge>
</TabsTrigger>
<TabsTrigger value="key-personnel" className="gap-1">
Key Personnel{" "}
<Badge
variant="secondary"
className="flex h-5 w-5 items-center justify-center rounded-full bg-muted-foreground/30"
>
2
</Badge>
</TabsTrigger>
<TabsTrigger value="focus-documents">Focus Documents</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<ColumnsIcon />
<span className="hidden lg:inline">Customize Columns</span>
<span className="lg:hidden">Columns</span>
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm">
<PlusIcon />
<span className="hidden lg:inline">Add Section</span>
</Button>
</div>
</div>
<TabsContent
value="outline"
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
>
<div className="overflow-hidden rounded-lg border">
<DndContext
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}
id={sortableId}
>
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted">
{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">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DndContext>
</div>
<div className="flex items-center justify-between px-4">
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex w-full items-center gap-8 lg:w-fit">
<div className="hidden items-center gap-2 lg:flex">
<Label htmlFor="rows-per-page" className="text-sm font-medium">
Rows per page
</Label>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="w-20" id="rows-per-page">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeftIcon />
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon />
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon />
</Button>
<Button
variant="outline"
className="hidden size-8 lg:flex"
size="icon"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRightIcon />
</Button>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="past-performance" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent value="focus-documents" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
</Tabs>
);
}
const chartData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 },
];
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--primary)",
},
mobile: {
label: "Mobile",
color: "var(--primary)",
},
} satisfies ChartConfig;
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
const isMobile = useIsMobile();
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="link" className="w-fit px-0 text-left text-foreground">
{item.header}
</Button>
</SheetTrigger>
<SheetContent side="right" className="flex flex-col">
<SheetHeader className="gap-1">
<SheetTitle>{item.header}</SheetTitle>
<SheetDescription>
Showing total visitors for the last 6 months
</SheetDescription>
</SheetHeader>
<div className="flex flex-1 flex-col gap-4 overflow-y-auto py-4 text-sm">
{!isMobile && (
<>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 10,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
hide
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.6}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
<Separator />
<div className="grid gap-2">
<div className="flex gap-2 font-medium leading-none">
Trending up by 5.2% this month{" "}
<TrendingUpIcon className="size-4" />
</div>
<div className="text-muted-foreground">
Showing total visitors for the last 6 months. This is just some
random text to test the layout. It spans multiple lines and should
wrap around.
</div>
</div>
<Separator />
</>
)}
<form className="flex flex-col gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="header">Header</Label>
<Input id="header" defaultValue={item.header} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="type">Type</Label>
<Select defaultValue={item.type}>
<SelectTrigger id="type" className="w-full">
<SelectValue placeholder="Select a type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Table of Contents">
Table of Contents
</SelectItem>
<SelectItem value="Executive Summary">
Executive Summary
</SelectItem>
<SelectItem value="Technical Approach">
Technical Approach
</SelectItem>
<SelectItem value="Design">Design</SelectItem>
<SelectItem value="Capabilities">Capabilities</SelectItem>
<SelectItem value="Focus Documents">Focus Documents</SelectItem>
<SelectItem value="Narrative">Narrative</SelectItem>
<SelectItem value="Cover Page">Cover Page</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="target">Target</Label>
<Input id="target" defaultValue={item.target} />
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="limit">Limit</Label>
<Input id="limit" defaultValue={item.limit} />
</div>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="reviewer">Reviewer</Label>
<Select defaultValue={item.reviewer}>
<SelectTrigger id="reviewer" className="w-full">
<SelectValue placeholder="Select a reviewer" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
<SelectItem value="Jamik Tashpulatov">
Jamik Tashpulatov
</SelectItem>
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
</SelectContent>
</Select>
</div>
</form>
</div>
<SheetFooter className="mt-auto flex gap-2 sm:flex-col sm:space-x-0">
<Button className="w-full">Submit</Button>
<SheetClose asChild>
<Button variant="outline" className="w-full">
Done
</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,83 @@
import {
FolderIcon,
MoreHorizontalIcon,
ShareIcon,
type LucideIcon,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
export function NavDocuments({
items,
}: {
items: {
name: string;
url: string;
icon: LucideIcon;
}[];
}) {
const { isMobile } = useSidebar();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Documents</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="rounded-sm data-[state=open]:bg-accent"
>
<MoreHorizontalIcon />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-24 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<FolderIcon />
<span>Open</span>
</DropdownMenuItem>
<DropdownMenuItem>
<ShareIcon />
<span>Share</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontalIcon className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}
@@ -0,0 +1,49 @@
"use client";
import { PlusCircleIcon, type LucideIcon } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
export function NavMain({
items,
}: {
items: {
title: string;
url: string;
icon?: LucideIcon;
}[];
}) {
return (
<SidebarGroup>
<SidebarGroupContent className="flex flex-col gap-2">
<SidebarMenu>
<SidebarMenuItem className="flex items-center gap-2">
<SidebarMenuButton
tooltip="Quick Create"
className="min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
>
<PlusCircleIcon />
<span>Quick Create</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}
@@ -0,0 +1,42 @@
"use client";
import * as React from "react";
import { LucideIcon } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
export function NavSecondary({
items,
...props
}: {
items: {
title: string;
url: string;
icon: LucideIcon;
}[];
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}
@@ -0,0 +1,106 @@
"use client";
import {
BellIcon,
CreditCardIcon,
LogOutIcon,
MoreVerticalIcon,
UserCircleIcon,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
export function NavUser({
user,
}: {
user: {
name: string;
email: string;
avatar: string;
};
}) {
const { isMobile } = useSidebar();
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg grayscale">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
<MoreVerticalIcon className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<UserCircleIcon />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCardIcon />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<BellIcon />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOutIcon />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}
@@ -0,0 +1,97 @@
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export function SectionCards() {
return (
<div className="*:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @5xl/main:grid-cols-4 grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card lg:px-6">
<Card className="@container/card">
<CardHeader className="relative">
<CardDescription>Total Revenue</CardDescription>
<CardTitle className="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
$1,250.00
</CardTitle>
<div className="absolute right-4 top-4">
<Badge variant="outline" className="flex gap-1 rounded-lg text-xs">
<TrendingUpIcon className="size-3" />
+12.5%
</Badge>
</div>
</CardHeader>
<CardFooter className="flex-col items-start gap-1 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Trending up this month <TrendingUpIcon className="size-4" />
</div>
<div className="text-muted-foreground">Visitors for the last 6 months</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader className="relative">
<CardDescription>New Customers</CardDescription>
<CardTitle className="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
1,234
</CardTitle>
<div className="absolute right-4 top-4">
<Badge variant="outline" className="flex gap-1 rounded-lg text-xs">
<TrendingDownIcon className="size-3" />
-20%
</Badge>
</div>
</CardHeader>
<CardFooter className="flex-col items-start gap-1 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Down 20% this period <TrendingDownIcon className="size-4" />
</div>
<div className="text-muted-foreground">Acquisition needs attention</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader className="relative">
<CardDescription>Active Accounts</CardDescription>
<CardTitle className="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
45,678
</CardTitle>
<div className="absolute right-4 top-4">
<Badge variant="outline" className="flex gap-1 rounded-lg text-xs">
<TrendingUpIcon className="size-3" />
+12.5%
</Badge>
</div>
</CardHeader>
<CardFooter className="flex-col items-start gap-1 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Strong user retention <TrendingUpIcon className="size-4" />
</div>
<div className="text-muted-foreground">Engagement exceed targets</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader className="relative">
<CardDescription>Growth Rate</CardDescription>
<CardTitle className="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
4.5%
</CardTitle>
<div className="absolute right-4 top-4">
<Badge variant="outline" className="flex gap-1 rounded-lg text-xs">
<TrendingUpIcon className="size-3" />
+4.5%
</Badge>
</div>
</CardHeader>
<CardFooter className="flex-col items-start gap-1 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Steady performance <TrendingUpIcon className="size-4" />
</div>
<div className="text-muted-foreground">Meets growth projections</div>
</CardFooter>
</Card>
</div>
);
}
@@ -0,0 +1,17 @@
import { Separator } from "@/components/ui/separator";
import { SidebarTrigger } from "@/components/ui/sidebar";
export function SiteHeader() {
return (
<header className="group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 flex h-12 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear">
<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>
</header>
);
}
+614
View File
@@ -0,0 +1,614 @@
[
{
"id": 1,
"header": "Cover page",
"type": "Cover page",
"status": "In Process",
"target": "18",
"limit": "5",
"reviewer": "Eddie Lake"
},
{
"id": 2,
"header": "Table of contents",
"type": "Table of contents",
"status": "Done",
"target": "29",
"limit": "24",
"reviewer": "Eddie Lake"
},
{
"id": 3,
"header": "Executive summary",
"type": "Narrative",
"status": "Done",
"target": "10",
"limit": "13",
"reviewer": "Eddie Lake"
},
{
"id": 4,
"header": "Technical approach",
"type": "Narrative",
"status": "Done",
"target": "27",
"limit": "23",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 5,
"header": "Design",
"type": "Narrative",
"status": "In Process",
"target": "2",
"limit": "16",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 6,
"header": "Capabilities",
"type": "Narrative",
"status": "In Process",
"target": "20",
"limit": "8",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 7,
"header": "Integration with existing systems",
"type": "Narrative",
"status": "In Process",
"target": "19",
"limit": "21",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 8,
"header": "Innovation and Advantages",
"type": "Narrative",
"status": "Done",
"target": "25",
"limit": "26",
"reviewer": "Assign reviewer"
},
{
"id": 9,
"header": "Overview of EMR's Innovative Solutions",
"type": "Technical content",
"status": "Done",
"target": "7",
"limit": "23",
"reviewer": "Assign reviewer"
},
{
"id": 10,
"header": "Advanced Algorithms and Machine Learning",
"type": "Narrative",
"status": "Done",
"target": "30",
"limit": "28",
"reviewer": "Assign reviewer"
},
{
"id": 11,
"header": "Adaptive Communication Protocols",
"type": "Narrative",
"status": "Done",
"target": "9",
"limit": "31",
"reviewer": "Assign reviewer"
},
{
"id": 12,
"header": "Advantages Over Current Technologies",
"type": "Narrative",
"status": "Done",
"target": "12",
"limit": "0",
"reviewer": "Assign reviewer"
},
{
"id": 13,
"header": "Past Performance",
"type": "Narrative",
"status": "Done",
"target": "22",
"limit": "33",
"reviewer": "Assign reviewer"
},
{
"id": 14,
"header": "Customer Feedback and Satisfaction Levels",
"type": "Narrative",
"status": "Done",
"target": "15",
"limit": "34",
"reviewer": "Assign reviewer"
},
{
"id": 15,
"header": "Implementation Challenges and Solutions",
"type": "Narrative",
"status": "Done",
"target": "3",
"limit": "35",
"reviewer": "Assign reviewer"
},
{
"id": 16,
"header": "Security Measures and Data Protection Policies",
"type": "Narrative",
"status": "In Process",
"target": "6",
"limit": "36",
"reviewer": "Assign reviewer"
},
{
"id": 17,
"header": "Scalability and Future Proofing",
"type": "Narrative",
"status": "Done",
"target": "4",
"limit": "37",
"reviewer": "Assign reviewer"
},
{
"id": 18,
"header": "Cost-Benefit Analysis",
"type": "Plain language",
"status": "Done",
"target": "14",
"limit": "38",
"reviewer": "Assign reviewer"
},
{
"id": 19,
"header": "User Training and Onboarding Experience",
"type": "Narrative",
"status": "Done",
"target": "17",
"limit": "39",
"reviewer": "Assign reviewer"
},
{
"id": 20,
"header": "Future Development Roadmap",
"type": "Narrative",
"status": "Done",
"target": "11",
"limit": "40",
"reviewer": "Assign reviewer"
},
{
"id": 21,
"header": "System Architecture Overview",
"type": "Technical content",
"status": "In Process",
"target": "24",
"limit": "18",
"reviewer": "Maya Johnson"
},
{
"id": 22,
"header": "Risk Management Plan",
"type": "Narrative",
"status": "Done",
"target": "15",
"limit": "22",
"reviewer": "Carlos Rodriguez"
},
{
"id": 23,
"header": "Compliance Documentation",
"type": "Legal",
"status": "In Process",
"target": "31",
"limit": "27",
"reviewer": "Sarah Chen"
},
{
"id": 24,
"header": "API Documentation",
"type": "Technical content",
"status": "Done",
"target": "8",
"limit": "12",
"reviewer": "Raj Patel"
},
{
"id": 25,
"header": "User Interface Mockups",
"type": "Visual",
"status": "In Process",
"target": "19",
"limit": "25",
"reviewer": "Leila Ahmadi"
},
{
"id": 26,
"header": "Database Schema",
"type": "Technical content",
"status": "Done",
"target": "22",
"limit": "20",
"reviewer": "Thomas Wilson"
},
{
"id": 27,
"header": "Testing Methodology",
"type": "Technical content",
"status": "In Process",
"target": "17",
"limit": "14",
"reviewer": "Assign reviewer"
},
{
"id": 28,
"header": "Deployment Strategy",
"type": "Narrative",
"status": "Done",
"target": "26",
"limit": "30",
"reviewer": "Eddie Lake"
},
{
"id": 29,
"header": "Budget Breakdown",
"type": "Financial",
"status": "In Process",
"target": "13",
"limit": "16",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 30,
"header": "Market Analysis",
"type": "Research",
"status": "Done",
"target": "29",
"limit": "32",
"reviewer": "Sophia Martinez"
},
{
"id": 31,
"header": "Competitor Comparison",
"type": "Research",
"status": "In Process",
"target": "21",
"limit": "19",
"reviewer": "Assign reviewer"
},
{
"id": 32,
"header": "Maintenance Plan",
"type": "Technical content",
"status": "Done",
"target": "16",
"limit": "23",
"reviewer": "Alex Thompson"
},
{
"id": 33,
"header": "User Personas",
"type": "Research",
"status": "In Process",
"target": "27",
"limit": "24",
"reviewer": "Nina Patel"
},
{
"id": 34,
"header": "Accessibility Compliance",
"type": "Legal",
"status": "Done",
"target": "18",
"limit": "21",
"reviewer": "Assign reviewer"
},
{
"id": 35,
"header": "Performance Metrics",
"type": "Technical content",
"status": "In Process",
"target": "23",
"limit": "26",
"reviewer": "David Kim"
},
{
"id": 36,
"header": "Disaster Recovery Plan",
"type": "Technical content",
"status": "Done",
"target": "14",
"limit": "17",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 37,
"header": "Third-party Integrations",
"type": "Technical content",
"status": "In Process",
"target": "25",
"limit": "28",
"reviewer": "Eddie Lake"
},
{
"id": 38,
"header": "User Feedback Summary",
"type": "Research",
"status": "Done",
"target": "20",
"limit": "15",
"reviewer": "Assign reviewer"
},
{
"id": 39,
"header": "Localization Strategy",
"type": "Narrative",
"status": "In Process",
"target": "12",
"limit": "19",
"reviewer": "Maria Garcia"
},
{
"id": 40,
"header": "Mobile Compatibility",
"type": "Technical content",
"status": "Done",
"target": "28",
"limit": "31",
"reviewer": "James Wilson"
},
{
"id": 41,
"header": "Data Migration Plan",
"type": "Technical content",
"status": "In Process",
"target": "19",
"limit": "22",
"reviewer": "Assign reviewer"
},
{
"id": 42,
"header": "Quality Assurance Protocols",
"type": "Technical content",
"status": "Done",
"target": "30",
"limit": "33",
"reviewer": "Priya Singh"
},
{
"id": 43,
"header": "Stakeholder Analysis",
"type": "Research",
"status": "In Process",
"target": "11",
"limit": "14",
"reviewer": "Eddie Lake"
},
{
"id": 44,
"header": "Environmental Impact Assessment",
"type": "Research",
"status": "Done",
"target": "24",
"limit": "27",
"reviewer": "Assign reviewer"
},
{
"id": 45,
"header": "Intellectual Property Rights",
"type": "Legal",
"status": "In Process",
"target": "17",
"limit": "20",
"reviewer": "Sarah Johnson"
},
{
"id": 46,
"header": "Customer Support Framework",
"type": "Narrative",
"status": "Done",
"target": "22",
"limit": "25",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 47,
"header": "Version Control Strategy",
"type": "Technical content",
"status": "In Process",
"target": "15",
"limit": "18",
"reviewer": "Assign reviewer"
},
{
"id": 48,
"header": "Continuous Integration Pipeline",
"type": "Technical content",
"status": "Done",
"target": "26",
"limit": "29",
"reviewer": "Michael Chen"
},
{
"id": 49,
"header": "Regulatory Compliance",
"type": "Legal",
"status": "In Process",
"target": "13",
"limit": "16",
"reviewer": "Assign reviewer"
},
{
"id": 50,
"header": "User Authentication System",
"type": "Technical content",
"status": "Done",
"target": "28",
"limit": "31",
"reviewer": "Eddie Lake"
},
{
"id": 51,
"header": "Data Analytics Framework",
"type": "Technical content",
"status": "In Process",
"target": "21",
"limit": "24",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 52,
"header": "Cloud Infrastructure",
"type": "Technical content",
"status": "Done",
"target": "16",
"limit": "19",
"reviewer": "Assign reviewer"
},
{
"id": 53,
"header": "Network Security Measures",
"type": "Technical content",
"status": "In Process",
"target": "29",
"limit": "32",
"reviewer": "Lisa Wong"
},
{
"id": 54,
"header": "Project Timeline",
"type": "Planning",
"status": "Done",
"target": "14",
"limit": "17",
"reviewer": "Eddie Lake"
},
{
"id": 55,
"header": "Resource Allocation",
"type": "Planning",
"status": "In Process",
"target": "27",
"limit": "30",
"reviewer": "Assign reviewer"
},
{
"id": 56,
"header": "Team Structure and Roles",
"type": "Planning",
"status": "Done",
"target": "20",
"limit": "23",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 57,
"header": "Communication Protocols",
"type": "Planning",
"status": "In Process",
"target": "15",
"limit": "18",
"reviewer": "Assign reviewer"
},
{
"id": 58,
"header": "Success Metrics",
"type": "Planning",
"status": "Done",
"target": "30",
"limit": "33",
"reviewer": "Eddie Lake"
},
{
"id": 59,
"header": "Internationalization Support",
"type": "Technical content",
"status": "In Process",
"target": "23",
"limit": "26",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 60,
"header": "Backup and Recovery Procedures",
"type": "Technical content",
"status": "Done",
"target": "18",
"limit": "21",
"reviewer": "Assign reviewer"
},
{
"id": 61,
"header": "Monitoring and Alerting System",
"type": "Technical content",
"status": "In Process",
"target": "25",
"limit": "28",
"reviewer": "Daniel Park"
},
{
"id": 62,
"header": "Code Review Guidelines",
"type": "Technical content",
"status": "Done",
"target": "12",
"limit": "15",
"reviewer": "Eddie Lake"
},
{
"id": 63,
"header": "Documentation Standards",
"type": "Technical content",
"status": "In Process",
"target": "27",
"limit": "30",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 64,
"header": "Release Management Process",
"type": "Planning",
"status": "Done",
"target": "22",
"limit": "25",
"reviewer": "Assign reviewer"
},
{
"id": 65,
"header": "Feature Prioritization Matrix",
"type": "Planning",
"status": "In Process",
"target": "19",
"limit": "22",
"reviewer": "Emma Davis"
},
{
"id": 66,
"header": "Technical Debt Assessment",
"type": "Technical content",
"status": "Done",
"target": "24",
"limit": "27",
"reviewer": "Eddie Lake"
},
{
"id": 67,
"header": "Capacity Planning",
"type": "Planning",
"status": "In Process",
"target": "21",
"limit": "24",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 68,
"header": "Service Level Agreements",
"type": "Legal",
"status": "Done",
"target": "26",
"limit": "29",
"reviewer": "Assign reviewer"
}
]
+30
View File
@@ -0,0 +1,30 @@
import { AppSidebar } from "@/components/examples/dashboard/components/app-sidebar";
import { ChartAreaInteractive } from "@/components/examples/dashboard/components/chart-area-interactive";
import { DataTable } from "@/components/examples/dashboard/components/data-table";
import { SectionCards } from "@/components/examples/dashboard/components/section-cards";
import { SiteHeader } from "@/components/examples/dashboard/components/site-header";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import data from "./data.json";
export default function Page() {
return (
<SidebarProvider>
<AppSidebar variant="inset" />
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col">
<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">
<SectionCards />
<div className="px-4 lg:px-6">
<ChartAreaInteractive />
</div>
<DataTable data={data} />
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
);
}
+79
View File
@@ -0,0 +1,79 @@
import { cn } from "@/lib/utils";
import { DemoCookieSettings } from "./cards/cookie-settings";
import { DemoCreateAccount } from "./cards/create-account";
import { DemoDatePicker } from "./cards/date-picker";
import { DemoGithub } from "./cards/github-card";
import { DemoNotifications } from "./cards/notifications";
import { DemoPaymentMethod } from "./cards/payment-method";
import { DemoReportAnIssue } from "./cards/report-an-issue";
import { DemoShareDocument } from "./cards/share-document";
import { DemoTeamMembers } from "./cards/team-members";
import { DemoStats } from "./cards/stats";
import { DemoChat } from "./cards/chat";
import { DemoFontShowcase } from "./cards/font-showcase";
export function DemoContainer({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("flex items-center justify-center [&>div]:w-full", className)}
{...props}
/>
);
}
function DemoCards() {
return (
<div className="@container">
<div className="flex flex-col @4xl:flex-row items-center @4xl:items-start justify-center gap-4">
{/* First column */}
<div className="flex flex-col gap-4 max-w-lg">
<DemoContainer>
<DemoStats />
</DemoContainer>
<DemoContainer>
<DemoCreateAccount />
</DemoContainer>
<DemoContainer>
<DemoGithub />
</DemoContainer>
<DemoContainer>
<DemoCookieSettings />
</DemoContainer>
<DemoContainer>
<DemoTeamMembers />
</DemoContainer>
<DemoContainer>
<DemoFontShowcase />
</DemoContainer>
</div>
{/* Third column */}
<div className="flex flex-col gap-4 max-w-lg">
<DemoContainer>
<DemoReportAnIssue />
</DemoContainer>
<DemoContainer>
<DemoPaymentMethod />
</DemoContainer>
<DemoContainer>
<DemoShareDocument />
</DemoContainer>
<DemoContainer>
<DemoNotifications />
</DemoContainer>
<DemoContainer>
<DemoChat />
</DemoContainer>
<DemoContainer>
<DemoDatePicker />
</DemoContainer>
</div>
</div>
</div>
);
}
export default DemoCards;
@@ -0,0 +1,55 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface AccountSwitcherProps {
isCollapsed: boolean;
accounts: {
label: string;
email: string;
icon: React.ReactNode;
}[];
}
export function AccountSwitcher({ isCollapsed, accounts }: AccountSwitcherProps) {
const [selectedAccount, setSelectedAccount] = React.useState<string>(
accounts[0].email
);
return (
<Select defaultValue={selectedAccount} onValueChange={setSelectedAccount}>
<SelectTrigger
className={cn(
"flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0",
isCollapsed &&
"flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden"
)}
aria-label="Select account"
>
<SelectValue placeholder="Select an account">
{accounts.find((account) => account.email === selectedAccount)?.icon}
<span className={cn("ml-2", isCollapsed && "hidden")}>
{accounts.find((account) => account.email === selectedAccount)?.label}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
{accounts.map((account) => (
<SelectItem key={account.email} value={account.email}>
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
{account.icon}
{account.email}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
}
@@ -0,0 +1,189 @@
import { addDays } from "date-fns";
import { addHours } from "date-fns";
import { format } from "date-fns";
import { nextSaturday } from "date-fns";
import {
Archive,
ArchiveX,
Clock,
Forward,
MoreVertical,
Reply,
ReplyAll,
Trash2,
} from "lucide-react";
import {
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { DropdownMenu, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { Mail } from "@/components/examples/mail/data";
interface MailDisplayProps {
mail: Mail | null;
}
export function MailDisplay({ mail }: MailDisplayProps) {
const today = new Date();
return (
<div className="flex h-full flex-col">
<div className="flex items-center p-2">
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" disabled={!mail} title="Archive">
<Archive className="h-4 w-4" />
<span className="sr-only">Archive</span>
</Button>
<Button variant="ghost" size="icon" disabled={!mail} title="Move to junk">
<ArchiveX className="h-4 w-4" />
<span className="sr-only">Move to junk</span>
</Button>
<Button variant="ghost" size="icon" disabled={!mail} title="Move to trash">
<Trash2 className="h-4 w-4" />
<span className="sr-only">Move to trash</span>
</Button>
<Separator orientation="vertical" className="mx-1 h-6" />
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail} title="Snooze">
<Clock className="h-4 w-4" />
<span className="sr-only">Snooze</span>
</Button>
</PopoverTrigger>
<PopoverContent className="flex w-[535px] p-0">
<div className="flex flex-col gap-2 border-r px-2 py-4">
<div className="px-4 text-sm font-medium">Snooze until</div>
<div className="grid min-w-[250px] gap-1">
<Button variant="ghost" className="justify-start font-normal">
Later today{" "}
<span className="ml-auto text-muted-foreground">
{format(addHours(today, 4), "E, h:m b")}
</span>
</Button>
<Button variant="ghost" className="justify-start font-normal">
Tomorrow
<span className="ml-auto text-muted-foreground">
{format(addDays(today, 1), "E, h:m b")}
</span>
</Button>
<Button variant="ghost" className="justify-start font-normal">
This weekend
<span className="ml-auto text-muted-foreground">
{format(nextSaturday(today), "E, h:m b")}
</span>
</Button>
<Button variant="ghost" className="justify-start font-normal">
Next week
<span className="ml-auto text-muted-foreground">
{format(addDays(today, 7), "E, h:m b")}
</span>
</Button>
</div>
</div>
<div className="p-2">
<Calendar />
</div>
</PopoverContent>
</Popover>
</div>
<div className="ml-auto flex items-center gap-2">
<Button variant="ghost" size="icon" disabled={!mail} title="Reply">
<Reply className="h-4 w-4" />
<span className="sr-only">Reply</span>
</Button>
<Button variant="ghost" size="icon" disabled={!mail} title="Reply all">
<ReplyAll className="h-4 w-4" />
<span className="sr-only">Reply all</span>
</Button>
<Button variant="ghost" size="icon" disabled={!mail} title="Forward">
<Forward className="h-4 w-4" />
<span className="sr-only">Forward</span>
</Button>
</div>
<Separator orientation="vertical" className="mx-2 h-6" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail}>
<MoreVertical className="h-4 w-4" />
<span className="sr-only">More</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Mark as unread</DropdownMenuItem>
<DropdownMenuItem>Star thread</DropdownMenuItem>
<DropdownMenuItem>Add label</DropdownMenuItem>
<DropdownMenuItem>Mute thread</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Separator />
{mail ? (
<div className="flex flex-1 flex-col">
<div className="flex items-start p-4">
<div className="flex items-start gap-4 text-sm">
<Avatar>
<AvatarImage alt={mail.name} />
<AvatarFallback>
{mail.name
.split(" ")
.map((chunk) => chunk[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="grid gap-1">
<div className="font-semibold">{mail.name}</div>
<div className="line-clamp-1 text-xs">{mail.subject}</div>
<div className="line-clamp-1 text-xs">
<span className="font-medium">Reply-To:</span> {mail.email}
</div>
</div>
</div>
{mail.date && (
<div className="ml-auto text-xs text-muted-foreground">
{format(new Date(mail.date), "PPpp")}
</div>
)}
</div>
<Separator />
<div className="flex-1 whitespace-pre-wrap p-4 text-sm">{mail.text}</div>
<Separator className="mt-auto" />
<div className="p-4">
<form>
<div className="grid gap-4">
<Textarea className="p-4" placeholder={`Reply ${mail.name}...`} />
<div className="flex items-center">
<Label
htmlFor="mute"
className="flex items-center gap-2 text-xs font-normal"
>
<Switch id="mute" aria-label="Mute thread" /> Mute this thread
</Label>
<Button
onClick={(e) => e.preventDefault()}
size="sm"
className="ml-auto"
>
Send
</Button>
</div>
</div>
</form>
</div>
</div>
) : (
<div className="p-8 text-center text-muted-foreground">
No message selected
</div>
)}
</div>
);
}
@@ -0,0 +1,88 @@
import { ComponentProps } from "react";
import { formatDistanceToNow } from "date-fns";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Mail } from "@/components/examples/mail/data";
import { useMail } from "@/components/examples/mail/use-mail";
interface MailListProps {
items: Mail[];
}
export function MailList({ items }: MailListProps) {
const [mail, setMail] = useMail();
return (
<ScrollArea className="h-screen">
<div className="flex flex-col gap-2 p-4 pt-0">
{items.map((item) => (
<button
key={item.id}
className={cn(
"flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent hover:text-accent-foreground",
mail.selected === item.id && "bg-muted"
)}
onClick={() =>
setMail({
...mail,
selected: item.id,
})
}
>
<div className="flex w-full flex-col gap-1">
<div className="flex items-center">
<div className="flex items-center gap-2">
<div className="font-semibold">{item.name}</div>
{!item.read && (
<span className="flex h-2 w-2 rounded-full bg-blue-600" />
)}
</div>
<div
className={cn(
"ml-auto text-xs",
mail.selected === item.id
? "text-foreground"
: "text-muted-foreground"
)}
>
{formatDistanceToNow(new Date(item.date), {
addSuffix: true,
})}
</div>
</div>
<div className="text-xs font-medium">{item.subject}</div>
</div>
<div className="line-clamp-2 text-xs text-muted-foreground">
{item.text.substring(0, 300)}
</div>
{item.labels.length ? (
<div className="flex items-center gap-2">
{item.labels.map((label) => (
<Badge key={label} variant={getBadgeVariantFromLabel(label)}>
{label}
</Badge>
))}
</div>
) : null}
</button>
))}
</div>
</ScrollArea>
);
}
function getBadgeVariantFromLabel(
label: string
): ComponentProps<typeof Badge>["variant"] {
if (["work"].includes(label.toLowerCase())) {
return "default";
}
if (["personal"].includes(label.toLowerCase())) {
return "outline";
}
return "secondary";
}
@@ -0,0 +1,211 @@
import * as React from "react";
import {
AlertCircle,
Archive,
ArchiveX,
File,
Inbox,
MessagesSquare,
Search,
Send,
ShoppingCart,
Trash2,
Users2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { TooltipProvider } from "@/components/ui/tooltip";
import { AccountSwitcher } from "@/components/examples/mail/components/account-switcher";
import { MailDisplay } from "@/components/examples/mail/components/mail-display";
import { MailList } from "@/components/examples/mail/components/mail-list";
import { Nav } from "@/components/examples/mail/components/nav";
import { type Mail } from "@/components/examples/mail/data";
import { useMail } from "@/components/examples/mail/use-mail";
interface MailProps {
accounts: {
label: string;
email: string;
icon: React.ReactNode;
}[];
mails: Mail[];
defaultLayout?: number[];
defaultCollapsed?: boolean;
navCollapsedSize: number;
}
export function Mail({
accounts,
mails,
defaultLayout = [20, 32, 48],
defaultCollapsed = false,
navCollapsedSize,
}: MailProps) {
const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed);
const [mail] = useMail();
return (
<TooltipProvider delayDuration={0}>
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes: number[]) => {
document.cookie = `react-resizable-panels:layout:mail=${JSON.stringify(
sizes
)}`;
}}
className="h-full max-h-[800px] items-stretch"
>
<ResizablePanel
defaultSize={defaultLayout[0]}
collapsedSize={navCollapsedSize}
collapsible={true}
minSize={15}
maxSize={20}
onCollapse={() => {
setIsCollapsed(true);
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(
true
)}`;
}}
onResize={() => {
setIsCollapsed(false);
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(
false
)}`;
}}
className={cn(
isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out"
)}
>
<div
className={cn(
"flex h-[52px] items-center justify-center",
isCollapsed ? "h-[52px]" : "px-2"
)}
>
<AccountSwitcher isCollapsed={isCollapsed} accounts={accounts} />
</div>
<Separator />
<Nav
isCollapsed={isCollapsed}
links={[
{
title: "Inbox",
label: "128",
icon: Inbox,
variant: "default",
},
{
title: "Drafts",
label: "9",
icon: File,
variant: "ghost",
},
{
title: "Sent",
label: "",
icon: Send,
variant: "ghost",
},
{
title: "Junk",
label: "23",
icon: ArchiveX,
variant: "ghost",
},
{
title: "Trash",
label: "",
icon: Trash2,
variant: "ghost",
},
{
title: "Archive",
label: "",
icon: Archive,
variant: "ghost",
},
]}
/>
<Separator />
<Nav
isCollapsed={isCollapsed}
links={[
{
title: "Social",
label: "972",
icon: Users2,
variant: "ghost",
},
{
title: "Updates",
label: "342",
icon: AlertCircle,
variant: "ghost",
},
{
title: "Forums",
label: "128",
icon: MessagesSquare,
variant: "ghost",
},
{
title: "Shopping",
label: "8",
icon: ShoppingCart,
variant: "ghost",
},
{
title: "Promotions",
label: "21",
icon: Archive,
variant: "ghost",
},
]}
/>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={defaultLayout[1]} minSize={30}>
<Tabs defaultValue="all">
<div className="flex items-center px-4 py-1.5">
<h1 className="text-xl font-bold">Inbox</h1>
<TabsList className="ml-auto">
<TabsTrigger value="all">All mail</TabsTrigger>
<TabsTrigger value="unread">Unread</TabsTrigger>
</TabsList>
</div>
<Separator />
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<form>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input placeholder="Search" className="pl-8" />
</div>
</form>
</div>
<TabsContent value="all" className="m-0">
<MailList items={mails} />
</TabsContent>
<TabsContent value="unread" className="m-0">
<MailList items={mails.filter((item) => !item.read)} />
</TabsContent>
</Tabs>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={defaultLayout[2]} minSize={30}>
<MailDisplay
mail={mails.find((item) => item.id === mail.selected) || null}
/>
</ResizablePanel>
</ResizablePanelGroup>
</TooltipProvider>
);
}
@@ -0,0 +1,79 @@
import Link from "next/link";
import { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
interface NavProps {
isCollapsed: boolean;
links: {
title: string;
label?: string;
icon: LucideIcon;
variant: "default" | "ghost";
}[];
}
export function Nav({ links, isCollapsed }: NavProps) {
return (
<div
data-collapsed={isCollapsed}
className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2"
>
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
{links.map((link, index) =>
isCollapsed ? (
<Tooltip key={index} delayDuration={0}>
<TooltipTrigger asChild>
<Link
href="#"
className={cn(
buttonVariants({ variant: link.variant, size: "icon" }),
"h-9 w-9",
link.variant === "default" &&
"dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white"
)}
>
<link.icon className="h-4 w-4" />
<span className="sr-only">{link.title}</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-4">
{link.title}
{link.label && (
<span className="ml-auto text-muted-foreground">{link.label}</span>
)}
</TooltipContent>
</Tooltip>
) : (
<Link
key={index}
href="#"
className={cn(
buttonVariants({ variant: link.variant, size: "sm" }),
link.variant === "default" &&
"group dark:bg-muted dark:text-foreground dark:hover:bg-muted dark:hover:text-foreground",
"justify-start"
)}
>
<link.icon className="mr-2 h-4 w-4" />
{link.title}
{link.label && (
<span
className={cn(
"ml-auto",
link.variant === "default" &&
"text-background dark:text-muted-foreground"
)}
>
{link.label}
</span>
)}
</Link>
)
)}
</nav>
</div>
);
}
+300
View File
@@ -0,0 +1,300 @@
export const mails = [
{
id: "6c84fb90-12c4-11e1-840d-7b25c5ee775a",
name: "William Smith",
email: "williamsmith@example.com",
subject: "Meeting Tomorrow",
text: "Hi, let's have a meeting tomorrow to discuss the project. I've been reviewing the project details and have some ideas I'd like to share. It's crucial that we align on our next steps to ensure the project's success.\n\nPlease come prepared with any questions or insights you may have. Looking forward to our meeting!\n\nBest regards, William",
date: "2023-10-22T09:00:00",
read: true,
labels: ["meeting", "work", "important"],
},
{
id: "110e8400-e29b-11d4-a716-446655440000",
name: "Alice Smith",
email: "alicesmith@example.com",
subject: "Re: Project Update",
text: "Thank you for the project update. It looks great! I've gone through the report, and the progress is impressive. The team has done a fantastic job, and I appreciate the hard work everyone has put in.\n\nI have a few minor suggestions that I'll include in the attached document.\n\nLet's discuss these during our next meeting. Keep up the excellent work!\n\nBest regards, Alice",
date: "2023-10-22T10:30:00",
read: true,
labels: ["work", "important"],
},
{
id: "3e7c3f6d-bdf5-46ae-8d90-171300f27ae2",
name: "Bob Johnson",
email: "bobjohnson@example.com",
subject: "Weekend Plans",
text: "Any plans for the weekend? I was thinking of going hiking in the nearby mountains. It's been a while since we had some outdoor fun.\n\nIf you're interested, let me know, and we can plan the details. It'll be a great way to unwind and enjoy nature.\n\nLooking forward to your response!\n\nBest, Bob",
date: "2023-04-10T11:45:00",
read: true,
labels: ["personal"],
},
{
id: "61c35085-72d7-42b4-8d62-738f700d4b92",
name: "Emily Davis",
email: "emilydavis@example.com",
subject: "Re: Question about Budget",
text: "I have a question about the budget for the upcoming project. It seems like there's a discrepancy in the allocation of resources.\n\nI've reviewed the budget report and identified a few areas where we might be able to optimize our spending without compromising the project's quality.\n\nI've attached a detailed analysis for your reference. Let's discuss this further in our next meeting.\n\nThanks, Emily",
date: "2023-03-25T13:15:00",
read: false,
labels: ["work", "budget"],
},
{
id: "8f7b5db9-d935-4e42-8e05-1f1d0a3dfb97",
name: "Michael Wilson",
email: "michaelwilson@example.com",
subject: "Important Announcement",
text: "I have an important announcement to make during our team meeting. It pertains to a strategic shift in our approach to the upcoming product launch. We've received valuable feedback from our beta testers, and I believe it's time to make some adjustments to better meet our customers' needs.\n\nThis change is crucial to our success, and I look forward to discussing it with the team. Please be prepared to share your insights during the meeting.\n\nRegards, Michael",
date: "2023-03-10T15:00:00",
read: false,
labels: ["meeting", "work", "important"],
},
{
id: "1f0f2c02-e299-40de-9b1d-86ef9e42126b",
name: "Sarah Brown",
email: "sarahbrown@example.com",
subject: "Re: Feedback on Proposal",
text: "Thank you for your feedback on the proposal. It looks great! I'm pleased to hear that you found it promising. The team worked diligently to address all the key points you raised, and I believe we now have a strong foundation for the project.\n\nI've attached the revised proposal for your review.\n\nPlease let me know if you have any further comments or suggestions. Looking forward to your response.\n\nBest regards, Sarah",
date: "2023-02-15T16:30:00",
read: true,
labels: ["work"],
},
{
id: "17c0a96d-4415-42b1-8b4f-764efab57f66",
name: "David Lee",
email: "davidlee@example.com",
subject: "New Project Idea",
text: "I have an exciting new project idea to discuss with you. It involves expanding our services to target a niche market that has shown considerable growth in recent months.\n\nI've prepared a detailed proposal outlining the potential benefits and the strategy for execution.\n\nThis project has the potential to significantly impact our business positively. Let's set up a meeting to dive into the details and determine if it aligns with our current goals.\n\nBest regards, David",
date: "2023-01-28T17:45:00",
read: false,
labels: ["meeting", "work", "important"],
},
{
id: "2f0130cb-39fc-44c4-bb3c-0a4337edaaab",
name: "Olivia Wilson",
email: "oliviawilson@example.com",
subject: "Vacation Plans",
text: "Let's plan our vacation for next month. What do you think? I've been thinking of visiting a tropical paradise, and I've put together some destination options.\n\nI believe it's time for us to unwind and recharge. Please take a look at the options and let me know your preferences.\n\nWe can start making arrangements to ensure a smooth and enjoyable trip.\n\nExcited to hear your thoughts! Olivia",
date: "2022-12-20T18:30:00",
read: true,
labels: ["personal"],
},
{
id: "de305d54-75b4-431b-adb2-eb6b9e546014",
name: "James Martin",
email: "jamesmartin@example.com",
subject: "Re: Conference Registration",
text: "I've completed the registration for the conference next month. The event promises to be a great networking opportunity, and I'm looking forward to attending the various sessions and connecting with industry experts.\n\nI've also attached the conference schedule for your reference.\n\nIf there are any specific topics or sessions you'd like me to explore, please let me know. It's an exciting event, and I'll make the most of it.\n\nBest regards, James",
date: "2022-11-30T19:15:00",
read: true,
labels: ["work", "conference"],
},
{
id: "7dd90c63-00f6-40f3-bd87-5060a24e8ee7",
name: "Sophia White",
email: "sophiawhite@example.com",
subject: "Team Dinner",
text: "Let's have a team dinner next week to celebrate our success. We've achieved some significant milestones, and it's time to acknowledge our hard work and dedication.\n\nI've made reservations at a lovely restaurant, and I'm sure it'll be an enjoyable evening.\n\nPlease confirm your availability and any dietary preferences. Looking forward to a fun and memorable dinner with the team!\n\nBest, Sophia",
date: "2022-11-05T20:30:00",
read: false,
labels: ["meeting", "work"],
},
{
id: "99a88f78-3eb4-4d87-87b7-7b15a49a0a05",
name: "Daniel Johnson",
email: "danieljohnson@example.com",
subject: "Feedback Request",
text: "I'd like your feedback on the latest project deliverables. We've made significant progress, and I value your input to ensure we're on the right track.\n\nI've attached the deliverables for your review, and I'm particularly interested in any areas where you think we can further enhance the quality or efficiency.\n\nYour feedback is invaluable, and I appreciate your time and expertise. Let's work together to make this project a success.\n\nRegards, Daniel",
date: "2022-10-22T09:30:00",
read: false,
labels: ["work"],
},
{
id: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
name: "Ava Taylor",
email: "avataylor@example.com",
subject: "Re: Meeting Agenda",
text: "Here's the agenda for our meeting next week. I've included all the topics we need to cover, as well as time allocations for each.\n\nIf you have any additional items to discuss or any specific points to address, please let me know, and we can integrate them into the agenda.\n\nIt's essential that our meeting is productive and addresses all relevant matters.\n\nLooking forward to our meeting! Ava",
date: "2022-10-10T10:45:00",
read: true,
labels: ["meeting", "work"],
},
{
id: "c1a0ecb4-2540-49c5-86f8-21e5ce79e4e6",
name: "William Anderson",
email: "williamanderson@example.com",
subject: "Product Launch Update",
text: "The product launch is on track. I'll provide an update during our call. We've made substantial progress in the development and marketing of our new product.\n\nI'm excited to share the latest updates with you during our upcoming call. It's crucial that we coordinate our efforts to ensure a successful launch. Please come prepared with any questions or insights you may have.\n\nLet's make this product launch a resounding success!\n\nBest regards, William",
date: "2022-09-20T12:00:00",
read: false,
labels: ["meeting", "work", "important"],
},
{
id: "ba54eefd-4097-4949-99f2-2a9ae4d1a836",
name: "Mia Harris",
email: "miaharris@example.com",
subject: "Re: Travel Itinerary",
text: "I've received the travel itinerary. It looks great! Thank you for your prompt assistance in arranging the details. I've reviewed the schedule and the accommodations, and everything seems to be in order. I'm looking forward to the trip, and I'm confident it'll be a smooth and enjoyable experience.\n\nIf there are any specific activities or attractions you recommend at our destination, please feel free to share your suggestions.\n\nExcited for the trip! Mia",
date: "2022-09-10T13:15:00",
read: true,
labels: ["personal", "travel"],
},
{
id: "df09b6ed-28bd-4e0c-85a9-9320ec5179aa",
name: "Ethan Clark",
email: "ethanclark@example.com",
subject: "Team Building Event",
text: "Let's plan a team-building event for our department. Team cohesion and morale are vital to our success, and I believe a well-organized team-building event can be incredibly beneficial. I've done some research and have a few ideas for fun and engaging activities.\n\nPlease let me know your thoughts and availability. We want this event to be both enjoyable and productive.\n\nTogether, we'll strengthen our team and boost our performance.\n\nRegards, Ethan",
date: "2022-08-25T15:30:00",
read: false,
labels: ["meeting", "work"],
},
{
id: "d67c1842-7f8b-4b4b-9be1-1b3b1ab4611d",
name: "Chloe Hall",
email: "chloehall@example.com",
subject: "Re: Budget Approval",
text: "The budget has been approved. We can proceed with the project. I'm delighted to inform you that our budget proposal has received the green light from the finance department. This is a significant milestone, and it means we can move forward with the project as planned.\n\nI've attached the finalized budget for your reference. Let's ensure that we stay on track and deliver the project on time and within budget.\n\nIt's an exciting time for us! Chloe",
date: "2022-08-10T16:45:00",
read: true,
labels: ["work", "budget"],
},
{
id: "6c9a7f94-8329-4d70-95d3-51f68c186ae1",
name: "Samuel Turner",
email: "samuelturner@example.com",
subject: "Weekend Hike",
text: "Who's up for a weekend hike in the mountains? I've been craving some outdoor adventure, and a hike in the mountains sounds like the perfect escape. If you're up for the challenge, we can explore some scenic trails and enjoy the beauty of nature.\n\nI've done some research and have a few routes in mind.\n\nLet me know if you're interested, and we can plan the details.\n\nIt's sure to be a memorable experience! Samuel",
date: "2022-07-28T17:30:00",
read: false,
labels: ["personal"],
},
];
export type Mail = (typeof mails)[number];
export const accounts = [
{
label: "Alicia Koch",
email: "alicia@example.com",
icon: (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Gmail</title>
<path
d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z"
fill="currentColor"
/>
</svg>
),
},
{
label: "Alicia Koch",
email: "alicia2@example.com",
icon: (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Vercel</title>
<path d="M24 22.525H0l12-21.05 12 21.05z" fill="currentColor" />
</svg>
),
},
{
label: "Alicia Koch",
email: "alicia3@example.com",
icon: (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>iCloud</title>
<path
d="M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z"
fill="currentColor"
/>
</svg>
),
},
];
export type Account = (typeof accounts)[number];
export const contacts = [
{
name: "Emma Johnson",
email: "emma.johnson@example.com",
},
{
name: "Liam Wilson",
email: "liam.wilson@example.com",
},
{
name: "Olivia Davis",
email: "olivia.davis@example.com",
},
{
name: "Noah Martinez",
email: "noah.martinez@example.com",
},
{
name: "Ava Taylor",
email: "ava.taylor@example.com",
},
{
name: "Lucas Brown",
email: "lucas.brown@example.com",
},
{
name: "Sophia Smith",
email: "sophia.smith@example.com",
},
{
name: "Ethan Wilson",
email: "ethan.wilson@example.com",
},
{
name: "Isabella Jackson",
email: "isabella.jackson@example.com",
},
{
name: "Mia Clark",
email: "mia.clark@example.com",
},
{
name: "Mason Lee",
email: "mason.lee@example.com",
},
{
name: "Layla Harris",
email: "layla.harris@example.com",
},
{
name: "William Anderson",
email: "william.anderson@example.com",
},
{
name: "Ella White",
email: "ella.white@example.com",
},
{
name: "James Thomas",
email: "james.thomas@example.com",
},
{
name: "Harper Lewis",
email: "harper.lewis@example.com",
},
{
name: "Benjamin Moore",
email: "benjamin.moore@example.com",
},
{
name: "Aria Hall",
email: "aria.hall@example.com",
},
{
name: "Henry Turner",
email: "henry.turner@example.com",
},
{
name: "Scarlett Adams",
email: "scarlett.adams@example.com",
},
];
export type Contact = (typeof contacts)[number];
+6
View File
@@ -0,0 +1,6 @@
import { Mail } from "@/components/examples/mail/components/mail";
import { accounts, mails } from "@/components/examples/mail/data";
export default function MailPage() {
return <Mail accounts={accounts} mails={mails} navCollapsedSize={4} />;
}
+19
View File
@@ -0,0 +1,19 @@
import { create } from "zustand";
import { Mail, mails } from "@/components/examples/mail/data";
interface Config {
selected: Mail["id"] | null;
}
const useMailStore = create<
Config & { setState: (newState: Partial<Config>) => void }
>((set) => ({
selected: mails[0].id,
setState: (newState) => set((state) => ({ ...state, ...newState })),
}));
export function useMail(): [Config, (newState: Partial<Config>) => void] {
const selected = useMailStore((state) => state.selected);
const setState = useMailStore((state) => state.setState);
return [{ selected }, setState];
}
@@ -0,0 +1,95 @@
import * as React from "react";
import { PlusCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { Album } from "../data/albums";
import { playlists } from "../data/playlists";
interface AlbumArtworkProps extends React.HTMLAttributes<HTMLDivElement> {
album: Album;
aspectRatio?: "portrait" | "square";
width?: number;
height?: number;
}
export function AlbumArtwork({
album,
aspectRatio = "portrait",
width,
height,
className,
...props
}: AlbumArtworkProps) {
return (
<div className={cn("flex flex-col gap-3", className)} {...props}>
<ContextMenu>
<ContextMenuTrigger>
<div className="overflow-hidden rounded-md">
<img
src={album.cover}
alt={album.name}
className={cn(
"h-auto w-auto object-cover transition-all hover:scale-105",
aspectRatio === "portrait" ? "aspect-[3/4]" : "aspect-square"
)}
style={{
width: width ? `${width}px` : "auto",
height: height ? `${height}px` : "auto",
}}
/>
</div>
</ContextMenuTrigger>
<ContextMenuContent className="w-40">
<ContextMenuItem>Add to Library</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>Add to Playlist</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
<ContextMenuItem>
<PlusCircle className="mr-2 h-4 w-4" />
New Playlist
</ContextMenuItem>
<ContextMenuSeparator />
{playlists.map((playlist) => (
<ContextMenuItem key={playlist}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="mr-2 h-4 w-4"
viewBox="0 0 24 24"
>
<path d="M21 15V6M18.5 18a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM12 12H3M16 6H3M12 18H3" />
</svg>
{playlist}
</ContextMenuItem>
))}
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem>Play Next</ContextMenuItem>
<ContextMenuItem>Play Later</ContextMenuItem>
<ContextMenuItem>Create Station</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>Like</ContextMenuItem>
<ContextMenuItem>Share</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<div className="flex flex-col gap-1 text-sm">
<h3 className="font-medium leading-none">{album.name}</h3>
<p className="text-xs text-muted-foreground">{album.artist}</p>
</div>
</div>
);
}
@@ -0,0 +1,200 @@
import {
Menubar,
MenubarCheckboxItem,
MenubarContent,
MenubarItem,
MenubarLabel,
MenubarMenu,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar";
export function Menu() {
return (
<Menubar className="rounded-none border-b border-none px-2 lg:px-4">
<MenubarMenu>
<MenubarTrigger className="font-bold">Music</MenubarTrigger>
<MenubarContent>
<MenubarItem>About Music</MenubarItem>
<MenubarSeparator />
<MenubarItem>
Preferences... <MenubarShortcut>,</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>
Hide Music... <MenubarShortcut>H</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Hide Others... <MenubarShortcut>H</MenubarShortcut>
</MenubarItem>
<MenubarShortcut />
<MenubarItem>
Quit Music <MenubarShortcut>Q</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger className="relative">File</MenubarTrigger>
<MenubarContent>
<MenubarSub>
<MenubarSubTrigger>New</MenubarSubTrigger>
<MenubarSubContent className="w-[230px]">
<MenubarItem>
Playlist <MenubarShortcut>N</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>
Playlist from Selection <MenubarShortcut>N</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Smart Playlist... <MenubarShortcut>N</MenubarShortcut>
</MenubarItem>
<MenubarItem>Playlist Folder</MenubarItem>
<MenubarItem disabled>Genius Playlist</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarItem>
Open Stream URL... <MenubarShortcut>U</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Close Window <MenubarShortcut>W</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Library</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>Update Cloud Library</MenubarItem>
<MenubarItem>Update Genius</MenubarItem>
<MenubarSeparator />
<MenubarItem>Organize Library...</MenubarItem>
<MenubarItem>Export Library...</MenubarItem>
<MenubarSeparator />
<MenubarItem>Import Playlist...</MenubarItem>
<MenubarItem disabled>Export Playlist...</MenubarItem>
<MenubarItem>Show Duplicate Items</MenubarItem>
<MenubarSeparator />
<MenubarItem>Get Album Artwork</MenubarItem>
<MenubarItem disabled>Get Track Names</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarItem>
Import... <MenubarShortcut>O</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>Burn Playlist to Disc...</MenubarItem>
<MenubarSeparator />
<MenubarItem>
Show in Finder <MenubarShortcut>R</MenubarShortcut>{" "}
</MenubarItem>
<MenubarItem>Convert</MenubarItem>
<MenubarSeparator />
<MenubarItem>Page Setup...</MenubarItem>
<MenubarItem disabled>
Print... <MenubarShortcut>P</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Edit</MenubarTrigger>
<MenubarContent>
<MenubarItem disabled>
Undo <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>
Redo <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem disabled>
Cut <MenubarShortcut>X</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>
Copy <MenubarShortcut>C</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>
Paste <MenubarShortcut>V</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>
Select All <MenubarShortcut>A</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>
Deselect All <MenubarShortcut>A</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>
Smart Dictation...{" "}
<MenubarShortcut>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4"
viewBox="0 0 24 24"
>
<path d="m12 8-9.04 9.06a2.82 2.82 0 1 0 3.98 3.98L16 12" />
<circle cx="17" cy="7" r="5" />
</svg>
</MenubarShortcut>
</MenubarItem>
<MenubarItem>
Emoji & Symbols{" "}
<MenubarShortcut>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent>
<MenubarCheckboxItem>Show Playing Next</MenubarCheckboxItem>
<MenubarCheckboxItem checked>Show Lyrics</MenubarCheckboxItem>
<MenubarSeparator />
<MenubarItem inset disabled>
Show Status Bar
</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>Hide Sidebar</MenubarItem>
<MenubarItem disabled inset>
Enter Full Screen
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger className="hidden md:block">Account</MenubarTrigger>
<MenubarContent forceMount>
<MenubarLabel inset>Switch Account</MenubarLabel>
<MenubarSeparator />
<MenubarRadioGroup value="benoit">
<MenubarRadioItem value="andy">Andy</MenubarRadioItem>
<MenubarRadioItem value="benoit">Benoit</MenubarRadioItem>
<MenubarRadioItem value="Luis">Luis</MenubarRadioItem>
</MenubarRadioGroup>
<MenubarSeparator />
<MenubarItem inset>Manage Family...</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>Add Account...</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
);
}
@@ -0,0 +1,64 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function PodcastEmptyPlaceholder() {
return (
<div className="flex h-[450px] shrink-0 items-center justify-center rounded-md border border-dashed">
<div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-10 w-10 text-muted-foreground"
viewBox="0 0 24 24"
>
<circle cx="12" cy="11" r="1" />
<path d="M11 17a1 1 0 0 1 2 0c0 .5-.34 3-.5 4.5a.5.5 0 0 1-1 0c-.16-1.5-.5-4-.5-4.5ZM8 14a5 5 0 1 1 8 0" />
<path d="M17 18.5a9 9 0 1 0-10 0" />
</svg>
<h3 className="mt-4 text-lg font-semibold">No episodes added</h3>
<p className="mb-4 mt-2 text-sm text-muted-foreground">
You have not added any podcasts. Add one below.
</p>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" className="relative">
Add Podcast
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Podcast</DialogTitle>
<DialogDescription>
Copy and paste the podcast feed URL to import.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="url">Podcast URL</Label>
<Input id="url" placeholder="https://example.com/feed.xml" />
</div>
</div>
<DialogFooter>
<Button>Import Podcast</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
);
}
@@ -0,0 +1,204 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Playlist } from "../data/playlists";
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
playlists: Playlist[];
}
export function Sidebar({ className, playlists }: SidebarProps) {
return (
<div className={cn("pb-12", className)}>
<div className="space-y-4 py-4">
<div className="px-3 py-2">
<h2 className="mb-2 px-4 text-lg font-semibold tracking-tight">
Discover
</h2>
<div className="space-y-1">
<Button variant="secondary" className="w-full justify-start">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<circle cx="12" cy="12" r="10" />
<polygon points="10 8 16 12 10 16 10 8" />
</svg>
Listen Now
</Button>
<Button variant="ghost" className="w-full justify-start">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<rect width="7" height="7" x="3" y="3" rx="1" />
<rect width="7" height="7" x="14" y="3" rx="1" />
<rect width="7" height="7" x="14" y="14" rx="1" />
<rect width="7" height="7" x="3" y="14" rx="1" />
</svg>
Browse
</Button>
<Button variant="ghost" className="w-full justify-start">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9" />
<path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5" />
<circle cx="12" cy="12" r="2" />
<path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5" />
<path d="M19.1 4.9C23 8.8 23 15.1 19.1 19" />
</svg>
Radio
</Button>
</div>
</div>
<div className="px-3 py-2">
<h2 className="mb-2 px-4 text-lg font-semibold tracking-tight">Library</h2>
<div className="space-y-1">
<Button variant="ghost" className="w-full justify-start">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M21 15V6" />
<path d="M18.5 18a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" />
<path d="M12 12H3" />
<path d="M16 6H3" />
<path d="M12 18H3" />
</svg>
Playlists
</Button>
<Button variant="ghost" className="w-full justify-start">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<circle cx="8" cy="18" r="4" />
<path d="M12 18V2l7 4" />
</svg>
Songs
</Button>
<Button variant="ghost" className="w-full justify-start">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
Made for You
</Button>
<Button variant="ghost" className="w-full justify-start">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="m12 8-9.04 9.06a2.82 2.82 0 1 0 3.98 3.98L16 12" />
<circle cx="17" cy="7" r="5" />
</svg>
Artists
</Button>
<Button variant="ghost" className="w-full justify-start">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="m16 6 4 14" />
<path d="M12 6v14" />
<path d="M8 8v12" />
<path d="M4 4v16" />
</svg>
Albums
</Button>
</div>
</div>
<div className="py-2">
<h2 className="relative px-7 text-lg font-semibold tracking-tight">
Playlists
</h2>
<ScrollArea className="h-[300px] px-1">
<div className="space-y-1 p-2">
{playlists?.map((playlist, i) => (
<Button
key={`${playlist}-${i}`}
variant="ghost"
className="w-full justify-start font-normal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M21 15V6" />
<path d="M18.5 18a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" />
<path d="M12 12H3" />
<path d="M16 6H3" />
<path d="M12 18H3" />
</svg>
{playlist}
</Button>
))}
</div>
</ScrollArea>
</div>
</div>
</div>
);
}
+71
View File
@@ -0,0 +1,71 @@
export interface Album {
name: string;
artist: string;
cover: string;
}
export const listenNowAlbums: Album[] = [
{
name: "React Rendezvous",
artist: "Ethan Byte",
cover:
"https://images.unsplash.com/photo-1611348586804-61bf6c080437?w=300&dpr=2&q=80",
},
{
name: "Async Awakenings",
artist: "Nina Netcode",
cover:
"https://images.unsplash.com/photo-1468817814611-b7edf94b5d60?w=300&dpr=2&q=80",
},
{
name: "The Art of Reusability",
artist: "Lena Logic",
cover:
"https://images.unsplash.com/photo-1528143358888-6d3c7f67bd5d?w=300&dpr=2&q=80",
},
{
name: "Stateful Symphony",
artist: "Beth Binary",
cover:
"https://images.unsplash.com/photo-1490300472339-79e4adc6be4a?w=300&dpr=2&q=80",
},
];
export const madeForYouAlbums: Album[] = [
{
name: "Thinking Components",
artist: "Lena Logic",
cover:
"https://images.unsplash.com/photo-1615247001958-f4bc92fa6a4a?w=300&dpr=2&q=80",
},
{
name: "Functional Fury",
artist: "Beth Binary",
cover:
"https://images.unsplash.com/photo-1513745405825-efaf9a49315f?w=300&dpr=2&q=80",
},
{
name: "React Rendezvous",
artist: "Ethan Byte",
cover:
"https://images.unsplash.com/photo-1614113489855-66422ad300a4?w=300&dpr=2&q=80",
},
{
name: "Stateful Symphony",
artist: "Beth Binary",
cover:
"https://images.unsplash.com/photo-1446185250204-f94591f7d702?w=300&dpr=2&q=80",
},
{
name: "Async Awakenings",
artist: "Nina Netcode",
cover:
"https://images.unsplash.com/photo-1468817814611-b7edf94b5d60?w=300&dpr=2&q=80",
},
{
name: "The Art of Reusability",
artist: "Lena Logic",
cover:
"https://images.unsplash.com/photo-1490300472339-79e4adc6be4a?w=300&dpr=2&q=80",
},
];
@@ -0,0 +1,16 @@
export type Playlist = (typeof playlists)[number];
export const playlists = [
"Recently Added",
"Recently Played",
"Top Songs",
"Top Albums",
"Top Artists",
"Logic Discography",
"Bedtime Beats",
"Feeling Happy",
"I miss Y2K Pop",
"Runtober",
"Mellow Days",
"Eminem Essentials",
];
+129
View File
@@ -0,0 +1,129 @@
import { PlusCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { AlbumArtwork } from "./components/album-artwork";
import { Menu } from "./components/menu";
import { PodcastEmptyPlaceholder } from "./components/podcast-empty-placeholder";
import { Sidebar } from "./components/sidebar";
import { listenNowAlbums, madeForYouAlbums } from "./data/albums";
import { playlists } from "./data/playlists";
export default function MusicPage() {
return (
<>
<div className="hidden md:block">
<Menu />
<div className="border-t">
<div className="bg-background">
<div className="grid lg:grid-cols-5">
<Sidebar playlists={playlists} className="hidden lg:block" />
<div className="col-span-3 lg:col-span-4 lg:border-l">
<div className="h-full px-4 py-6 lg:px-8">
<Tabs defaultValue="music" className="h-full space-y-6">
<div className="space-between flex items-center">
<TabsList>
<TabsTrigger value="music" className="relative">
Music
</TabsTrigger>
<TabsTrigger value="podcasts">Podcasts</TabsTrigger>
<TabsTrigger value="live" disabled>
Live
</TabsTrigger>
</TabsList>
<div className="ml-auto mr-4">
<Button>
<PlusCircle />
Add music
</Button>
</div>
</div>
<TabsContent
value="music"
className="border-none p-0 outline-none"
>
<div className="flex items-center justify-between">
<div className="space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">
Listen Now
</h2>
<p className="text-sm text-muted-foreground">
Top picks for you. Updated daily.
</p>
</div>
</div>
<Separator className="my-4" />
<div className="relative">
<ScrollArea>
<div className="flex space-x-4 pb-4">
{listenNowAlbums.map((album) => (
<AlbumArtwork
key={album.name}
album={album}
className="w-[250px]"
aspectRatio="portrait"
width={250}
height={330}
/>
))}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
<div className="mt-6 space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">
Made for You
</h2>
<p className="text-sm text-muted-foreground">
Your personal playlists. Updated daily.
</p>
</div>
<Separator className="my-4" />
<div className="relative">
<ScrollArea>
<div className="flex space-x-4 pb-4">
{madeForYouAlbums.map((album) => (
<AlbumArtwork
key={album.name}
album={album}
className="w-[150px]"
aspectRatio="square"
width={150}
height={150}
/>
))}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
</TabsContent>
<TabsContent
value="podcasts"
className="h-full flex-col border-none p-0 data-[state=active]:flex"
>
<div className="flex items-center justify-between">
<div className="space-y-1">
<h2 className="text-2xl font-semibold tracking-tight">
New Episodes
</h2>
<p className="text-sm text-muted-foreground">
Your favorite podcasts. Updated daily.
</p>
</div>
</div>
<Separator className="my-4" />
<PodcastEmptyPlaceholder />
</TabsContent>
</Tabs>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
}
@@ -0,0 +1,115 @@
import { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { labels, priorities, statuses } from "../data/data";
import { Task } from "../data/schema";
import { DataTableColumnHeader } from "./data-table-column-header";
import { DataTableRowActions } from "./data-table-row-actions";
export const columns: ColumnDef<Task>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() ? "indeterminate" : false)
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
className="translate-y-[2px]"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="translate-y-[2px]"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "id",
header: ({ column }) => <DataTableColumnHeader column={column} title="Task" />,
cell: ({ row }) => <div className="w-[80px]">{row.getValue("id")}</div>,
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "title",
header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />,
cell: ({ row }) => {
const label = labels.find((label) => label.value === row.original.label);
return (
<div className="flex space-x-2">
{label && <Badge variant="outline">{label.label}</Badge>}
<span className="max-w-[500px] truncate font-medium">
{row.getValue("title")}
</span>
</div>
);
},
},
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => {
const status = statuses.find(
(status) => status.value === row.getValue("status")
);
if (!status) {
return null;
}
return (
<div className="flex w-[100px] items-center">
{status.icon && (
<status.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{status.label}</span>
</div>
);
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
},
{
accessorKey: "priority",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Priority" />
),
cell: ({ row }) => {
const priority = priorities.find(
(priority) => priority.value === row.getValue("priority")
);
if (!priority) {
return null;
}
return (
<div className="flex items-center">
{priority.icon && (
<priority.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{priority.label}</span>
</div>
);
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
},
{
id: "actions",
cell: ({ row }) => <DataTableRowActions row={row} />,
},
];
@@ -0,0 +1,66 @@
import { Column } from "@tanstack/react-table";
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>;
title: string;
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>;
}
return (
<div className={cn("flex items-center space-x-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDown />
) : column.getIsSorted() === "asc" ? (
<ArrowUp />
) : (
<ChevronsUpDown />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUp className="h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDown className="h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeOff className="h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
@@ -0,0 +1,140 @@
import * as React from "react";
import { Column } from "@tanstack/react-table";
import { Check, PlusCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
interface DataTableFacetedFilterProps<TData, TValue> {
column?: Column<TData, TValue>;
title?: string;
options: {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
}[];
}
export function DataTableFacetedFilter<TData, TValue>({
column,
title,
options,
}: DataTableFacetedFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues();
const selectedValues = new Set(column?.getFilterValue() as string[]);
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border-dashed">
<PlusCircle />
{title}
{selectedValues?.size > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge variant="secondary" className="rounded-sm px-1 font-normal">
{selectedValues.size} selected
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = selectedValues.has(option.value);
return (
<CommandItem
key={option.value}
onSelect={() => {
if (isSelected) {
selectedValues.delete(option.value);
} else {
selectedValues.add(option.value);
}
const filterValues = Array.from(selectedValues);
column?.setFilterValue(
filterValues.length ? filterValues : undefined
);
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}
>
<Check />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{option.label}</span>
{facets?.get(option.value) && (
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
{facets.get(option.value)}
</span>
)}
</CommandItem>
);
})}
</CommandGroup>
{selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,96 @@
import { Table } from "@tanstack/react-table";
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface DataTablePaginationProps<TData> {
table: Table<TData>;
}
export function DataTablePagination<TData>({
table,
}: DataTablePaginationProps<TData>) {
return (
<div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeft />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRight />
</Button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,67 @@
import { Row } from "@tanstack/react-table";
import { MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { labels } from "../data/data";
import { taskSchema } from "../data/schema";
interface DataTableRowActionsProps<TData> {
row: Row<TData>;
}
export function DataTableRowActions<TData>({
row,
}: DataTableRowActionsProps<TData>) {
const task = taskSchema.parse(row.original);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<MoreHorizontal />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[160px]">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={task.label}>
{labels.map((label) => (
<DropdownMenuRadioItem key={label.value} value={label.value}>
{label.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem>
Delete
<DropdownMenuShortcut></DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -0,0 +1,57 @@
import { Table } from "@tanstack/react-table";
import { X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { DataTableViewOptions } from "@/components/examples/tasks/components/data-table-view-options";
import { priorities, statuses } from "../data/data";
import { DataTableFacetedFilter } from "./data-table-faceted-filter";
interface DataTableToolbarProps<TData> {
table: Table<TData>;
}
export function DataTableToolbar<TData>({ table }: DataTableToolbarProps<TData>) {
const isFiltered = table.getState().columnFilters.length > 0;
return (
<div className="flex items-center justify-between">
<div className="flex flex-1 items-center space-x-2">
<Input
placeholder="Filter tasks..."
value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("title")?.setFilterValue(event.target.value)
}
className="h-8 w-[150px] lg:w-[250px]"
/>
{table.getColumn("status") && (
<DataTableFacetedFilter
column={table.getColumn("status")}
title="Status"
options={statuses}
/>
)}
{table.getColumn("priority") && (
<DataTableFacetedFilter
column={table.getColumn("priority")}
title="Priority"
options={priorities}
/>
)}
{isFiltered && (
<Button
variant="ghost"
onClick={() => table.resetColumnFilters()}
className="h-8 px-2 lg:px-3"
>
Reset
<X />
</Button>
)}
</div>
<DataTableViewOptions table={table} />
</div>
);
}
@@ -0,0 +1,53 @@
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { Table } from "@tanstack/react-table";
import { Settings2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
interface DataTableViewOptionsProps<TData> {
table: Table<TData>;
}
export function DataTableViewOptions<TData>({
table,
}: DataTableViewOptionsProps<TData>) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="ml-auto hidden h-8 lg:flex">
<Settings2 />
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -0,0 +1,119 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { DataTablePagination } from "./data-table-pagination";
import { DataTableToolbar } from "./data-table-toolbar";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
{}
);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const table = useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
});
return (
<div className="space-y-4">
<DataTableToolbar table={table} />
<div className="rounded-md border">
<Table>
<TableHeader>
{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>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<DataTablePagination table={table} />
</div>
);
}
@@ -0,0 +1,58 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function UserNav() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-9 w-9">
<AvatarImage src="/avatars/03.png" alt="@shadcn" />
<AvatarFallback>SC</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">shadcn</p>
<p className="text-xs leading-none text-muted-foreground">
m@example.com
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>New Team</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
Log out
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
+71
View File
@@ -0,0 +1,71 @@
import {
ArrowDown,
ArrowRight,
ArrowUp,
CheckCircle,
Circle,
CircleOff,
HelpCircle,
Timer,
} from "lucide-react"
export const labels = [
{
value: "bug",
label: "Bug",
},
{
value: "feature",
label: "Feature",
},
{
value: "documentation",
label: "Documentation",
},
]
export const statuses = [
{
value: "backlog",
label: "Backlog",
icon: HelpCircle,
},
{
value: "todo",
label: "Todo",
icon: Circle,
},
{
value: "in progress",
label: "In Progress",
icon: Timer,
},
{
value: "done",
label: "Done",
icon: CheckCircle,
},
{
value: "canceled",
label: "Canceled",
icon: CircleOff,
},
]
export const priorities = [
{
label: "Low",
value: "low",
icon: ArrowDown,
},
{
label: "Medium",
value: "medium",
icon: ArrowRight,
},
{
label: "High",
value: "high",
icon: ArrowUp,
},
]
+13
View File
@@ -0,0 +1,13 @@
import { z } from "zod"
// We're keeping a simple non-relational schema here.
// IRL, you will have a schema for your data models.
export const taskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string(),
})
export type Task = z.infer<typeof taskSchema>
+702
View File
@@ -0,0 +1,702 @@
[
{
"id": "TASK-8782",
"title": "You can't compress the program without quantifying the open-source SSD pixel!",
"status": "in progress",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-7878",
"title": "Try to calculate the EXE feed, maybe it will index the multi-byte pixel!",
"status": "backlog",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-7839",
"title": "We need to bypass the neural TCP card!",
"status": "todo",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-5562",
"title": "The SAS interface is down, bypass the open-source pixel so we can back up the PNG bandwidth!",
"status": "backlog",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-8686",
"title": "I'll parse the wireless SSL protocol, that should driver the API panel!",
"status": "canceled",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-1280",
"title": "Use the digital TLS panel, then you can transmit the haptic system!",
"status": "done",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-7262",
"title": "The UTF8 application is down, parse the neural bandwidth so we can back up the PNG firewall!",
"status": "done",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-1138",
"title": "Generating the driver won't do anything, we need to quantify the 1080p SMTP bandwidth!",
"status": "in progress",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-7184",
"title": "We need to program the back-end THX pixel!",
"status": "todo",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-5160",
"title": "Calculating the bus won't do anything, we need to navigate the back-end JSON protocol!",
"status": "in progress",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-5618",
"title": "Generating the driver won't do anything, we need to index the online SSL application!",
"status": "done",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-6699",
"title": "I'll transmit the wireless JBOD capacitor, that should hard drive the SSD feed!",
"status": "backlog",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-2858",
"title": "We need to override the online UDP bus!",
"status": "backlog",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-9864",
"title": "I'll reboot the 1080p FTP panel, that should matrix the HEX hard drive!",
"status": "done",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-8404",
"title": "We need to generate the virtual HEX alarm!",
"status": "in progress",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-5365",
"title": "Backing up the pixel won't do anything, we need to transmit the primary IB array!",
"status": "in progress",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-1780",
"title": "The CSS feed is down, index the bluetooth transmitter so we can compress the CLI protocol!",
"status": "todo",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-6938",
"title": "Use the redundant SCSI application, then you can hack the optical alarm!",
"status": "todo",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-9885",
"title": "We need to compress the auxiliary VGA driver!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-3216",
"title": "Transmitting the transmitter won't do anything, we need to compress the virtual HDD sensor!",
"status": "backlog",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-9285",
"title": "The IP monitor is down, copy the haptic alarm so we can generate the HTTP transmitter!",
"status": "todo",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-1024",
"title": "Overriding the microchip won't do anything, we need to transmit the digital OCR transmitter!",
"status": "in progress",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-7068",
"title": "You can't generate the capacitor without indexing the wireless HEX pixel!",
"status": "canceled",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-6502",
"title": "Navigating the microchip won't do anything, we need to bypass the back-end SQL bus!",
"status": "todo",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-5326",
"title": "We need to hack the redundant UTF8 transmitter!",
"status": "todo",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-6274",
"title": "Use the virtual PCI circuit, then you can parse the bluetooth alarm!",
"status": "canceled",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-1571",
"title": "I'll input the neural DRAM circuit, that should protocol the SMTP interface!",
"status": "in progress",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-9518",
"title": "Compressing the interface won't do anything, we need to compress the online SDD matrix!",
"status": "canceled",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-5581",
"title": "I'll synthesize the digital COM pixel, that should transmitter the UTF8 protocol!",
"status": "backlog",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-2197",
"title": "Parsing the feed won't do anything, we need to copy the bluetooth DRAM bus!",
"status": "todo",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-8484",
"title": "We need to parse the solid state UDP firewall!",
"status": "in progress",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-9892",
"title": "If we back up the application, we can get to the UDP application through the multi-byte THX capacitor!",
"status": "done",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-9616",
"title": "We need to synthesize the cross-platform ASCII pixel!",
"status": "in progress",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-9744",
"title": "Use the back-end IP card, then you can input the solid state hard drive!",
"status": "done",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-1376",
"title": "Generating the alarm won't do anything, we need to generate the mobile IP capacitor!",
"status": "backlog",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-7382",
"title": "If we back up the firewall, we can get to the RAM alarm through the primary UTF8 pixel!",
"status": "todo",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-2290",
"title": "I'll compress the virtual JSON panel, that should application the UTF8 bus!",
"status": "canceled",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-1533",
"title": "You can't input the firewall without overriding the wireless TCP firewall!",
"status": "done",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-4920",
"title": "Bypassing the hard drive won't do anything, we need to input the bluetooth JSON program!",
"status": "in progress",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-5168",
"title": "If we synthesize the bus, we can get to the IP panel through the virtual TLS array!",
"status": "in progress",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-7103",
"title": "We need to parse the multi-byte EXE bandwidth!",
"status": "canceled",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-4314",
"title": "If we compress the program, we can get to the XML alarm through the multi-byte COM matrix!",
"status": "in progress",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-3415",
"title": "Use the cross-platform XML application, then you can quantify the solid state feed!",
"status": "todo",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-8339",
"title": "Try to calculate the DNS interface, maybe it will input the bluetooth capacitor!",
"status": "in progress",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-6995",
"title": "Try to hack the XSS bandwidth, maybe it will override the bluetooth matrix!",
"status": "todo",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-8053",
"title": "If we connect the program, we can get to the UTF8 matrix through the digital UDP protocol!",
"status": "todo",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-4336",
"title": "If we synthesize the microchip, we can get to the SAS sensor through the optical UDP program!",
"status": "todo",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-8790",
"title": "I'll back up the optical COM alarm, that should alarm the RSS capacitor!",
"status": "done",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-8980",
"title": "Try to navigate the SQL transmitter, maybe it will back up the virtual firewall!",
"status": "canceled",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-7342",
"title": "Use the neural CLI card, then you can parse the online port!",
"status": "backlog",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-5608",
"title": "I'll hack the haptic SSL program, that should bus the UDP transmitter!",
"status": "canceled",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-1606",
"title": "I'll generate the bluetooth PNG firewall, that should pixel the SSL driver!",
"status": "done",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-7872",
"title": "Transmitting the circuit won't do anything, we need to reboot the 1080p RSS monitor!",
"status": "canceled",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-4167",
"title": "Use the cross-platform SMS circuit, then you can synthesize the optical feed!",
"status": "canceled",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-9581",
"title": "You can't index the port without hacking the cross-platform XSS monitor!",
"status": "backlog",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-8806",
"title": "We need to bypass the back-end SSL panel!",
"status": "done",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-6542",
"title": "Try to quantify the RSS firewall, maybe it will quantify the open-source system!",
"status": "done",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-6806",
"title": "The VGA protocol is down, reboot the back-end matrix so we can parse the CSS panel!",
"status": "canceled",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-9549",
"title": "You can't bypass the bus without connecting the neural JBOD bus!",
"status": "todo",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-1075",
"title": "Backing up the driver won't do anything, we need to parse the redundant RAM pixel!",
"status": "done",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-1427",
"title": "Use the auxiliary PCI circuit, then you can calculate the cross-platform interface!",
"status": "done",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-1907",
"title": "Hacking the circuit won't do anything, we need to back up the online DRAM system!",
"status": "todo",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-4309",
"title": "If we generate the system, we can get to the TCP sensor through the optical GB pixel!",
"status": "backlog",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-3973",
"title": "I'll parse the back-end ADP array, that should bandwidth the RSS bandwidth!",
"status": "todo",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-7962",
"title": "Use the wireless RAM program, then you can hack the cross-platform feed!",
"status": "canceled",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-3360",
"title": "You can't quantify the program without synthesizing the neural OCR interface!",
"status": "done",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-9887",
"title": "Use the auxiliary ASCII sensor, then you can connect the solid state port!",
"status": "backlog",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-3649",
"title": "I'll input the virtual USB system, that should circuit the DNS monitor!",
"status": "in progress",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-3586",
"title": "If we quantify the circuit, we can get to the CLI feed through the mobile SMS hard drive!",
"status": "in progress",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-5150",
"title": "I'll hack the wireless XSS port, that should transmitter the IP interface!",
"status": "canceled",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-3652",
"title": "The SQL interface is down, override the optical bus so we can program the ASCII interface!",
"status": "backlog",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-6884",
"title": "Use the digital PCI circuit, then you can synthesize the multi-byte microchip!",
"status": "canceled",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-1591",
"title": "We need to connect the mobile XSS driver!",
"status": "in progress",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-3802",
"title": "Try to override the ASCII protocol, maybe it will parse the virtual matrix!",
"status": "in progress",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-7253",
"title": "Programming the capacitor won't do anything, we need to bypass the neural IB hard drive!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-9739",
"title": "We need to hack the multi-byte HDD bus!",
"status": "done",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-4424",
"title": "Try to hack the HEX alarm, maybe it will connect the optical pixel!",
"status": "in progress",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-3922",
"title": "You can't back up the capacitor without generating the wireless PCI program!",
"status": "backlog",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-4921",
"title": "I'll index the open-source IP feed, that should system the GB application!",
"status": "canceled",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-5814",
"title": "We need to calculate the 1080p AGP feed!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-2645",
"title": "Synthesizing the system won't do anything, we need to navigate the multi-byte HDD firewall!",
"status": "todo",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-4535",
"title": "Try to copy the JSON circuit, maybe it will connect the wireless feed!",
"status": "in progress",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-4463",
"title": "We need to copy the solid state AGP monitor!",
"status": "done",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-9745",
"title": "If we connect the protocol, we can get to the GB system through the bluetooth PCI microchip!",
"status": "canceled",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-2080",
"title": "If we input the bus, we can get to the RAM matrix through the auxiliary RAM card!",
"status": "todo",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-3838",
"title": "I'll bypass the online TCP application, that should panel the AGP system!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-1340",
"title": "We need to navigate the virtual PNG circuit!",
"status": "todo",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-6665",
"title": "If we parse the monitor, we can get to the SSD hard drive through the cross-platform AGP alarm!",
"status": "canceled",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-7585",
"title": "If we calculate the hard drive, we can get to the SSL program through the multi-byte CSS microchip!",
"status": "backlog",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-6319",
"title": "We need to copy the multi-byte SCSI program!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-4369",
"title": "Try to input the SCSI bus, maybe it will generate the 1080p pixel!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-9035",
"title": "We need to override the solid state PNG array!",
"status": "canceled",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-3970",
"title": "You can't index the transmitter without quantifying the haptic ASCII card!",
"status": "todo",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-4473",
"title": "You can't bypass the protocol without overriding the neural RSS program!",
"status": "todo",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-4136",
"title": "You can't hack the hard drive without hacking the primary JSON program!",
"status": "canceled",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-3939",
"title": "Use the back-end SQL firewall, then you can connect the neural hard drive!",
"status": "done",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-2007",
"title": "I'll input the back-end USB protocol, that should bandwidth the PCI system!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-7516",
"title": "Use the primary SQL program, then you can generate the auxiliary transmitter!",
"status": "done",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-6906",
"title": "Try to back up the DRAM system, maybe it will reboot the online transmitter!",
"status": "done",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-5207",
"title": "The SMS interface is down, copy the bluetooth bus so we can quantify the VGA card!",
"status": "in progress",
"label": "bug",
"priority": "low"
}
]
+35
View File
@@ -0,0 +1,35 @@
import { z } from "zod";
import { columns } from "./components/columns";
import { DataTable } from "./components/data-table";
import { UserNav } from "./components/user-nav";
import { taskSchema } from "./data/schema";
import tasks from "./data/tasks.json";
// Simulate a database read for tasks.
function getTasks() {
return z.array(taskSchema).parse(tasks);
}
export default function TaskPage() {
const tasks = getTasks();
return (
<>
<div className="hidden h-full flex-1 flex-col space-y-8 p-8 md:flex">
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">Welcome back!</h2>
<p className="text-muted-foreground">
Here&apos;s a list of your tasks for this month!
</p>
</div>
<div className="flex items-center space-x-2">
<UserNav />
</div>
</div>
<DataTable data={tasks} columns={columns} />
</div>
</>
);
}
+82
View File
@@ -0,0 +1,82 @@
import { Button } from "@/components/ui/button";
import { motion } from "motion/react";
import { ArrowRight } from "lucide-react";
import Link from "next/link";
export function CTA() {
return (
<section className="w-full py-20 md:py-32 bg-gradient-to-br from-primary to-primary/80 text-primary-foreground relative overflow-hidden">
<div className="absolute inset-0 -z-10 bg-[linear-gradient(to_right,#ffffff10_1px,transparent_1px),linear-gradient(to_bottom,#ffffff10_1px,transparent_1px)] bg-[size:4rem_4rem]"></div>
<div className="absolute -top-24 -left-24 w-64 h-64 bg-white/10 rounded-full blur-3xl animate-pulse"></div>
<div
className="absolute -bottom-24 -right-24 w-64 h-64 bg-white/10 rounded-full blur-3xl animate-pulse"
style={{ animationDelay: "1.5s" }}
></div>
<div className="container px-4 md:px-6 relative">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center justify-center space-y-6 text-center"
>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight"
>
Ready to Make Your Components Stand Out?
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="mx-auto max-w-[700px] text-primary-foreground/80 md:text-xl"
>
Start customizing your shadcn/ui components today and create a unique
look for your application.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="flex flex-col sm:flex-row gap-4 mt-4"
>
<Link href="/editor/theme">
<Button
size="lg"
variant="secondary"
className="rounded-full h-12 px-8 text-base cursor-pointer shadow-md hover:shadow-lg transition-all duration-300 hover:translate-y-[-2px]"
>
Try It Now
<ArrowRight className="ml-2 size-4" />
</Button>
</Link>
<Link href="https://github.com/jnsahaj/tweakcn">
<Button
size="lg"
variant="outline"
className="rounded-full bg-transparent h-12 px-8 text-base transition-all duration-300 hover:translate-y-[-2px]"
>
View on GitHub
</Button>
</Link>
</motion.div>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.3 }}
className="text-sm text-primary-foreground/80 mt-4"
>
No login required. Free to use. Open source.
</motion.p>
</motion.div>
</div>
</section>
);
}
+101
View File
@@ -0,0 +1,101 @@
import { Badge } from "@/components/ui/badge";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { motion } from "motion/react";
const faqs = [
{
question: "What is tweakcn?",
answer:
"tweakcn is a visual theme editor for shadcn/ui components with Tailwind CSS support. It comes with a set of pre-built themes that you can use to customize your project.",
},
{
question: "Is tweakcn free to use?",
answer:
"Yes, tweakcn is completely free to use. We may introduce premium features in the future, but the core functionality will always remain free.",
},
{
question: "How do I customise a shadcn/ui theme?",
answer:
"You can customise a shadcn/ui theme by selecting the a preset theme you want to use from the dropdown menu and then adjusting the colors to you liking. Once you are happy with the theme, you can export the code by either copying it or running the command to apply the theme to your project automatically.",
},
{
question: "Does tweakcn support Tailwind CSS v4?",
answer:
"Yes, tweakcn supports Tailwind CSS v4 (and v3). You can choose the version of Tailwind CSS you want to use from the dropdown menu in the Code section. It also supports multiple color formats to best suit your project.",
},
{
question: "Do I need to know Tailwind CSS to use tweakcn?",
answer:
"No, you don't need to know Tailwind CSS to use tweakcn. Our visual editor makes it easy to customize components without writing any code. However, having some knowledge of Tailwind CSS will help you understand the generated code better.",
},
{
question: "Can I use tweakcn with my existing shadcn/ui project?",
answer:
"Yes, tweakcn is designed to work with existing shadcn/ui projects. You can export the generated code by either copying it or running the command to apply the theme to your project automatically.",
},
{
question: "Is tweakcn open source?",
answer:
"Yes :) tweakcn is open source. You can find the source code on GitHub and contribute to the project if you'd like to help improve it. You can also join the discord server to get help from the community.",
},
];
export function FAQ() {
return (
<section id="faq" className="w-full py-20 md:py-32">
<div className="container px-4 md:px-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center justify-center space-y-4 text-center mb-12"
>
<Badge
className="rounded-full px-4 py-1.5 text-sm font-medium shadow-sm"
variant="secondary"
>
<span className="mr-1 text-primary"></span> FAQ
</Badge>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/80">
Frequently Asked Questions
</h2>
<p className="max-w-[800px] text-muted-foreground md:text-lg">
Find answers to common questions about tweakcn.
</p>
</motion.div>
<div className="mx-auto max-w-3xl">
<Accordion type="single" collapsible className="w-full">
{faqs.map((faq, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: i * 0.05 }}
>
<AccordionItem
value={`item-${i}`}
className="border-b border-border/40 py-2 group"
>
<AccordionTrigger className="text-left font-medium hover:no-underline group-hover:text-primary transition-colors">
{faq.question}
</AccordionTrigger>
<AccordionContent className="text-muted-foreground">
{faq.answer}
</AccordionContent>
</AccordionItem>
</motion.div>
))}
</Accordion>
</div>
</div>
</section>
);
}
+122
View File
@@ -0,0 +1,122 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { motion } from "motion/react";
import {
Code,
FileCode,
Layers,
Palette,
Paintbrush,
PaintBucket,
} from "lucide-react";
const features = [
{
title: "Visual Theme Customizer",
description:
"Customize your shadcn/ui components with a real-time preview to see changes instantly.",
icon: <Palette className="size-5" />,
},
{
title: "Color Control",
description:
"Customize background, text, and border colors with an intuitive color picker interface.",
icon: <Paintbrush className="size-5" />,
},
{
title: "Typography Settings",
description:
"Fine-tune font size, weight, and text transform to create the perfect look.",
icon: <FileCode className="size-5" />,
},
{
title: "Tailwind v4 & v3 Support",
description:
"Seamlessly switch between Tailwind v4 and v3, with support for multiple color formats including OKLCH & HSL.",
icon: <Code className="size-5" />,
},
{
title: "Tailwind properties",
description:
"Fine-tune every aspect of your components with precise control over radius, spacing, shadows, and other Tailwind properties.",
icon: <Layers className="size-5" />,
},
{
title: "Beautiful Theme Presets",
description:
"Choose from stunning pre-designed themes and customize both light and dark mode colors effortlessly.",
icon: <PaintBucket className="size-5" />,
},
];
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
export function Features() {
return (
<section id="features" className="w-full py-20 md:py-32 relative">
<div className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_center,rgba(var(--primary-rgb),0.03),transparent_70%)]"></div>
<div className="container px-4 md:px-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center justify-center space-y-4 text-center mb-12"
>
<Badge
className="rounded-full px-4 py-1.5 text-sm font-medium shadow-sm"
variant="secondary"
>
<span className="mr-1 text-primary"></span> Features
</Badge>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/80">
Powerful Customization Tools
</h2>
<p className="max-w-[800px] text-muted-foreground md:text-lg">
tweakcn provides all the tools you need to customize your shadcn/ui
components and make them unique.
</p>
</motion.div>
<motion.div
variants={container}
initial="hidden"
whileInView="show"
viewport={{ once: true }}
className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"
>
{features.map((feature, i) => (
<motion.div
key={i}
variants={item}
whileHover={{ y: -5, transition: { duration: 0.2 } }}
>
<Card className="h-full overflow-hidden border-border/40 bg-gradient-to-b from-card to-card/50 backdrop-blur transition-all hover:shadow-lg hover:border-primary/20 group">
<CardContent className="p-6 flex flex-col h-full">
<div className="size-12 rounded-full bg-primary/10 flex items-center justify-center text-primary mb-4 group-hover:bg-primary/20 transition-colors duration-300">
{feature.icon}
</div>
<h3 className="text-xl font-bold mb-2">{feature.title}</h3>
<p className="text-muted-foreground">{feature.description}</p>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}
+113
View File
@@ -0,0 +1,113 @@
import Link from "next/link";
import Logo from "@/assets/logo.svg";
import GitHubIcon from "@/assets/github.svg";
import TwitterIcon from "@/assets/twitter.svg";
import DiscordIcon from "@/assets/discord.svg";
export function Footer() {
return (
<footer className="w-full border-t bg-background/95 backdrop-blur-sm">
<div className="container max-w-8xl mx-auto flex flex-col gap-8 px-4 md:px-0 py-10 lg:py-16">
<div className="grid gap-8 sm:grid-cols-2 md:grid-cols-4">
<div className="space-y-4 col-span-2 max-w-md">
<Link href="/" className="flex items-center gap-2 font-bold">
<Logo className="size-6" />
<span>tweakcn</span>
</Link>
<p className="text-sm text-muted-foreground">
A powerful visual theme editor for shadcn/ui components with Tailwind
CSS support. Make your components stand out.
</p>
<div className="flex gap-4">
<a
href="https://github.com/jnsahaj/tweakcn"
className="text-muted-foreground hover:text-foreground transition-colors"
>
<GitHubIcon className="size-5" />
<span className="sr-only">GitHub</span>
</a>
<a
href="https://discord.gg/Phs4u2NM3n"
className="text-muted-foreground hover:text-foreground transition-colors"
>
<DiscordIcon className="size-5" />
<span className="sr-only">Discord</span>
</a>
<a
href="https://x.com/iamsahaj_xyz"
className="text-muted-foreground hover:text-foreground transition-colors"
>
<TwitterIcon className="size-5" />
<span className="sr-only">Twitter</span>
</a>
</div>
</div>
<div className="space-y-4">
<h4 className="text-sm font-bold">Product</h4>
<ul className="space-y-2 text-sm">
<li>
<a
href="#features"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Features
</a>
</li>
<li>
<a
href="#examples"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Examples
</a>
</li>
<li>
<a
href="#roadmap"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Roadmap
</a>
</li>
</ul>
</div>
<div className="space-y-4">
<h4 className="text-sm font-bold">Resources</h4>
<ul className="space-y-2 text-sm">
<li>
<a
href="https://github.com/jnsahaj/tweakcn"
className="text-muted-foreground hover:text-foreground transition-colors"
>
GitHub
</a>
</li>
<li>
<a
href="https://discord.gg/Phs4u2NM3n"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Discord
</a>
</li>
<li>
<a
href="https://x.com/messages/compose?recipient_id=1426676644152889345"
className="text-muted-foreground hover:text-foreground transition-colors"
>
Contact
</a>
</li>
</ul>
</div>
</div>
<div className="flex flex-col gap-4 sm:flex-row justify-between items-center border-t border-border/40 pt-8">
<p className="text-xs text-muted-foreground">
&copy; {new Date().getFullYear()} tweakcn. All rights reserved.
</p>
</div>
</div>
</footer>
);
}
+196
View File
@@ -0,0 +1,196 @@
"use client";
import { Button } from "@/components/ui/button";
import { useTheme } from "@/components/theme-provider";
import { motion } from "motion/react";
import Link from "next/link";
import { Menu, Moon, Sun, X, ChevronRight } from "lucide-react";
import Logo from "@/assets/logo.svg";
import GitHubIcon from "@/assets/github.svg";
import { useGithubStars } from "@/hooks/use-github-stars";
import { cn } from "@/lib/utils";
interface HeaderProps {
isScrolled: boolean;
mobileMenuOpen: boolean;
setMobileMenuOpen: (open: boolean) => void;
}
export function Header({
isScrolled,
mobileMenuOpen,
setMobileMenuOpen,
}: HeaderProps) {
const { theme, toggleTheme } = useTheme();
const { stargazersCount } = useGithubStars("jnsahaj", "tweakcn");
const handleThemeToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
const { clientX: x, clientY: y } = event;
toggleTheme({ x, y });
};
const handleScrollToSection = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
const targetId = e.currentTarget.getAttribute("href")?.slice(1);
if (!targetId) return;
const element = document.getElementById(targetId);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
};
return (
<header
className={cn(
"sticky top-0 z-50 w-full backdrop-blur-lg",
isScrolled
? "bg-background/90 shadow-xs border-b border-border/20"
: "bg-transparent"
)}
>
<div className="container flex h-16 px-4 min-w-full items-center justify-between">
<Link href="/">
<div className="flex items-center gap-2 font-bold">
<Logo className="size-6" />
<span>tweakcn</span>
</div>
</Link>
<nav className="hidden md:flex gap-8">
{["Examples", "Features", "How It Works", "Roadmap", "FAQ"].map(
(item, i) => (
<motion.a
key={item}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 + i * 0.05 }}
href={`#${item.toLowerCase().replace(/\s+/g, "-")}`}
onClick={handleScrollToSection}
className="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground relative group"
>
{item}
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</motion.a>
)
)}
</nav>
<div className="hidden md:flex gap-4 items-center cursor-pointer">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.45 }}
>
<Button variant="ghost" asChild>
<a
href="https://github.com/jnsahaj/tweakcn"
target="_blank"
rel="noopener noreferrer"
className="font-semibold"
>
<GitHubIcon className="size-5" />
{stargazersCount > 0 && stargazersCount.toLocaleString()}
</a>
</Button>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.4 }}
>
<Button
variant="secondary"
size="icon"
onClick={handleThemeToggle}
className="rounded-full transition-transform hover:scale-105"
>
{theme === "light" ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</Button>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.5 }}
>
<Link href="/editor/theme">
<Button className="rounded-full cursor-pointer transition-transform hover:scale-105 font-medium">
Try It Now
<ChevronRight className="ml-1 size-4" />
</Button>
</Link>
</motion.div>
</div>
<div className="flex items-center gap-4 md:hidden">
<Button
variant="ghost"
size="icon"
onClick={handleThemeToggle}
className="rounded-full cursor-pointer"
>
{theme === "dark" ? (
<Sun className="size-[18px]" />
) : (
<Moon className="size-[18px]" />
)}
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <X className="size-5" /> : <Menu className="size-5" />}
<span className="sr-only">Toggle menu</span>
</Button>
</div>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="md:hidden absolute top-16 inset-x-0 bg-background/95 backdrop-blur-lg border-b"
>
<div className="container py-4 flex flex-col gap-4 px-4">
{["Examples", "Features", "How It Works", "Roadmap", "FAQ"].map(
(item, i) => (
<motion.a
key={item}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: i * 0.05 }}
href={`#${item.toLowerCase().replace(/\s+/g, "-")}`}
onClick={(e) => {
handleScrollToSection(e);
setMobileMenuOpen(false);
}}
className="py-2 text-sm font-medium relative overflow-hidden group"
>
<span className="relative z-10">{item}</span>
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</motion.a>
)
)}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.3 }}
className="pt-2 mt-2 border-t border-border/30"
>
<Link href="/editor/theme" onClick={() => setMobileMenuOpen(false)}>
<Button className="w-full rounded-full">
Try It Now
<ChevronRight className="ml-2 size-4" />
</Button>
</Link>
</motion.div>
</div>
</motion.div>
)}
</header>
);
}
+149
View File
@@ -0,0 +1,149 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { ArrowRight, Check, Copy, Circle, Eye, Palette } from "lucide-react";
import Link from "next/link";
export function Hero() {
return (
<section className="w-full py-20 md:py-32 lg:py-40 relative">
<div className="container px-4 md:px-6 z-10 relative">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left Column - Text Content */}
<div className="text-left max-w-2xl mx-auto lg:mx-0">
<div>
<Badge
className="mb-4 rounded-full px-4 py-1.5 text-sm font-medium shadow-sm transition-none"
variant="secondary"
>
<span className="mr-1 text-primary"></span> Visual Theme Editor
</Badge>
</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight mb-6 bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground/90 to-foreground/70">
Design Your Perfect <span className="text-primary">shadcn/ui</span>{" "}
Theme
</h1>
<p className="text-muted-foreground mb-8 text-lg md:text-xl leading-relaxed">
Customize colors, typography, and layouts with a real-time preview. No
signup required.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Link href="/editor/theme">
<Button
size="lg"
className="rounded-full h-12 px-8 cursor-pointer shadow-md hover:shadow-lg transition-transform duration-300 hover:translate-y-[-2px] text-base"
>
Start Customizing
<ArrowRight className="ml-2 size-4" />
</Button>
</Link>
<a href="#examples">
<Button
size="lg"
variant="outline"
className="rounded-full h-12 px-8 cursor-pointer border-primary/20 hover:border-primary/50 transition-transform duration-300 hover:translate-y-[-2px] text-base"
>
View Examples
</Button>
</a>
</div>
<div className="flex flex-wrap items-center gap-6 mt-8">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Check className="size-5 text-primary" />
<span>Real-time Preview</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Check className="size-5 text-primary" />
<span>Export to Tailwind</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Check className="size-5 text-primary" />
<span>Beautiful Presets</span>
</div>
</div>
</div>
{/* Right Column - Preview Card */}
<div className="relative hidden lg:block">
<Card className="relative overflow-hidden border-border/40 bg-gradient-to-b from-background to-background/95 backdrop-blur shadow-xl rounded-2xl">
<CardContent className="p-0">
{/* Header */}
<div className="flex items-center justify-between border-b p-4">
<div className="flex items-center gap-3">
<div className="flex gap-2">
<div className="size-3 rounded-full bg-red-500"></div>
<div className="size-3 rounded-full bg-yellow-500"></div>
<div className="size-3 rounded-full bg-green-500"></div>
</div>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Color Palette */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-sm font-medium">Color Palette</div>
<Palette className="size-4 text-muted-foreground" />
</div>
<div className="space-y-2 text-center">
<div className="h-24 w-full bg-gradient-to-r from-primary via-secondary via-accent via-muted to-background rounded-xl"></div>
<div className="grid grid-cols-5 gap-2 text-xs text-muted-foreground">
<div>Primary</div>
<div>Secondary</div>
<div>Accent</div>
<div>Muted</div>
<div>Background</div>
</div>
</div>
</div>
{/* Preview */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="text-sm font-medium">Preview</div>
<Eye className="size-4 text-muted-foreground" />
</div>
<div className="space-y-3">
<div className="flex gap-2">
<Button
className="w-full shadow-sm transition-none"
variant="secondary"
>
<Copy className="size-4 mr-2" />
Copy Code
</Button>
<Button
className="w-full shadow-sm transition-none"
variant="outline"
>
<Circle className="size-4 mr-2" />
oklch, hsl, rgb, hex
</Button>
</div>
<Card className="w-full">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="size-8 rounded-full bg-primary/10 flex items-center justify-center">
<span className="text-xs text-primary">UI</span>
</div>
<div className="flex-1">
<div className="h-2 w-24 bg-foreground/90 rounded mb-2"></div>
<div className="h-2 w-16 bg-muted-foreground/60 rounded"></div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_90%_30%,var(--muted),transparent_35%)] blur-3xl"></div>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_10%_70%,var(--muted),transparent_10%)] blur-3xl"></div>
</section>
);
}
+82
View File
@@ -0,0 +1,82 @@
import { Badge } from "@/components/ui/badge";
import { motion } from "motion/react";
const steps = [
{
step: "01",
title: "Select Theme Preset",
description: "Choose the theme you want to customize from our growing library.",
},
{
step: "02",
title: "Customize Visually",
description:
"Use our intuitive interface to adjust colors, dimensions, typography, and other properties.",
},
{
step: "03",
title: "Copy Code",
description: "Copy the generated Tailwind CSS code directly into your project.",
},
];
export function HowItWorks() {
return (
<section
id="how-it-works"
className="w-full py-20 md:py-32 bg-muted/30 relative overflow-hidden"
>
<div className="absolute inset-0 -z-10 h-full w-full bg-background"></div>
<div className="absolute inset-0 -z-10 bg-[linear-gradient(to_right,rgba(var(--muted-rgb),0.05)_1px,transparent_1px),linear-gradient(to_bottom,rgba(var(--muted-rgb),0.05)_1px,transparent_1px)] bg-[size:3rem_3rem]"></div>
<div className="container px-4 md:px-6 relative">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center justify-center space-y-4 text-center mb-16"
>
<Badge
className="rounded-full px-4 py-1.5 text-sm font-medium shadow-sm"
variant="secondary"
>
<span className="mr-1 text-primary"></span> How It Works
</Badge>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/80">
Simple Process, Beautiful Results
</h2>
<p className="max-w-[800px] text-muted-foreground md:text-lg">
Customize your shadcn/ui components in just a few simple steps.
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-8 md:gap-12 relative">
{steps.map((step, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.2 }}
className="relative z-10 flex flex-col items-center text-center space-y-4"
>
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-primary to-primary/70 text-primary-foreground text-xl font-bold shadow-lg relative">
{step.step}
<div
className="absolute inset-0 rounded-full bg-primary/20 animate-ping opacity-75"
style={{
animationDuration: "3s",
animationDelay: `${i * 0.5}s`,
}}
></div>
</div>
<h3 className="text-xl font-bold">{step.title}</h3>
<p className="text-muted-foreground">{step.description}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { motion } from "motion/react";
import { Folder, Grid, Layers, Palette, Repeat, Users } from "lucide-react";
const roadmapItems = [
{
title: "Global Theme Editor",
description:
"Create and manage complete themes with presets for your entire application.",
status: "In Progress",
icon: <Palette className="size-5" />,
},
{
title: "Theme Import/Export",
description: "Save and share your custom themes with others.",
status: "In Progress",
icon: <Repeat className="size-5" />,
},
{
title: "More Controls",
description:
"Support for more controls, including Spacing, Shadows, Tracking and more",
status: "Coming Soon",
icon: <Layers className="size-5" />,
},
{
title: "Community Themes",
description: "Allow users to submit themes, vote on the best designs",
status: "Planned",
icon: <Users className="size-5" />,
},
{
title: "Multi-Project Management",
description:
"Save and manage multiple theme projects, making it easy to switch between designs.",
status: "Planned",
icon: <Folder className="size-5" />,
},
{
title: "More Presets",
description:
"Expand the preset library with a wider variety of stunning themes for quick customization.",
status: "Planned",
icon: <Grid className="size-5" />,
},
];
export function Roadmap() {
return (
<section
id="roadmap"
className="w-full py-20 md:py-32 bg-muted/30 relative overflow-hidden"
>
<div className="absolute inset-0 -z-10 h-full w-full bg-background/20"></div>
<div className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_top,rgba(var(--secondary-rgb),0.05),transparent_50%)]"></div>
<div className="container px-4 md:px-6 relative">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center justify-center space-y-4 text-center mb-16"
>
<Badge
className="rounded-full px-4 py-1.5 text-sm font-medium shadow-sm"
variant="secondary"
>
<span className="mr-1 text-primary"></span> Roadmap
</Badge>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/80">
What's Coming Next
</h2>
<p className="max-w-[800px] text-muted-foreground md:text-lg">
We're constantly working to improve tweakcn and add new features. Here's
what's on our roadmap.
</p>
</motion.div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{roadmapItems.map((item, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.1 }}
whileHover={{ y: -5, transition: { duration: 0.2 } }}
>
<Card className="h-full overflow-hidden border-border/40 bg-gradient-to-b from-card to-card/50 backdrop-blur transition-all hover:shadow-lg hover:border-primary/20">
<CardContent className="p-6 flex flex-col h-full">
<div className="size-12 rounded-full bg-primary/10 flex items-center justify-center text-primary mb-4">
{item.icon}
</div>
<div className="flex justify-between items-center mb-2">
<h3 className="text-xl font-bold">{item.title}</h3>
<Badge
variant={
item.status === "In Progress"
? "default"
: item.status === "Coming Soon"
? "secondary"
: "outline"
}
className="shadow-sm"
>
{item.status}
</Badge>
</div>
<p className="text-muted-foreground">{item.description}</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
);
}
+135
View File
@@ -0,0 +1,135 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useEditorStore } from "@/store/editor-store";
import { motion } from "motion/react";
import { getPresetThemeStyles, presets } from "@/utils/theme-presets";
import { cn } from "@/lib/utils";
import { colorFormatter } from "@/utils/color-converter";
import { DemoContainer } from "@/components/examples/demo-cards";
import { DemoGithub } from "@/components/examples/cards/github-card";
import { DemoStats } from "@/components/examples/cards/stats";
import { lazy, Suspense } from "react";
import { Loading } from "../loading";
import { useIsMobile } from "@/hooks/use-mobile";
const DemoMail = lazy(() => import("@/components/examples/mail"));
const ColorBox = ({ color }: { color: string }) => {
return (
<div className="w-3 h-3 rounded-sm border" style={{ backgroundColor: color }} />
);
};
export function ThemePresetSelector() {
const { themeState, applyThemePreset } = useEditorStore();
const mode = themeState.currentMode;
const presetNames = Object.keys(presets);
const isMobile = useIsMobile();
return (
<section id="examples" className="w-full py-20 md:py-32">
<div className="container px-4 md:px-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center justify-center space-y-4 text-center mb-12"
>
<div className="flex items-center justify-center gap-4 mb-4">
<Badge
className="rounded-full px-4 py-1.5 text-sm font-medium shadow-sm"
variant="secondary"
>
<span className="mr-1 text-primary"></span> Theme Presets
</Badge>
</div>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/80">
Preview and Select a Theme
</h2>
<p className="max-w-[800px] text-muted-foreground md:text-lg">
Click on a theme below to preview how it transforms the page.
</p>
</motion.div>
{/* Theme Selector Buttons */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="grid grid-cols-2 md:grid-cols-6 gap-4 mb-8"
>
{presetNames?.slice(4, 10).map((presetName, index) => {
const themeStyles = getPresetThemeStyles(presetName)[mode];
const bgColor = colorFormatter(themeStyles.primary, "hsl", "4");
const isSelected = presetName === themeState.preset;
return (
<motion.div
key={presetName}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: 0.1 + index * 0.05 }}
>
<Button
className={cn(
"flex w-full items-center relative transition-all hover:shadow-md bg-primary/10 hover:bg-primary/20 hover:translate-y-[-2px]",
isSelected ? "ring-2 ring-primary/30 shadow-md" : ""
)}
variant="ghost"
style={{
backgroundColor: bgColor
.replace("hsl", "hsla")
.replace(/\s+/g, ", ")
.replace(")", ", 0.15)"),
color: themeStyles.foreground,
borderRadius: themeStyles.radius,
}}
onClick={() => applyThemePreset(presetName)}
>
<div className="flex gap-0.5 mr-1">
<ColorBox color={themeStyles.primary} />
<ColorBox color={themeStyles.accent} />
</div>
<span className="capitalize">{presetName.replace(/-/g, " ")}</span>
</Button>
</motion.div>
);
})}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="@container relative overflow-hidden border rounded-lg max-h-[60vh] md:max-h-[70vh] shadow-lg bg-gradient-to-b from-card/50 to-card/30 backdrop-blur-sm"
>
<div
className="absolute bottom-0 left-0 right-0 h-16 pointer-events-none z-10"
style={{
background:
"linear-gradient(to bottom, rgba(255,255,255,0), var(--background))",
}}
/>
<Suspense fallback={<Loading />}>
{!isMobile ? (
<DemoMail />
) : (
<div className="p-4 flex flex-col gap-4">
<DemoContainer>
<DemoStats />
</DemoContainer>
<DemoContainer>
<DemoGithub />
</DemoContainer>
</div>
)}
</Suspense>
</motion.div>
</div>
</section>
);
}
+148
View File
@@ -0,0 +1,148 @@
type IconProps = React.HTMLAttributes<SVGElement>;
export const Icons = {
logo: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" {...props}>
<rect width="256" height="256" fill="none" />
<line
x1="208"
y1="128"
x2="128"
y2="208"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="32"
/>
<line
x1="192"
y1="40"
x2="40"
y2="192"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="32"
/>
</svg>
),
twitter: (props: IconProps) => (
<svg
{...props}
height="23"
viewBox="0 0 1200 1227"
width="23"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" />
</svg>
),
gitHub: (props: IconProps) => (
<svg viewBox="0 0 438.549 438.549" {...props}>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
),
radix: (props: IconProps) => (
<svg viewBox="0 0 25 25" fill="none" {...props}>
<path
d="M12 25C7.58173 25 4 21.4183 4 17C4 12.5817 7.58173 9 12 9V25Z"
fill="currentcolor"
></path>
<path d="M12 0H4V8H12V0Z" fill="currentcolor"></path>
<path
d="M17 8C19.2091 8 21 6.20914 21 4C21 1.79086 19.2091 0 17 0C14.7909 0 13 1.79086 13 4C13 6.20914 14.7909 8 17 8Z"
fill="currentcolor"
></path>
</svg>
),
aria: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M13.966 22.624l-1.69-4.281H8.122l3.892-9.144 5.662 13.425zM8.884 1.376H0v21.248zm15.116 0h-8.884L24 22.624Z" />
</svg>
),
npm: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"
fill="currentColor"
/>
</svg>
),
yarn: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M12 0C5.375 0 0 5.375 0 12s5.375 12 12 12 12-5.375 12-12S18.625 0 12 0zm.768 4.105c.183 0 .363.053.525.157.125.083.287.185.755 1.154.31-.088.468-.042.551-.019.204.056.366.19.463.375.477.917.542 2.553.334 3.605-.241 1.232-.755 2.029-1.131 2.576.324.329.778.899 1.117 1.825.278.774.31 1.478.273 2.015a5.51 5.51 0 0 0 .602-.329c.593-.366 1.487-.917 2.553-.931.714-.009 1.269.445 1.353 1.103a1.23 1.23 0 0 1-.945 1.362c-.649.158-.95.278-1.821.843-1.232.797-2.539 1.242-3.012 1.39a1.686 1.686 0 0 1-.704.343c-.737.181-3.266.315-3.466.315h-.046c-.783 0-1.214-.241-1.45-.491-.658.329-1.51.19-2.122-.134a1.078 1.078 0 0 1-.58-1.153 1.243 1.243 0 0 1-.153-.195c-.162-.25-.528-.936-.454-1.946.056-.723.556-1.367.88-1.71a5.522 5.522 0 0 1 .408-2.256c.306-.727.885-1.348 1.32-1.737-.32-.537-.644-1.367-.329-2.21.227-.602.412-.936.82-1.08h-.005c.199-.074.389-.153.486-.259a3.418 3.418 0 0 1 2.298-1.103c.037-.093.079-.185.125-.283.31-.658.639-1.029 1.024-1.168a.94.94 0 0 1 .328-.06zm.006.7c-.507.016-1.001 1.519-1.001 1.519s-1.27-.204-2.266.871c-.199.218-.468.334-.746.44-.079.028-.176.023-.417.672-.371.991.625 2.094.625 2.094s-1.186.839-1.626 1.881c-.486 1.144-.338 2.261-.338 2.261s-.843.732-.899 1.487c-.051.663.139 1.2.343 1.515.227.343.51.176.51.176s-.561.653-.037.931c.477.25 1.283.394 1.71-.037.31-.31.371-1.001.486-1.283.028-.065.12.111.209.199.097.093.264.195.264.195s-.755.324-.445 1.066c.102.246.468.403 1.066.398.222-.005 2.664-.139 3.313-.296.375-.088.505-.283.505-.283s1.566-.431 2.998-1.357c.917-.598 1.293-.76 2.034-.936.612-.148.57-1.098-.241-1.084-.839.009-1.575.44-2.196.825-1.163.718-1.742.672-1.742.672l-.018-.032c-.079-.13.371-1.293-.134-2.678-.547-1.515-1.413-1.881-1.344-1.997.297-.5 1.038-1.297 1.334-2.78.176-.899.13-2.377-.269-3.151-.074-.144-.732.241-.732.241s-.616-1.371-.788-1.483a.271.271 0 0 0-.157-.046z"
fill="currentColor"
/>
</svg>
),
pnpm: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z"
fill="currentColor"
/>
</svg>
),
react: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
fill="currentColor"
/>
</svg>
),
tailwind: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z"
fill="currentColor"
/>
</svg>
),
google: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
/>
</svg>
),
apple: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
),
paypal: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.106zm14.146-14.42a3.35 3.35 0 0 0-.607-.541c-.013.076-.026.175-.041.254-.93 4.778-4.005 7.201-9.138 7.201h-2.19a.563.563 0 0 0-.556.479l-1.187 7.527h-.506l-.24 1.516a.56.56 0 0 0 .554.647h3.882c.46 0 .85-.334.922-.788.06-.26.76-4.852.816-5.09a.932.932 0 0 1 .923-.788h.58c3.76 0 6.705-1.528 7.565-5.946.36-1.847.174-3.388-.777-4.471z"
fill="currentColor"
/>
</svg>
),
spinner: (props: IconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
),
};
+16
View File
@@ -0,0 +1,16 @@
import { cn } from "@/lib/utils";
interface LoadingProps {
className?: string;
}
export function Loading({ className }: LoadingProps) {
return (
<div className={cn("flex items-center justify-center min-h-[400px]", className)}>
<div className="relative">
<div className="w-12 h-12 rounded-full absolute border-4 border-solid border-gray-200"></div>
<div className="w-12 h-12 rounded-full animate-spin absolute border-4 border-solid border-primary border-t-transparent"></div>
</div>
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
"use client";
import { useEffect } from "react";
import { initPostHog } from "@/lib/posthog";
export function PostHogInit() {
useEffect(() => {
initPostHog();
}, []);
return null;
}
+20
View File
@@ -0,0 +1,20 @@
import { ReactNode } from "react";
interface SocialLinkProps {
href: string;
children: ReactNode;
className?: string;
}
export function SocialLink({ href, children, className = "" }: SocialLinkProps) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={`text-foreground/60 hover:text-foreground transition-colors ${className}`}
>
{children}
</a>
);
}
+138
View File
@@ -0,0 +1,138 @@
"use client";
import { createContext, useContext, useEffect } from "react";
import { useEditorStore } from "../store/editor-store";
import { colorFormatter } from "../utils/color-converter";
import { setShadowVariables } from "@/utils/shadows";
import { applyStyleToElement } from "@/utils/apply-style-to-element";
import { ThemeStyleProps, ThemeStyles } from "@/types/theme";
import { useThemePresetFromUrl } from "@/hooks/use-theme-preset-from-url";
import { COMMON_STYLES } from "@/config/theme";
type Theme = "dark" | "light";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
};
type Coords = { x: number; y: number };
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: (coords?: Coords) => void;
};
const COMMON_NON_COLOR_KEYS = COMMON_STYLES;
const initialState: ThemeProviderState = {
theme: "light",
setTheme: () => null,
toggleTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
// Helper functions
const applyCommonStyles = (root: HTMLElement, themeStyles: ThemeStyleProps) => {
Object.entries(themeStyles)
.filter(([key]) =>
COMMON_NON_COLOR_KEYS.includes(key as (typeof COMMON_NON_COLOR_KEYS)[number])
)
.forEach(([key, value]) => {
if (typeof value === "string") {
applyStyleToElement(root, key, value);
}
});
};
const applyThemeColors = (
root: HTMLElement,
themeStyles: ThemeStyles,
mode: Theme
) => {
Object.entries(themeStyles[mode]).forEach(([key, value]) => {
if (
typeof value === "string" &&
!COMMON_NON_COLOR_KEYS.includes(key as (typeof COMMON_NON_COLOR_KEYS)[number])
) {
const hslValue = colorFormatter(value, "hsl", "4");
applyStyleToElement(root, key, hslValue);
}
});
};
const updateThemeClass = (root: HTMLElement, mode: Theme) => {
if (mode === "light") {
root.classList.remove("dark");
} else {
root.classList.add("dark");
}
};
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const { themeState, setThemeState } = useEditorStore();
// Handle theme preset from URL
useThemePresetFromUrl();
useEffect(() => {
const root = window.document.documentElement;
const { currentMode: mode, styles: themeStyles } = themeState;
updateThemeClass(root, mode);
applyCommonStyles(root, themeStyles.light);
applyThemeColors(root, themeStyles, mode);
setShadowVariables(themeState);
}, [themeState]);
const handleThemeChange = (newMode: Theme) => {
setThemeState({ ...themeState, currentMode: newMode });
};
const handleThemeToggle = (coords?: Coords) => {
const root = document.documentElement;
const newMode = themeState.currentMode === "light" ? "dark" : "light";
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (!document.startViewTransition || prefersReducedMotion) {
handleThemeChange(newMode);
return;
}
if (coords) {
root.style.setProperty("--x", `${coords.x}px`);
root.style.setProperty("--y", `${coords.y}px`);
}
document.startViewTransition(() => {
handleThemeChange(newMode);
});
};
const value: ThemeProviderState = {
theme: themeState.currentMode,
setTheme: handleThemeChange,
toggleTheme: handleThemeToggle,
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};

Some files were not shown because too many files have changed in this diff Show More