chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:11 +08:00
commit 94d5d7e16e
605 changed files with 80879 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}
+10
View File
@@ -0,0 +1,10 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
+8
View File
@@ -0,0 +1,8 @@
node_modules/
target/
.next/
build/
dist/
/templates/
/fixtures/
+32
View File
@@ -0,0 +1,32 @@
{
"$schema": "https://json.schemastore.org/eslintrc",
"root": true,
"extends": [
"next/core-web-vitals",
"turbo",
"prettier",
"plugin:tailwindcss/recommended"
],
"plugins": ["tailwindcss"],
"ignorePatterns": ["**/fixtures/**"],
"rules": {
"@next/next/no-html-link-for-pages": "off",
"tailwindcss/no-custom-classname": "off",
"tailwindcss/classnames-order": "error"
},
"settings": {
"tailwindcss": {
"callees": ["cn", "cva"],
"config": "tailwind.config.cjs"
},
"next": {
"rootDir": ["apps/*/"]
}
},
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser"
}
]
}
+51
View File
@@ -0,0 +1,51 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnpm-store
.pnp
.pnp.js
# testing
coverage
# next.js
.next/
out/
build
# misc
.DS_Store
*.pem
.bmad-core/
.claude/
.cursor/
web-bundles/
.replit
replit.md
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# turbo
.turbo
.contentlayer
tsconfig.tsbuildinfo
# ide
.idea
.fleet
.vscode
REGISTRY_STRUCTURE.md
packages/*
+18
View File
@@ -0,0 +1,18 @@
# .kodiak.toml
version = 1
[merge]
automerge_label = "automerge"
require_automerge_label = true
method = "squash"
delete_branch_on_merge = true
optimistic_updates = false
prioritize_ready_to_merge = true
notify_on_conflict = true
[merge.message]
title = "pull_request_title"
body = "pull_request_body"
include_pr_number = true
body_type = "markdown"
strip_html_comments = true
+7
View File
@@ -0,0 +1,7 @@
dist
node_modules
.next
build
.contentlayer
apps/www/pages/api/registry.json
**/fixtures
+143
View File
@@ -0,0 +1,143 @@
# Contributing
Thanks for your interest in contributing to ui.creative-tim.com.
Please take a moment to review this document before submitting your first pull request. We also strongly recommend that you check for open issues and pull requests to see if someone else is working on something similar.
If you need any help, feel free to reach out to [Creative Tim](https://creative-tim.com/contact).
## About this repository
This repository is a monorepo.
- We use [pnpm](https://pnpm.io) and [`workspaces`](https://pnpm.io/workspaces) for development.
- We use [Turborepo](https://turbo.build/repo) as our build system.
## Structure
This repository is structured as follows:
```
apps
└── www
├── app
├── components
├── content
└── registry
└── creative-tim-ui
├── example
└── ui
```
| Path | Description |
| --------------------- | ---------------------------------------- |
| `apps/www/app` | The Next.js application for the website. |
| `apps/www/components` | The React components for the website. |
| `apps/www/content` | The content for the website. |
| `apps/www/registry` | The registry for the components. |
## Development
### Fork this repo
You can fork this repo by clicking the fork button in the top right corner of this page.
### Clone on your local machine
```bash
git clone https://github.com/creative-tim/ui.git
```
### Navigate to project directory
```bash
cd ui
```
### Create a new Branch
```bash
git checkout -b my-new-branch
```
### Install dependencies
```bash
pnpm install
```
### Run a workspace
You can use the `pnpm --filter=[WORKSPACE]` command to start the development process for a workspace.
#### Examples
1. To run the `creative-tim.com/ui` website:
```bash
pnpm dev
```
## Documentation
The documentation for this project is located in the `www` workspace. You can run the documentation locally by running the following command:
```bash
pnpm dev
```
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/www/content/docs` directory.
## Components
We use a registry system for developing components. You can find the source code for the components under `apps/www/registry`. The components are organized by styles.
```bash
apps
└── www
└── registry
└── creative-tim
├── blocks
└── ui
```
When adding or modifying components, please ensure that:
1. You make the changes for every style.
2. You update the documentation.
3. You run `pnpm build:registry` to update the registry.
## Commit Convention
Before you create a Pull Request, please check whether your commits comply with
the commit conventions used in this repository.
When you create a commit we kindly ask you to follow the convention
`category(scope or module): message` in your commit message while using one of
the following categories:
- `feat / feature`: all changes that introduce completely new code or new
features
- `fix`: changes that fix a bug (ideally you will additionally reference an
issue if present)
- `refactor`: any code related change that is not a fix nor a feature
- `docs`: changing existing or creating new documentation (i.e. README, docs for
usage of a lib or cli usage)
- `build`: all changes regarding the build of the software, changes to
dependencies or the addition of new dependencies
- `test`: all changes regarding tests (adding new tests or changing existing
ones)
- `ci`: all changes regarding the configuration of continuous integration (i.e.
github actions, ci system)
- `chore`: all changes to the repository that do not fit into any of the above
categories
e.g. `feat(components): add new prop to the avatar component`
If you are interested in the detailed specification you can visit
https://www.conventionalcommits.org/ or check out the
[Angular Commit Message Guidelines](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines).
## Requests for new components
If you have a request for a new component, please open a discussion on GitHub. We'll be happy to help you out.
Please ensure that the tests are passing when submitting a pull request. If you're adding new features, please include tests.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 [Creative Tim](https://www.creative-tim.com/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software "), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+205
View File
@@ -0,0 +1,205 @@
[![Creative Tim UI](https://raw.githubusercontent.com/creativetimofficial/ui/refs/heads/main/apps/www/public/opengraph-image.png)](https://creative-tim.com/ui)
# Creative Tim UI
[Creative Tim UI](https://creative-tim.com/ui) is a comprehensive component library built on top of [shadcn/ui](https://ui.shadcn.com/) to help you build modern web applications faster.
## Overview
Creative Tim UI provides pre-built, customizable React components and blocks designed for building beautiful, production-ready web applications.
The CLI makes it easy to add these components to your Next.js project.
## Installation
You can use the Creative Tim UI CLI directly with npx, or install it globally:
```bash
# Use directly (recommended)
npx @creative-tim/ui@latest add <component-name>
# Or using shadcn cli
npx shadcn@latest add https://creative-tim.com/ui/r/all.json
```
## Prerequisites
Before using Creative Tim UI, ensure your Next.js project meets these requirements:
- **Node.js 18** or later
- **shadcn/ui** initialized in your project (npx shadcn@latest init)
- **Tailwind CSS** configured
## Usage
### Install All Components
Install all available Creative Tim UI components at once:
```bash
npx @creative-tim/ui@latest add all
```
This command will:
- Set up shadcn/ui if not already configured
- Install all Creative Tim UI components to your configured components directory
- Add necessary dependencies to your project
### Install Specific Components
Install individual components using the `components add` command:
```bash
npx @creative-tim/ui@latest add <component-name>
```
Examples:
```bash
# Install the orb component
npx @creative-tim/ui@latest add card
```
### Alternative: Use with shadcn CLI
You can also install components using the standard shadcn/ui CLI:
```bash
# Install all components
npx shadcn@latest add https://creative-tim.com/ui/r/all.json
# Install a specific component
npx shadcn@latest add https://creative-tim.com/ui/r/button.json
```
All available components can be found [here](https://creative-tim.com/ui/docs/components) or explore a list of example blocks [here](https://creative-tim.com/ui/blocks).
## Blocks
Explore our collection of ready-to-use blocks organized by category. Each block is fully customizable and can be added to your project with a single command.
### Application UI
<table>
<tr>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/modals">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/modals-thumbnail.jpg" alt="Modals" />
<br/>
<strong>Modals</strong><br/>
<em>5 Blocks</em>
</a>
</td>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/account">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/account-thumbnail.jpg" alt="Account" />
<br/>
<strong>Account</strong><br/>
<em>7 Blocks</em>
</a>
</td>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/billing">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/billing-thumbnail.jpg" alt="Billing" />
<br/>
<strong>Billing</strong><br/>
<em>5 Blocks</em>
</a>
</td>
</tr>
</table>
### Marketing
<table>
<tr>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/testimonials">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/testimonial-thumbnail.jpg" alt="Testimonial Sections" />
<br/>
<strong>Testimonial Sections</strong><br/>
<em>17 Blocks</em>
</a>
</td>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/contact">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/contact-us-thumbnail.jpg" alt="Contact Sections" />
<br/>
<strong>Contact Sections</strong><br/>
<em>15 Blocks</em>
</a>
</td>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/footers">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/footer-thumbnail.jpg" alt="Footers" />
<br/>
<strong>Footers</strong><br/>
<em>16 Blocks</em>
</a>
</td>
</tr>
<tr>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/faqs">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/faq-thumbnail.jpg" alt="FAQs" />
<br/>
<strong>FAQs</strong><br/>
<em>6 Blocks</em>
</a>
</td>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/blog">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/blog-posts-thumbnail.jpg" alt="Blog" />
<br/>
<strong>Blog</strong><br/>
<em>15 Blocks</em>
</a>
</td>
</tr>
</table>
### Ecommerce UI
Ready-to-use blocks for product listings, shopping carts, and checkout flows.
<table>
<tr>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/ecommerce">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/ecommerce-thumbnail.jpg" alt="Ecommerce Sections" />
<br/>
<strong>Ecommerce Sections</strong><br/>
<em>14 Blocks</em>
</a>
</td>
</tr>
</table>
### Web 3.0
Innovative sections built for decentralized applications, blockchain projects, and crypto platforms.
<table>
<tr>
<td width="25%">
<a href="https://creative-tim.com/ui/blocks/web3">
<img src="https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/collections-thumbnail.jpg" alt="Web 3.0 Cards" />
<br/>
<strong>Web 3.0 Cards</strong><br/>
<em>5 Blocks</em>
</a>
</td>
</tr>
</table>
## Contributing
If you'd like to contribute to Creative Tim UI, please follow these steps:
1. Fork the repository
2. Create a new branch
3. Make your changes to the components in the registry.
4. Open a PR to the main branch.
Please read the [contributing guide](/CONTRIBUTING.md).
## Copyrights and Licenses
Creative Tim UI is built upon the incredible work of the open source community:
- **[shadcn/ui](https://ui.shadcn.com/)** - The documentation structure, registry system, and foundational (atomic) components are from the open source work in shadcn/ui. [MIT License](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md)
- **[Material Tailwind](https://material-tailwind.com/v3)** by Creative Tim - The blocks and component designs are inspired by and based on Material Tailwind Framework. [MIT License](https://github.com/creativetimofficial/material-tailwind/blob/main/LICENSE.md)
- **[Eleven Labs UI](https://github.com/elevenlabs/elevenlabs-ui)** - General inspiration for the documentation structure and approach of blocks. [MIT License](https://github.com/elevenlabs/ui/blob/main/LICENSE.md)
- **[Geist Font](https://vercel.com/font)** by Vercel - The beautiful typeface used throughout the interface. [SIL Open Font License 1.1](https://github.com/vercel/geist-font/blob/main/LICENSE.txt)
We are grateful to these projects for making their work available under open source licenses.
## License
Licensed under the [MIT license](https://github.com/creativetimofficial/ui/blob/main/LICENSE.md).
Made with love by [Creative Tim](https://creative-tim.com).
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`creativetimofficial/ui`
- 原始仓库:https://github.com/creativetimofficial/ui
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+10
View File
@@ -0,0 +1,10 @@
# Private Registry Authentication
# Set your API key to access private components (testimonials-03, testimonials-04)
# Get your API key from: https://creative-tim.com/ui
API_KEY=your_api_key_here
NEXT_PUBLIC_V0_URL=https://v0.app
NEXT_PUBLIC_APP_URL=http://localhost:5000
NEXT_PUBLIC_ASSET_PREFIX = "/ui"
NEXT_PUBLIC_VIEW_PATH = "/ui/view"
+48
View File
@@ -0,0 +1,48 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# generated content
.contentlayer
.content-collections
.source
+7
View File
@@ -0,0 +1,7 @@
dist
node_modules
.next
build
.contentlayer
registry/__index__.tsx
content/docs/components/calendar.mdx
+220
View File
@@ -0,0 +1,220 @@
import { Metadata } from "next"
import Link from "next/link"
import { Download, Package, Star, Users } from "lucide-react"
import { ExamplesNav } from "@/components/examples-nav"
import { ExamplesPreview } from "@/components/examples-preview"
import { ShowcaseMasonry } from "@/components/showcase-masonry"
import { TestimonialsSection } from "@/components/testimonials-section"
import {
PageActions,
PageHeader,
PageHeaderDescription,
PageHeaderHeading,
} from "@/components/page-header"
import { PageNav } from "@/components/page-nav"
import { ThemeSelector } from "@/components/theme-selector"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
const title = "Creative Tim UI"
const description =
"Open-source components, blocks, and AI agents designed to speed up your workflow. Import them seamlessly into your favorite tools through Registry and MCPs."
export const dynamic = "force-static"
export const revalidate = false
export const metadata: Metadata = {
title,
description,
openGraph: {
images: [
{
url: `/og?title=${encodeURIComponent(
title
)}&description=${encodeURIComponent(description)}`,
},
],
},
twitter: {
card: "summary_large_image",
images: [
{
url: `/og?title=${encodeURIComponent(
title
)}&description=${encodeURIComponent(description)}`,
},
],
},
}
export default function IndexPage() {
const assetPrefix = process.env.NEXT_PUBLIC_ASSET_PREFIX
return (
<div className="bg-stone-50 dark:bg-stone-950 flex flex-1 flex-col">
<div className="relative overflow-hidden">
<div
className="pointer-events-none absolute inset-0"
style={{
background:
"radial-gradient(circle at center, transparent 0%, transparent 25%, hsl(var(--background) / 0.5) 50%, hsl(var(--background) / 0.8) 70%, hsl(var(--background) / 0.95) 85%, hsl(var(--background)) 95%)",
}}
/>
<PageHeader className="relative z-10">
<PageHeaderHeading className="max-w-4xl">
<span className="flex items-baseline gap-2 sm:gap-3">
<span className="font-geist-bold leading-[0.95] font-bold tracking-[-0.03em]">
Creative Tim
</span>
<span className="font-geist font-normal tracking-[-0.02em] opacity-90">
UI
</span>
</span>
</PageHeaderHeading>
<PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions>
<Button asChild size="sm" variant="outline">
<Link href="/docs">Get Started</Link>
</Button>
<Button asChild size="sm">
<Link href="/blocks">View Blocks</Link>
</Button>
</PageActions>
{/* Logo Section */}
<div className="mt-16 flex flex-col items-center gap-6">
<div className="flex flex-wrap items-center justify-center gap-8">
{/* OpenAI */}
<div className="opacity-50 grayscale transition-all hover:opacity-100 hover:grayscale-0 dark:invert dark:opacity-60 dark:hover:invert-0">
<img
src={`${assetPrefix}/logo-open-ai.png`}
alt="OpenAI"
className="h-7 w-auto"
/>
</div>
{/* Claude */}
<div className="opacity-50 grayscale transition-all hover:opacity-100 hover:grayscale-0 dark:invert dark:opacity-60 dark:hover:invert-0">
<img
src={`${assetPrefix}/logo-claude.png`}
alt="Claude"
className="h-7 w-auto"
/>
</div>
{/* v0 */}
<div className="opacity-50 grayscale transition-all hover:opacity-100 hover:grayscale-0 dark:invert dark:opacity-60 dark:hover:invert-0">
<img
src={`${assetPrefix}/logo-v0.png`}
alt="v0"
className="h-7 w-auto"
/>
</div>
{/* Replit */}
<div className="opacity-50 grayscale transition-all hover:opacity-100 hover:grayscale-0 dark:invert dark:opacity-60 dark:hover:invert-0">
<img
src={`${assetPrefix}/logo-replit.png`}
alt="Replit"
className="h-7 w-auto"
/>
</div>
{/* Bolt */}
<div className="opacity-50 grayscale transition-all hover:opacity-100 hover:grayscale-0 dark:invert dark:opacity-60 dark:hover:invert-0">
<img
src={`${assetPrefix}/logo-bolt.png`}
alt="Bolt"
className="h-7 w-auto"
/>
</div>
{/* Lovable */}
<div className="opacity-50 grayscale transition-all hover:opacity-100 hover:grayscale-0 dark:invert dark:opacity-60 dark:hover:invert-0">
<img
src={`${assetPrefix}/logo-lovable.png`}
alt="Lovable"
className="h-7 w-auto"
/>
</div>
</div>
</div>
</PageHeader>
</div>
<ShowcaseMasonry />
{/* Achievements Section */}
<section className="container py-12 md:py-20">
<div className="mx-auto text-center">
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">
Remarkable Achievements
</h2>
<p className="text-muted-foreground mx-auto mb-12 max-w-3xl text-base sm:text-lg">
Discover how our UI Tools have transformed web development. These
achievements showcase our dedication to innovation and our
community&apos;s growth.
</p>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card className="p-6 text-left gap-0">
<Users className="text-muted-foreground mb-4 h-8 w-8" />
<div className="mb-1 text-3xl font-bold">2.6M+</div>
<div className="mb-3 text-sm font-medium">Community Members</div>
<p className="text-muted-foreground text-sm">
Join our community of developers and designers
</p>
</Card>
<Card className="p-6 text-left gap-0">
<Download className="text-muted-foreground mb-4 h-8 w-8" />
<div className="mb-1 text-3xl font-bold">8.6M+</div>
<div className="mb-3 text-sm font-medium">
Cumulative Downloads
</div>
<p className="text-muted-foreground text-sm">
Based on Material Tailwind and Creative Tim Products
</p>
</Card>
<Card className="p-6 text-left gap-0">
<Star className="text-muted-foreground mb-4 h-8 w-8" />
<div className="mb-1 text-3xl font-bold">48,000+</div>
<div className="mb-3 text-sm font-medium">
Github Cumulative Stars
</div>
<p className="text-muted-foreground text-sm">
On 100+ Open Source Repositories and Projects
</p>
</Card>
<Card className="p-6 text-left gap-0">
<Package className="text-muted-foreground mb-4 h-8 w-8" />
<div className="mb-1 text-3xl font-bold">280,000+</div>
<div className="mb-3 text-sm font-medium">
Monthly NPM Downloads
</div>
<p className="text-muted-foreground text-sm">
Including React, HTML, Tailwind CSS and more.
</p>
</Card>
</div>
</div>
</section>
{/* Examples Preview Section */}
<ExamplesPreview />
{/* Testimonials Section */}
<TestimonialsSection />
<PageNav className="hidden md:flex">
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
<ThemeSelector className="mr-4 hidden md:flex" />
</PageNav>
</div>
)
}
@@ -0,0 +1,30 @@
import { getAllBlockIds } from "@/lib/blocks"
import { BlockDisplay } from "@/components/block-display"
import { registryCategories } from "@/registry/registry-categories"
export const revalidate = false
export const dynamic = "force-static"
export const dynamicParams = false
export async function generateStaticParams() {
return registryCategories.map((category) => ({
categories: [category.slug],
}))
}
export default async function BlocksPage({
params,
}: {
params: Promise<{ categories?: string[] }>
}) {
const { categories = [] } = await params
const blocks = await getAllBlockIds(["registry:block"], categories)
return (
<div className="flex flex-col gap-12 md:gap-24">
{blocks.map((name) => (
<BlockDisplay name={name} key={name} />
))}
</div>
)
}
+71
View File
@@ -0,0 +1,71 @@
import { Metadata } from "next"
import Link from "next/link"
import { BlocksNav } from "@/components/blocks-nav"
import {
PageActions,
PageHeader,
PageHeaderDescription,
PageHeaderHeading,
} from "@/components/page-header"
import { PageNav } from "@/components/page-nav"
import { Button } from "@/components/ui/button"
const title = "Blocks"
const description =
"A collection of building blocks and components made by Creative Tim on top of shadcn/ui."
export const metadata: Metadata = {
title,
description,
openGraph: {
images: [
{
url: `/og?title=${encodeURIComponent(
title
)}&description=${encodeURIComponent(description)}`,
},
],
},
twitter: {
card: "summary_large_image",
images: [
{
url: `/og?title=${encodeURIComponent(
title
)}&description=${encodeURIComponent(description)}`,
},
],
},
}
export default function BlocksLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<PageHeader>
<PageHeaderHeading>{title}</PageHeaderHeading>
<PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions>
<Button asChild size="sm">
<a href="#blocks">Browse Blocks</a>
</Button>
<Button asChild variant="ghost" size="sm">
<Link href="https://github.com/creativetimofficial/ui">
Add a block
</Link>
</Button>
</PageActions>
</PageHeader>
<PageNav id="blocks">
<BlocksNav />
</PageNav>
<div className="container-wrapper section-soft flex-1 md:py-12">
<div className="container">{children}</div>
</div>
</>
)
}
+23
View File
@@ -0,0 +1,23 @@
import { BlockDisplay } from "@/components/block-display"
export const dynamic = "force-static"
export const revalidate = false
const FEATURED_BLOCKS = [
"software-purchase-01",
"dark-product-overview-01",
"web3-04",
"account-basic-info-01",
"cruds-01",
"testimonials-03",
]
export default async function BlocksPage() {
return (
<div className="flex flex-col gap-12 md:gap-24">
{FEATURED_BLOCKS.map((name) => (
<BlockDisplay name={name} key={name} />
))}
</div>
)
}
@@ -0,0 +1,212 @@
import Link from "next/link"
import { notFound } from "next/navigation"
import { mdxComponents } from "@/mdx-components"
import {
IconArrowLeft,
IconArrowRight,
IconArrowUpRight,
} from "@tabler/icons-react"
import { findNeighbour } from "fumadocs-core/server"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
import { DocsCopyPage } from "@/components/docs-copy-page"
import { DocsSidebarCta } from "@/components/docs-sidebar-cta"
import { DocsTableOfContents } from "@/components/docs-toc"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
export const revalidate = false
export const dynamic = "force-static"
export const dynamicParams = false
export function generateStaticParams() {
return source.generateParams()
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>
}) {
const params = await props.params
const page = source.getPage(params.slug)
if (!page) {
notFound()
}
const doc = page.data
if (!doc.title || !doc.description) {
notFound()
}
return {
title: doc.title,
description: doc.description,
openGraph: {
title: doc.title,
description: doc.description,
type: "article",
url: absoluteUrl(page.url),
images: [
{
url: `/og?title=${encodeURIComponent(
doc.title
)}&description=${encodeURIComponent(doc.description)}`,
},
],
},
twitter: {
card: "summary_large_image",
title: doc.title,
description: doc.description,
images: [
{
url: `/og?title=${encodeURIComponent(
doc.title
)}&description=${encodeURIComponent(doc.description)}`,
},
],
creator: "@creativetim",
},
}
}
export default async function Page(props: {
params: Promise<{ slug?: string[] }>
}) {
const params = await props.params
const page = source.getPage(params.slug)
if (!page) {
notFound()
}
const doc = page.data
// @ts-expect-error - revisit fumadocs types.
const MDX = doc.body
const neighbours = await findNeighbour(source.pageTree, page.url)
// @ts-expect-error - revisit fumadocs types.
const links = doc.links
return (
<div
data-slot="docs"
className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full"
>
<div className="flex min-w-0 flex-1 flex-col">
<div className="h-(--top-spacing) shrink-0" />
<div className="mx-auto flex w-full max-w-2xl min-w-0 flex-1 flex-col gap-8 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2">
<div className="flex items-start justify-between">
<h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
{doc.title}
</h1>
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:pt-1.5 sm:backdrop-blur-none">
<DocsCopyPage
// @ts-expect-error - revisit fumadocs types.
page={doc.content}
url={absoluteUrl(page.url)}
/>
{neighbours.previous && (
<Button
variant="secondary"
size="icon"
className="extend-touch-target ml-auto size-8 shadow-none md:size-7"
asChild
>
<Link href={neighbours.previous.url}>
<IconArrowLeft />
<span className="sr-only">Previous</span>
</Link>
</Button>
)}
{neighbours.next && (
<Button
variant="secondary"
size="icon"
className="extend-touch-target size-8 shadow-none md:size-7"
asChild
>
<Link href={neighbours.next.url}>
<span className="sr-only">Next</span>
<IconArrowRight />
</Link>
</Button>
)}
</div>
</div>
{doc.description && (
<p className="text-muted-foreground text-[1.05rem] text-balance sm:text-base">
{doc.description}
</p>
)}
</div>
{links ? (
<div className="flex items-center space-x-2 pt-4">
{links?.doc && (
<Badge asChild variant="secondary">
<Link href={links.doc} target="_blank" rel="noreferrer">
Docs <IconArrowUpRight />
</Link>
</Badge>
)}
{links?.api && (
<Badge asChild variant="secondary">
<Link href={links.api} target="_blank" rel="noreferrer">
API Reference <IconArrowUpRight />
</Link>
</Badge>
)}
</div>
) : null}
</div>
<div className="w-full flex-1 *:data-[slot=alert]:first:mt-0">
<MDX components={mdxComponents} />
</div>
</div>
<div className="mx-auto hidden h-16 w-full max-w-2xl items-center gap-2 px-4 sm:flex md:px-0">
{neighbours.previous && (
<Button
variant="secondary"
size="sm"
asChild
className="shadow-none"
>
<Link href={neighbours.previous.url}>
<IconArrowLeft /> {neighbours.previous.name}
</Link>
</Button>
)}
{neighbours.next && (
<Button
variant="secondary"
size="sm"
className="ml-auto shadow-none"
asChild
>
<Link href={neighbours.next.url}>
{neighbours.next.name} <IconArrowRight />
</Link>
</Button>
)}
</div>
</div>
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="h-(--top-spacing) shrink-0" />
{/* @ts-expect-error - revisit fumadocs types. */}
{doc.toc?.length ? (
<div className="no-scrollbar overflow-y-auto px-8">
{/* @ts-expect-error - revisit fumadocs types. */}
<DocsTableOfContents toc={doc.toc} />
<div className="h-12" />
</div>
) : null}
<div className="flex flex-1 flex-col gap-12 px-6">
<DocsSidebarCta />
</div>
</div>
</div>
)
}
+18
View File
@@ -0,0 +1,18 @@
import { source } from "@/lib/source"
import { DocsSidebar } from "@/components/docs-sidebar"
import { SidebarProvider } from "@/components/ui/sidebar"
export default function DocsLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="container-wrapper flex flex-1 flex-col px-2">
<SidebarProvider className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--sidebar-width:220px] [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--sidebar-width:240px] lg:[--top-spacing:calc(var(--spacing)*4)]">
<DocsSidebar tree={source.pageTree} />
<div className="h-full w-full">{children}</div>
</SidebarProvider>
</div>
)
}
+12
View File
@@ -0,0 +1,12 @@
import { SiteFooter } from "@/components/site-footer"
import { SiteHeader } from "@/components/site-header"
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="bg-background relative z-10 flex min-h-svh flex-col">
<SiteHeader />
<main className="flex flex-1 flex-col">{children}</main>
<SiteFooter />
</div>
)
}
@@ -0,0 +1,29 @@
import { notFound } from "next/navigation"
import { NextResponse, type NextRequest } from "next/server"
import { source } from "@/lib/source"
export const revalidate = false
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
const slug = (await params).slug
const page = source.getPage(slug)
if (!page) {
notFound()
}
// @ts-expect-error - revisit fumadocs types.
return new NextResponse(page.data.content, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},
})
}
export function generateStaticParams() {
return source.generateParams()
}
+103
View File
@@ -0,0 +1,103 @@
import * as React from "react"
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { registryItemSchema } from "shadcn/schema"
import { z } from "zod"
import { siteConfig } from "@/lib/config"
import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
import { absoluteUrl, cn } from "@/lib/utils"
export const revalidate = false
export const dynamic = "force-static"
export const dynamicParams = false
const getCachedRegistryItem = React.cache(async (name: string) => {
return await getRegistryItem(name)
})
export async function generateMetadata({
params,
}: {
params: Promise<{
name: string
}>
}): Promise<Metadata> {
const { name } = await params
const item = await getCachedRegistryItem(name)
if (!item) {
return {}
}
const title = item.name
const description = item.description
return {
title: item.description,
description,
openGraph: {
title,
description,
type: "article",
url: absoluteUrl(`/view/${item.name}`),
images: [
{
url: siteConfig.ogImage,
width: 1200,
height: 630,
alt: siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [siteConfig.ogImage],
creator: "@creativetim",
},
}
}
export async function generateStaticParams() {
const { Index } = await import("@/registry/__index__")
const index = z.record(registryItemSchema).parse(Index)
return Object.values(index)
.filter((block) =>
[
"registry:block",
"registry:component",
"registry:example",
"registry:internal",
].includes(block.type)
)
.map((block) => ({
name: block.name,
}))
}
export default async function BlockPage({
params,
}: {
params: Promise<{
name: string
}>
}) {
const { name } = await params
const item = await getCachedRegistryItem(name)
const Component = getRegistryComponent(name)
if (!item || !Component) {
return notFound()
}
return (
<>
<div className={cn("bg-background", item.meta?.container)}>
<Component />
</div>
</>
)
}
+87
View File
@@ -0,0 +1,87 @@
import { existsSync, readFileSync } from "fs"
import path from "path"
import { NextRequest, NextResponse } from "next/server"
const PRIVATE_COMPONENTS = ["testimonials-03", "testimonials-04"]
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ name: string }> }
) {
const { name: componentName } = await params
if (!PRIVATE_COMPONENTS.includes(componentName)) {
return NextResponse.json(
{ error: "Component not found or not private" },
{ status: 404 }
)
}
const authHeader = request.headers.get("authorization")
const token = authHeader?.replace("Bearer ", "")
const queryToken = request.nextUrl.searchParams.get("token")
const apiKey = request.headers.get("x-api-key")
const providedToken = token || queryToken || apiKey
if (!isValidToken(providedToken)) {
return NextResponse.json(
{
error: "Unauthorized",
message:
"Authentication required for private components. Set API_KEY in your .env.local file or provide it via Authorization header, X-API-Key header, or ?token= query parameter.",
},
{ status: 401 }
)
}
try {
const componentPath = path.join(
process.cwd(),
"public",
"r",
`${componentName}.json`
)
if (!existsSync(componentPath)) {
return NextResponse.json(
{ error: "Component not found" },
{ status: 404 }
)
}
const componentData = readFileSync(componentPath, "utf-8")
const component = JSON.parse(componentData)
return NextResponse.json(component, {
headers: {
"Content-Type": "application/json",
"Cache-Control": "private, max-age=3600",
},
})
} catch (error) {
console.error(`Error serving private component ${componentName}:`, error)
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}
function isValidToken(token: string | null): boolean {
if (!token) {
return false
}
const validApiKey = process.env.API_KEY
if (!validApiKey) {
console.warn(
"API_KEY not set in environment variables. Private components will be inaccessible."
)
return false
}
return token === validApiKey
}
+107
View File
@@ -0,0 +1,107 @@
import type { Metadata } from "next"
import { META_THEME_COLORS, siteConfig } from "@/lib/config"
import { fontVariables } from "@/lib/fonts"
import { cn } from "@/lib/utils"
import { LayoutProvider } from "@/hooks/use-layout"
import { ActiveThemeProvider } from "@/components/active-theme"
import { Analytics } from "@/components/analytics"
import { TailwindIndicator } from "@/components/tailwind-indicator"
import { ThemeProvider } from "@/components/theme-provider"
import { Toaster } from "@/components/ui/sonner"
import "@/styles/globals.css"
export const metadata: Metadata = {
title: {
default: siteConfig.name,
template: `%s | ${siteConfig.name}`,
},
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL!),
description: siteConfig.description,
keywords: ["Creative Tim", "UI", "shadcn", "Components", "agents"],
authors: [
{
name: "Creative Tim",
url: "https://creative-tim.com",
},
],
creator: "creative-tim",
openGraph: {
type: "website",
locale: "en_US",
url:
process.env.NEXT_PUBLIC_APP_URL! + process.env.NEXT_PUBLIC_ASSET_PREFIX,
title: siteConfig.name,
description: siteConfig.description,
siteName: siteConfig.name,
images: [
{
url: `${process.env.NEXT_PUBLIC_APP_URL}${process.env.NEXT_PUBLIC_ASSET_PREFIX}/opengraph-image.png`,
width: 1200,
height: 630,
alt: siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
title: siteConfig.name,
description: siteConfig.description,
images: [
`${process.env.NEXT_PUBLIC_APP_URL}${process.env.NEXT_PUBLIC_ASSET_PREFIX}/opengraph-image.png`,
],
creator: "@creativetim",
},
icons: {
icon: `${process.env.NEXT_PUBLIC_APP_URL}${process.env.NEXT_PUBLIC_ASSET_PREFIX}/favicon.ico`,
shortcut: `${process.env.NEXT_PUBLIC_APP_URL}${process.env.NEXT_PUBLIC_ASSET_PREFIX}/favicon-16x16.png`,
apple: `${process.env.NEXT_PUBLIC_APP_URL}${process.env.NEXT_PUBLIC_ASSET_PREFIX}/apple-touch-icon.png`,
},
manifest: `${process.env.NEXT_PUBLIC_APP_URL}${process.env.NEXT_PUBLIC_ASSET_PREFIX}/site.webmanifest`,
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.theme === 'dark' || ((!('theme' in localStorage) || localStorage.theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${META_THEME_COLORS.dark}')
}
if (localStorage.layout) {
document.documentElement.classList.add('layout-' + localStorage.layout)
}
} catch (_) {}
`,
}}
/>
<meta name="theme-color" content={META_THEME_COLORS.light} />
</head>
<body
className={cn(
"text-foreground group/body overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
fontVariables
)}
>
<ThemeProvider>
<LayoutProvider>
<ActiveThemeProvider>
{children}
<TailwindIndicator />
<Toaster position="top-center" />
<Analytics />
</ActiveThemeProvider>
</LayoutProvider>
</ThemeProvider>
</body>
</html>
)
}
+140
View File
@@ -0,0 +1,140 @@
import { registryCategories } from "@/registry/registry-categories"
import { registryCollections } from "@/registry/registry-collections"
import blocksMetadata from "@/registry/__blocks__.json"
const BASE_URL = "https://www.creative-tim.com/ui"
type BlockMeta = {
name: string
description: string
categories: string[]
}
export async function GET() {
const blocks = blocksMetadata as BlockMeta[]
// Group blocks by category slug
const byCategory: Record<string, BlockMeta[]> = {}
for (const block of blocks) {
for (const cat of block.categories) {
if (!byCategory[cat]) byCategory[cat] = []
byCategory[cat].push(block)
}
}
const lines: string[] = []
lines.push("# Creative Tim UI — Blocks & Components Library")
lines.push(
"> 390+ ready-to-use React/Next.js UI blocks built on shadcn/ui and Tailwind CSS."
)
lines.push(
"> Built by Creative Tim — 300+ open source UI products used by 2.8 million developers over the last decade."
)
lines.push(
"> Docs: https://www.creative-tim.com/ui/docs"
)
lines.push(
"> API key required for PRO blocks — get one at: https://www.creative-tim.com/ui/pricing"
)
lines.push("")
lines.push("## Installation")
lines.push("")
lines.push("### Option 1 — Creative Tim CLI (recommended)")
lines.push("```bash")
lines.push("# Add a block by name")
lines.push("npx @creative-tim/ui@latest add banner-01")
lines.push("")
lines.push("# Add multiple blocks")
lines.push("npx @creative-tim/ui@latest add banner-01 hero-01 navbar-with-search")
lines.push("```")
lines.push("")
lines.push("### Option 2 — shadcn CLI with full registry URL")
lines.push("```bash")
lines.push("# Add a block via the shadcn CLI")
lines.push(
"npx shadcn@latest add https://www.creative-tim.com/ui/r/banner-01.json"
)
lines.push("```")
lines.push("")
lines.push(
"Replace `banner-01` with any block name listed below."
)
lines.push("")
lines.push("## AI Agent Skill")
lines.push("")
lines.push(
"An agent skill is available for Claude Code, Cursor, Cline, and 40+ other AI coding agents."
)
lines.push(
"It encodes Creative Tim's design philosophy, all install commands, PRO API key setup, and design rules — so agents generate blocks consistently without being told each time."
)
lines.push("")
lines.push("```bash")
lines.push("npx skills add creativetimofficial/ui")
lines.push("```")
lines.push("")
lines.push(
"Skill source: https://github.com/creativetimofficial/ui/tree/main/skills/creative-tim-ui"
)
lines.push(
"Skill page: https://skills.sh/creativetimofficial/ui/creative-tim-ui"
)
lines.push("")
lines.push("## Collections")
lines.push("")
for (const collection of registryCollections) {
lines.push(
`- [${collection.name}](${BASE_URL}/blocks/${collection.slug}): ${collection.description}`
)
}
lines.push("")
lines.push("## Categories & Blocks")
lines.push("")
const visibleCategories = registryCategories.filter((c) => !c.hidden)
for (const category of visibleCategories) {
const categoryBlocks = byCategory[category.slug] ?? []
if (categoryBlocks.length === 0) continue
// Find which collection this category belongs to
const collection = registryCollections.find((col) =>
col.categories.includes(category.slug)
)
const browsePath = collection
? `${BASE_URL}/blocks/${collection.slug}/${category.slug}`
: `${BASE_URL}/blocks`
lines.push(`### ${category.name}`)
lines.push(`> ${category.description}`)
lines.push(`> Browse: ${browsePath}`)
lines.push("")
for (const block of categoryBlocks) {
lines.push(
`- [${block.name}](${BASE_URL}/r/${block.name}.json): ${block.description}`
)
}
lines.push("")
}
lines.push("## Full Registry")
lines.push("")
lines.push(
`Machine-readable registry JSON: ${BASE_URL}/r/registry.json`
)
const text = lines.join("\n")
return new Response(text, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
},
})
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+97
View File
@@ -0,0 +1,97 @@
import { ImageResponse } from "next/og"
async function loadAssets(): Promise<
{ name: string; data: Buffer; weight: 400 | 600; style: "normal" }[]
> {
const [
{ base64Font: normal },
{ base64Font: mono },
{ base64Font: semibold },
] = await Promise.all([
import("./geist-regular-otf.json").then((mod) => mod.default || mod),
import("./geistmono-regular-otf.json").then((mod) => mod.default || mod),
import("./geist-semibold-otf.json").then((mod) => mod.default || mod),
])
return [
{
name: "Geist",
data: Buffer.from(normal, "base64"),
weight: 400 as const,
style: "normal" as const,
},
{
name: "Geist Mono",
data: Buffer.from(mono, "base64"),
weight: 400 as const,
style: "normal" as const,
},
{
name: "Geist",
data: Buffer.from(semibold, "base64"),
weight: 600 as const,
style: "normal" as const,
},
]
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const title = searchParams.get("title")
const description = searchParams.get("description")
const [fonts] = await Promise.all([loadAssets()])
return new ImageResponse(
(
<div
tw="flex h-full w-full bg-black text-white"
style={{ fontFamily: "Geist Sans" }}
>
<div tw="flex border absolute border-stone-700 border-dashed inset-y-0 left-16 w-[1px]" />
<div tw="flex border absolute border-stone-700 border-dashed inset-y-0 right-16 w-[1px]" />
<div tw="flex border absolute border-stone-700 inset-x-0 h-[1px] top-16" />
<div tw="flex border absolute border-stone-700 inset-x-0 h-[1px] bottom-16" />
<div tw="flex absolute flex-row bottom-24 right-24 text-white">
<svg
viewBox="0 0 876 876"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={80}
height={80}
>
<path d="M468 292H528V584H468V292Z" fill="currentColor" />
<path d="M348 292H408V584H348V292Z" fill="currentColor" />
</svg>
</div>
<div tw="flex flex-col absolute w-[896px] justify-center inset-32">
<div
tw="tracking-tight flex-grow-1 flex flex-col justify-center leading-[1.1]"
style={{
textWrap: "balance",
fontWeight: 600,
fontSize: title && title.length > 20 ? 64 : 80,
letterSpacing: "-0.04em",
}}
>
{title}
</div>
<div
tw="text-[40px] leading-[1.5] flex-grow-1 text-stone-400"
style={{
fontWeight: 500,
textWrap: "balance",
}}
>
{description}
</div>
</div>
</div>
),
{
width: 1200,
height: 628,
fonts,
}
)
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+56
View File
@@ -0,0 +1,56 @@
"use client"
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react"
const DEFAULT_THEME = "default"
type ThemeContextType = {
activeTheme: string
setActiveTheme: (theme: string) => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ActiveThemeProvider({
children,
initialTheme,
}: {
children: ReactNode
initialTheme?: string
}) {
const [activeTheme, setActiveTheme] = useState<string>(
() => initialTheme || DEFAULT_THEME
)
useEffect(() => {
Array.from(document.body.classList)
.filter((className) => className.startsWith("theme-"))
.forEach((className) => {
document.body.classList.remove(className)
})
document.body.classList.add(`theme-${activeTheme}`)
if (activeTheme.endsWith("-scaled")) {
document.body.classList.add("theme-scaled")
}
}, [activeTheme])
return (
<ThemeContext.Provider value={{ activeTheme, setActiveTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useThemeConfig() {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error("useThemeConfig must be used within an ActiveThemeProvider")
}
return context
}
+80
View File
@@ -0,0 +1,80 @@
"use client"
import React, { useEffect, useState } from "react"
import Script from "next/script"
// import { Analytics as VercelAnalytics } from "@vercel/analytics/react"
export function Analytics() {
const gtmId = process.env.NEXT_PUBLIC_GTM_ID
// GA measurement ID: prefer env var, fallback to the ID you provided
const gaId = process.env.NEXT_PUBLIC_GA_AI_ID
// Mode: 'ga' | 'gtm' | null (not decided yet)
const [mode, setMode] = useState<null | "ga" | "gtm">(null)
useEffect(() => {
try {
const path = window.location.pathname || ""
const isR = path.includes("/r/")
const isUiView = path.startsWith("/ui/view")
if (isUiView) {
// Disable analytics for /ui/view routes
setMode(null)
return
}
if (isR) {
setMode("ga")
} else {
setMode("gtm")
}
} catch {
// noop
setMode("gtm")
}
}, [])
// Not ready yet
if (!mode) return null
return (
<>
{mode === "gtm" && gtmId && (
<>
<Script
id="gtm-script"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${gtmId}');`,
}}
/>
<noscript
dangerouslySetInnerHTML={{
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=${gtmId}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
}}
/>
</>
)}
{mode === "ga" && gaId && (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
strategy="afterInteractive"
data-ga
/>
<Script id="gtag-init" strategy="afterInteractive">
{`window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '${gaId}');`}
</Script>
</>
)}
{/* If you also want Vercel Analytics, uncomment the import above and
add <VercelAnalytics /> here. */}
</>
)
}
+14
View File
@@ -0,0 +1,14 @@
import Link from "next/link"
import { ArrowRightIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
export function Announcement() {
return (
<Badge asChild variant="secondary" className="rounded-full">
<Link href="/docs/changelog">
Insert Announcement here <ArrowRightIcon />
</Link>
</Badge>
)
}
+63
View File
@@ -0,0 +1,63 @@
import * as React from "react"
import { registryItemFileSchema } from "shadcn/schema"
import { z } from "zod"
import { highlightCode } from "@/lib/highlight-code"
import {
createFileTreeForRegistryItemFiles,
getRegistryItem,
} from "@/lib/registry"
import { cn } from "@/lib/utils"
import { BlockViewer } from "@/components/block-viewer"
import { ComponentPreview } from "@/components/component-preview"
export async function BlockDisplay({ name }: { name: string }) {
const item = await getCachedRegistryItem(name)
if (!item?.files) {
return null
}
const [tree, highlightedFiles] = await Promise.all([
getCachedFileTree(item.files),
getCachedHighlightedFiles(item.files),
])
return (
<BlockViewer item={item} tree={tree} highlightedFiles={highlightedFiles}>
<ComponentPreview
name={item.name}
hideCode
className={cn(
"my-0 **:[.preview]:h-auto **:[.preview]:p-4 **:[.preview>.p-6]:p-0",
item.meta?.containerClassName
)}
/>
</BlockViewer>
)
}
const getCachedRegistryItem = React.cache(async (name: string) => {
return await getRegistryItem(name)
})
const getCachedFileTree = React.cache(
async (files: Array<{ path: string; target?: string }>) => {
if (!files) {
return null
}
return await createFileTreeForRegistryItemFiles(files)
}
)
const getCachedHighlightedFiles = React.cache(
async (files: z.infer<typeof registryItemFileSchema>[]) => {
return await Promise.all(
files.map(async (file) => ({
...file,
highlightedContent: await highlightCode(file.content ?? ""),
}))
)
}
)
+36
View File
@@ -0,0 +1,36 @@
import Image from "next/image"
import { cn } from "@/lib/utils"
export function BlockImage({
name,
width = 1440,
height = 900,
className,
}: Omit<React.ComponentProps<typeof Image>, "src" | "alt"> & { name: string }) {
return (
<div
className={cn(
"relative aspect-[1440/900] w-full overflow-hidden rounded-lg",
className
)}
>
<Image
src={`/r/${name}-light.png`}
alt={name}
width={width}
height={height}
className="object-cover dark:hidden"
data-image="light"
/>
<Image
src={`/r/${name}-dark.png`}
alt={name}
width={width}
height={height}
className="hidden object-cover dark:block"
data-image="dark"
/>
</div>
)
}
+560
View File
@@ -0,0 +1,560 @@
"use client"
import * as React from "react"
import Image from "next/image"
import Link from "next/link"
import {
Check,
ChevronRight,
Clipboard,
File,
Folder,
Fullscreen,
Monitor,
RotateCw,
Smartphone,
Tablet,
Terminal,
} from "lucide-react"
import { ImperativePanelHandle } from "react-resizable-panels"
import { registryItemFileSchema, registryItemSchema } from "shadcn/schema"
import { z } from "zod"
import { trackEvent } from "@/lib/events"
import { createFileTreeForRegistryItemFiles, FileTree } from "@/lib/registry"
import { cn } from "@/lib/utils"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { getIconForLanguageExtension } from "@/components/icons"
import { OpenInV0Button } from "@/components/open-in-v0-button"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { Separator } from "@/components/ui/separator"
import {
Sidebar,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarProvider,
} from "@/components/ui/sidebar"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group"
type BlockViewerContext = {
item: z.infer<typeof registryItemSchema>
view: "code" | "preview"
setView: (view: "code" | "preview") => void
activeFile: string | null
setActiveFile: (file: string) => void
resizablePanelRef: React.RefObject<ImperativePanelHandle | null> | null
tree: ReturnType<typeof createFileTreeForRegistryItemFiles> | null
highlightedFiles:
| (z.infer<typeof registryItemFileSchema> & {
highlightedContent: string
})[]
| null
iframeKey?: number
setIframeKey?: React.Dispatch<React.SetStateAction<number>>
}
const BlockViewerContext = React.createContext<BlockViewerContext | null>(null)
function useBlockViewer() {
const context = React.useContext(BlockViewerContext)
if (!context) {
throw new Error("useBlockViewer must be used within a BlockViewerProvider.")
}
return context
}
function BlockViewerProvider({
item,
tree,
highlightedFiles,
children,
}: Pick<BlockViewerContext, "item" | "tree" | "highlightedFiles"> & {
children: React.ReactNode
}) {
const [view, setView] = React.useState<BlockViewerContext["view"]>("preview")
const [activeFile, setActiveFile] = React.useState<
BlockViewerContext["activeFile"]
>(highlightedFiles?.[0].target ?? null)
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
const [iframeKey, setIframeKey] = React.useState(0)
return (
<BlockViewerContext.Provider
value={{
item,
view,
setView,
resizablePanelRef,
activeFile,
setActiveFile,
tree,
highlightedFiles,
iframeKey,
setIframeKey,
}}
>
<div
id={item.name}
data-view={view}
className="group/block-view-wrapper flex min-w-0 scroll-mt-24 flex-col-reverse items-stretch gap-4 overflow-hidden md:flex-col"
style={
{
"--height": item.meta?.iframeHeight ?? "930px",
} as React.CSSProperties
}
>
{children}
</div>
</BlockViewerContext.Provider>
)
}
function BlockViewerToolbar() {
const { setView, view, item, resizablePanelRef, setIframeKey } =
useBlockViewer()
const { copyToClipboard, isCopied } = useCopyToClipboard()
// const viewPath = process.env.NEXT_PUBLIC_VIEW_PATH || "/view"
return (
<div className="hidden w-full flex-col gap-4 pl-2 md:pr-6 lg:flex">
{/* Title and Description Section */}
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<h2 className="text-2xl font-semibold tracking-tight">
<a
href={`#${item.name}`}
className="underline-offset-2 hover:underline"
>
{item.description?.replace(/\.$/, "")}
</a>
</h2>
</div>
{item.meta?.details && (
<p className="text-muted-foreground text-sm leading-relaxed max-w-2xl">
{item.meta.details}
</p>
)}
</div>
{/* Controllers Section */}
<div className="flex w-full items-center gap-2">
{/* {isPro && !hasAccess ? (
<Button
variant="outline"
className="w-fit cursor-not-allowed gap-1 px-2 opacity-50 shadow-none"
size="sm"
disabled
title="Upgrade to PRO to access this component"
>
<Terminal />
<span>
npx @creative-tim/ui@latest add {item.name}
</span>
</Button>
) : ( */}
<Button
variant="outline"
className="w-fit gap-1 px-2 shadow-none"
size="sm"
onClick={() => {
copyToClipboard(
`npx @creative-tim/ui@latest add ${item.name}`
)
}}
>
{isCopied ? <Check /> : <Terminal />}
<span>
npx @creative-tim/ui@latest add {item.name}
</span>
</Button>
{/* )} */}
<div className="ml-auto flex items-center gap-2">
<div className="h-8 items-center gap-1.5 rounded-md border p-1 shadow-none">
<ToggleGroup
type="single"
defaultValue="100"
onValueChange={(value) => {
setView("preview")
if (resizablePanelRef?.current) {
resizablePanelRef.current.resize(parseInt(value))
}
}}
className="gap-1 *:data-[slot=toggle-group-item]:!size-6 *:data-[slot=toggle-group-item]:!rounded-sm"
>
<ToggleGroupItem value="100" title="Desktop">
<Monitor />
</ToggleGroupItem>
<ToggleGroupItem value="60" title="Tablet">
<Tablet />
</ToggleGroupItem>
<ToggleGroupItem value="30" title="Mobile">
<Smartphone />
</ToggleGroupItem>
<Separator orientation="vertical" className="!h-4" />
<Button
size="icon"
variant="ghost"
className="size-6 rounded-sm p-0"
asChild
title="Open in New Tab"
>
<Link href={`/view/${item.name}`} target="_blank">
<span className="sr-only">Open in New Tab</span>
<Fullscreen />
</Link>
</Button>
<Separator orientation="vertical" className="!h-4" />
<Button
size="icon"
variant="ghost"
className="size-6 rounded-sm p-0"
title="Refresh Preview"
onClick={() => {
if (setIframeKey) {
setIframeKey((k) => k + 1)
}
}}
>
<RotateCw />
<span className="sr-only">Refresh Preview</span>
</Button>
</ToggleGroup>
</div>
<Separator orientation="vertical" className="mx-1 !h-4" />
<Tabs
value={view}
onValueChange={(value) => setView(value as "preview" | "code")}
>
<TabsList className="grid h-8 grid-cols-2 items-center rounded-md p-1 *:data-[slot=tabs-trigger]:h-6 *:data-[slot=tabs-trigger]:rounded-sm *:data-[slot=tabs-trigger]:px-2 *:data-[slot=tabs-trigger]:text-xs">
<TabsTrigger value="preview">Preview</TabsTrigger>
{/* <TabsTrigger value="code" disabled={isPro && !hasAccess}> */}
<TabsTrigger value="code">
Code
</TabsTrigger>
</TabsList>
</Tabs>
<Separator orientation="vertical" className="mx-1 !h-4" />
<OpenInV0Button name={item.name} />
</div>
</div>
</div>
)
}
function BlockViewerIframe({ className }: { className?: string }) {
const { item, iframeKey } = useBlockViewer()
const viewPath = process.env.NEXT_PUBLIC_VIEW_PATH || "/view"
return (
<iframe
key={iframeKey}
src={`${viewPath}/${item.name}`}
height={item.meta?.iframeHeight ?? 930}
loading="lazy"
className={cn(
"bg-background no-scrollbar relative z-20 w-full",
className
)}
/>
)
}
function BlockViewerView() {
const { resizablePanelRef } = useBlockViewer()
return (
<div className="hidden group-data-[view=code]/block-view-wrapper:hidden md:h-(--height) lg:flex">
<div className="relative grid w-full gap-4">
<div className="absolute inset-0 right-4 [background-image:radial-gradient(#d4d4d4_1px,transparent_1px)] [background-size:20px_20px] dark:[background-image:radial-gradient(#404040_1px,transparent_1px)]"></div>
<ResizablePanelGroup
direction="horizontal"
className="after:bg-surface/50 relative z-10 after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-xl"
>
<ResizablePanel
ref={resizablePanelRef}
className="bg-background relative aspect-[4/2.5] overflow-hidden rounded-lg border md:aspect-auto md:rounded-xl"
defaultSize={100}
minSize={30}
>
<BlockViewerIframe />
</ResizablePanel>
<ResizableHandle className="after:bg-border relative hidden w-3 bg-transparent p-0 after:absolute after:top-1/2 after:right-0 after:h-8 after:w-[6px] after:translate-x-[-1px] after:-translate-y-1/2 after:rounded-full after:transition-all after:hover:h-10 md:block" />
<ResizablePanel defaultSize={0} minSize={0} />
</ResizablePanelGroup>
</div>
</div>
)
}
function BlockViewerMobile({ children }: { children: React.ReactNode }) {
const { item } = useBlockViewer()
return (
<div className="flex flex-col gap-2 lg:hidden">
<div className="flex items-center gap-2 px-2">
<div className="line-clamp-1 text-sm font-medium">
{item.description}
</div>
<div className="text-muted-foreground ml-auto shrink-0 font-mono text-xs">
{item.name}
</div>
</div>
{item.meta?.mobile === "component" ? (
children
) : (
<div className="overflow-hidden rounded-xl border">
<Image
src={`/r/${item.name}-light.png`}
alt={item.name}
data-block={item.name}
width={1440}
height={900}
className="object-cover dark:hidden"
/>
<Image
src={`/r/${item.name}-dark.png`}
alt={item.name}
data-block={item.name}
width={1440}
height={900}
className="hidden object-cover dark:block"
/>
</div>
)}
</div>
)
}
function BlockViewerCode() {
const { activeFile, highlightedFiles } = useBlockViewer()
const file = React.useMemo(() => {
return highlightedFiles?.find((file) => file.target === activeFile)
}, [highlightedFiles, activeFile])
if (!file) {
return null
}
const language = file.path.split(".").pop() ?? "tsx"
// All components are accessible in open source version
if (false) {
return (
<div className="bg-code text-code-foreground mr-[14px] flex overflow-hidden rounded-xl border group-data-[view=preview]/block-view-wrapper:hidden md:h-(--height)">
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
<Badge
variant="default"
className="border-0 bg-gradient-to-r from-amber-500 to-orange-500 px-4 py-2 text-lg text-white"
>
PRO
</Badge>
<p className="text-muted-foreground max-w-md text-center">
This is a PRO component. Upgrade your account to access the source
code and install this component.
</p>
<Button variant="default" className="mt-2">
Upgrade to PRO
</Button>
</div>
</div>
)
}
return (
<div className="bg-code text-code-foreground mr-[14px] flex overflow-hidden rounded-xl border group-data-[view=preview]/block-view-wrapper:hidden md:h-(--height)">
<div className="w-72">
<BlockViewerFileTree />
</div>
<figure
data-rehype-pretty-code-figure=""
className="!mx-0 mt-0 flex min-w-0 flex-1 flex-col rounded-xl border-none"
>
<figcaption
className="text-code-foreground [&_svg]:text-code-foreground flex h-12 shrink-0 items-center gap-2 border-b px-4 py-2 [&_svg]:size-4 [&_svg]:opacity-70"
data-language={language}
>
{getIconForLanguageExtension(language)}
{file.target}
<div className="ml-auto flex items-center gap-2">
<BlockCopyCodeButton />
</div>
</figcaption>
<div
key={file?.path}
dangerouslySetInnerHTML={{ __html: file?.highlightedContent ?? "" }}
className="no-scrollbar overflow-y-auto"
/>
</figure>
</div>
)
}
export function BlockViewerFileTree() {
const { tree } = useBlockViewer()
if (!tree) {
return null
}
return (
<SidebarProvider className="flex !min-h-full flex-col border-r">
<Sidebar collapsible="none" className="w-full flex-1">
<SidebarGroupLabel className="h-12 rounded-none border-b px-4 text-sm">
Files
</SidebarGroupLabel>
<SidebarGroup className="p-0">
<SidebarGroupContent>
<SidebarMenu className="translate-x-0 gap-1.5">
{tree.map((file, index) => (
<Tree key={index} item={file} index={1} />
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</Sidebar>
</SidebarProvider>
)
}
function Tree({ item, index }: { item: FileTree; index: number }) {
const { activeFile, setActiveFile } = useBlockViewer()
if (!item.children) {
return (
<SidebarMenuItem>
<SidebarMenuButton
isActive={item.path === activeFile}
onClick={() => item.path && setActiveFile(item.path)}
className="hover:bg-muted-foreground/15 focus:bg-muted-foreground/15 focus-visible:bg-muted-foreground/15 active:bg-muted-foreground/15 data-[active=true]:bg-muted-foreground/15 rounded-none pl-(--index) whitespace-nowrap"
data-index={index}
style={
{
"--index": `${index * (index === 2 ? 1.2 : 1.3)}rem`,
} as React.CSSProperties
}
>
<ChevronRight className="invisible" />
<File className="h-4 w-4" />
{item.name}
</SidebarMenuButton>
</SidebarMenuItem>
)
}
return (
<SidebarMenuItem>
<Collapsible
className="group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90"
defaultOpen
>
<CollapsibleTrigger asChild>
<SidebarMenuButton
className="hover:bg-muted-foreground/15 focus:bg-muted-foreground/15 focus-visible:bg-muted-foreground/15 active:bg-muted-foreground/15 data-[active=true]:bg-muted-foreground/15 rounded-none pl-(--index) whitespace-nowrap"
style={
{
"--index": `${index * (index === 1 ? 1 : 1.2)}rem`,
} as React.CSSProperties
}
>
<ChevronRight className="transition-transform" />
<Folder />
{item.name}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub className="m-0 w-full translate-x-0 border-none p-0">
{item.children.map((subItem, key) => (
<Tree key={key} item={subItem} index={index + 1} />
))}
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>
</SidebarMenuItem>
)
}
function BlockCopyCodeButton() {
const { activeFile, item } = useBlockViewer()
const { copyToClipboard, isCopied } = useCopyToClipboard()
const file = React.useMemo(() => {
return item.files?.find((file) => file.target === activeFile)
}, [activeFile, item.files])
const content = file?.content
if (!content) {
return null
}
return (
<Button
variant="ghost"
size="icon"
className="size-7"
onClick={() => {
copyToClipboard(content)
trackEvent({
name: "copy_block_code",
properties: {
name: item.name,
file: file.path,
},
})
}}
>
{isCopied ? <Check /> : <Clipboard />}
</Button>
)
}
function BlockViewer({
item,
tree,
highlightedFiles,
children,
...props
}: Pick<BlockViewerContext, "item" | "tree" | "highlightedFiles"> & {
children: React.ReactNode
}) {
return (
<BlockViewerProvider
item={item}
tree={tree}
highlightedFiles={highlightedFiles}
{...props}
>
<BlockViewerToolbar />
<BlockViewerView />
<BlockViewerCode />
<BlockViewerMobile>{children}</BlockViewerMobile>
</BlockViewerProvider>
)
}
export { BlockViewer }
+58
View File
@@ -0,0 +1,58 @@
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
ScrollArea,
ScrollBar,
} from "@/components/ui/scroll-area"
import { registryCategories } from "@/registry/registry-categories"
export function BlocksNav() {
const pathname = usePathname()
return (
<div className="relative overflow-hidden">
<ScrollArea className="max-w-none">
<div className="flex items-center">
<BlocksNavLink
category={{ name: "Featured", slug: "", hidden: false }}
isActive={pathname === "/blocks"}
/>
{registryCategories.map((category) => (
<BlocksNavLink
key={category.slug}
category={category}
isActive={pathname === `/blocks/${category.slug}`}
/>
))}
</div>
<ScrollBar orientation="horizontal" className="invisible" />
</ScrollArea>
</div>
)
}
function BlocksNavLink({
category,
isActive,
}: {
category: (typeof registryCategories)[number]
isActive: boolean
}) {
if (category.hidden) {
return null
}
return (
<Link
href={`/blocks/${category.slug}`}
key={category.slug}
className="text-muted-foreground hover:text-primary data-[active=true]:text-primary flex h-7 items-center justify-center px-4 text-center text-base font-medium transition-colors"
data-active={isActive}
>
{category.name}
</Link>
)
}
+101
View File
@@ -0,0 +1,101 @@
"use client"
import React, { useState } from "react"
import { Switch } from "@/components/ui/switch"
interface BlurVignetteProps {
children: React.ReactNode
className?: string
radius?: string
inset?: string
transitionLength?: string
blur?: string
switchView?: boolean
}
const BlurVignette = ({
children,
switchView,
className = "",
radius = "24px",
inset = "16px",
transitionLength = "32px",
blur = "21px",
}: BlurVignetteProps) => {
const [isEnabled, setIsEnabled] = useState(true)
const shouldShowBlur = switchView ? isEnabled : true
const blurStyles: React.CSSProperties = {
position: "absolute",
inset: 0,
zIndex: 10,
pointerEvents: "none",
WebkitBackdropFilter: `blur(${blur})`,
backdropFilter: `blur(${blur})`,
opacity: shouldShowBlur ? 1 : 0,
transition: "opacity 0.3s ease",
["--radius" as string]: radius,
["--inset" as string]: inset,
["--transition-length" as string]: transitionLength,
["--blur" as string]: blur,
["--r" as string]: `max(${transitionLength}, calc(${radius} - ${inset}))`,
["--corner-size" as string]: `calc(var(--r) + ${inset}) calc(var(--r) + ${inset})`,
["--corner-gradient" as string]: `transparent 0px, transparent calc(var(--r) - ${transitionLength}), black var(--r)`,
["--fill-gradient" as string]: `black, black ${inset}, transparent calc(${inset} + ${transitionLength}), transparent calc(100% - ${transitionLength} - ${inset}), black calc(100% - ${inset})`,
["--fill-narrow-size" as string]: `calc(100% - (${inset} + var(--r)) * 2)`,
["--fill-farther-position" as string]: `calc(${inset} + var(--r))`,
WebkitMaskImage: `linear-gradient(to right, var(--fill-gradient)),
linear-gradient(to bottom, var(--fill-gradient)),
radial-gradient(at bottom right, var(--corner-gradient)),
radial-gradient(at bottom left, var(--corner-gradient)),
radial-gradient(at top left, var(--corner-gradient)),
radial-gradient(at top right, var(--corner-gradient))`,
maskImage: `linear-gradient(to right, var(--fill-gradient)),
linear-gradient(to bottom, var(--fill-gradient)),
radial-gradient(at bottom right, var(--corner-gradient)),
radial-gradient(at bottom left, var(--corner-gradient)),
radial-gradient(at top left, var(--corner-gradient)),
radial-gradient(at top right, var(--corner-gradient))`,
WebkitMaskSize: `100% var(--fill-narrow-size),
var(--fill-narrow-size) 100%,
var(--corner-size),
var(--corner-size),
var(--corner-size),
var(--corner-size)`,
maskSize: `100% var(--fill-narrow-size),
var(--fill-narrow-size) 100%,
var(--corner-size),
var(--corner-size),
var(--corner-size),
var(--corner-size)`,
WebkitMaskPosition: `0 var(--fill-farther-position),
var(--fill-farther-position) 0,
0 0,
100% 0,
100% 100%,
0 100%`,
maskPosition: `0 var(--fill-farther-position),
var(--fill-farther-position) 0,
0 0,
100% 0,
100% 100%,
0 100%`,
WebkitMaskRepeat: "no-repeat",
maskRepeat: "no-repeat",
} as React.CSSProperties
return (
<div className={`relative overflow-hidden ${className}`}>
<div style={blurStyles} />
{children}
{switchView && (
<div className="absolute top-4 right-4 z-20 flex items-center gap-2">
<Switch checked={isEnabled} onCheckedChange={setIsEnabled} />
</div>
)}
</div>
)
}
export default BlurVignette
+30
View File
@@ -0,0 +1,30 @@
import { cn } from "@/lib/utils"
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert"
export function Callout({
title,
children,
icon,
className,
...props
}: React.ComponentProps<typeof Alert> & { icon?: React.ReactNode }) {
return (
<Alert
className={cn(
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-1",
className
)}
{...props}
>
{icon}
{title && <AlertTitle>{title}</AlertTitle>}
<AlertDescription className="text-card-foreground/80">
{children}
</AlertDescription>
</Alert>
)
}
+132
View File
@@ -0,0 +1,132 @@
"use client"
import * as React from "react"
import { MinusIcon, PlusIcon } from "lucide-react"
import { Bar, BarChart } from "recharts"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
ChartConfig,
ChartContainer,
} from "@/components/ui/chart"
const data = [
{
goal: 400,
},
{
goal: 300,
},
{
goal: 200,
},
{
goal: 300,
},
{
goal: 200,
},
{
goal: 278,
},
{
goal: 189,
},
{
goal: 239,
},
{
goal: 300,
},
{
goal: 200,
},
{
goal: 278,
},
{
goal: 189,
},
{
goal: 349,
},
]
const chartConfig = {
goal: {
label: "Goal",
color: "var(--primary)",
},
} satisfies ChartConfig
export function CardsActivityGoal() {
const [goal, setGoal] = React.useState(350)
function onClick(adjustment: number) {
setGoal(Math.max(200, Math.min(400, goal + adjustment)))
}
return (
<Card className="h-full gap-5">
<CardHeader>
<CardTitle>Move Goal</CardTitle>
<CardDescription>Set your daily activity goal.</CardDescription>
</CardHeader>
<CardContent className="flex flex-1 flex-col">
<div className="flex items-center justify-center gap-4">
<Button
variant="outline"
size="icon"
className="size-7 rounded-full"
onClick={() => onClick(-10)}
disabled={goal <= 200}
>
<MinusIcon />
<span className="sr-only">Decrease</span>
</Button>
<div className="text-center">
<div className="text-4xl font-bold tracking-tighter tabular-nums">
{goal}
</div>
<div className="text-muted-foreground text-xs uppercase">
Calories/day
</div>
</div>
<Button
variant="outline"
size="icon"
className="size-7 rounded-full"
onClick={() => onClick(10)}
disabled={goal >= 400}
>
<PlusIcon />
<span className="sr-only">Increase</span>
</Button>
</div>
<div className="flex-1">
<ChartContainer
config={chartConfig}
className="aspect-auto h-full w-full"
>
<BarChart data={data}>
<Bar dataKey="goal" radius={4} fill="var(--color-goal)" />
</BarChart>
</ChartContainer>
</div>
</CardContent>
<CardFooter>
<Button className="w-full" variant="secondary">
Set Goal
</Button>
</CardFooter>
</Card>
)
}
+26
View File
@@ -0,0 +1,26 @@
"use client"
import { addDays } from "date-fns"
import { Calendar } from "@/components/ui/calendar"
import { Card, CardContent } from "@/components/ui/card"
const start = new Date(2025, 5, 5)
export function CardsCalendar() {
return (
<Card className="hidden max-w-[260px] p-0 sm:flex">
<CardContent className="p-0">
<Calendar
numberOfMonths={1}
mode="range"
defaultMonth={start}
selected={{
from: start,
to: addDays(start, 8),
}}
/>
</CardContent>
</Card>
)
}
@@ -0,0 +1,51 @@
"use client"
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 CardsCookieSettings() {
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 gap-4">
<Label htmlFor="necessary" className="flex flex-col items-start">
<span>Strictly Necessary</span>
<span className="text-muted-foreground leading-snug font-normal">
These cookies are essential in order to use the website and use
its features.
</span>
</Label>
<Switch id="necessary" defaultChecked aria-label="Necessary" />
</div>
<div className="flex items-center justify-between gap-4">
<Label htmlFor="functional" className="flex flex-col items-start">
<span>Functional Cookies</span>
<span className="text-muted-foreground leading-snug font-normal">
These cookies allow the website to provide personalized
functionality.
</span>
</Label>
<Switch id="functional" aria-label="Functional" />
</div>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full">
Save preferences
</Button>
</CardFooter>
</Card>
)
}
@@ -0,0 +1,73 @@
"use client"
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 CardsCreateAccount() {
return (
<Card>
<CardHeader>
<CardTitle className="text-2xl">Create an account</CardTitle>
<CardDescription>
Enter your email below to create your account
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="grid grid-cols-2 gap-6">
<Button variant="outline">
<svg viewBox="0 0 438.549 438.549">
<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>
GitHub
</Button>
<Button variant="outline">
<svg role="img" viewBox="0 0 24 24">
<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>
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-card text-muted-foreground px-2">
Or continue with
</span>
</div>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="email-create-account">Email</Label>
<Input
id="email-create-account"
type="email"
placeholder="m@example.com"
/>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="password-create-account">Password</Label>
<Input id="password-create-account" type="password" />
</div>
</CardContent>
<CardFooter>
<Button className="w-full">Create account</Button>
</CardFooter>
</Card>
)
}
+127
View File
@@ -0,0 +1,127 @@
"use client"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
RadioGroup,
RadioGroupItem,
} from "@/components/ui/radio-group"
import { Textarea } from "@/components/ui/textarea"
const plans = [
{
id: "starter",
name: "Starter Plan",
description: "Perfect for small businesses.",
price: "$10",
},
{
id: "pro",
name: "Pro Plan",
description: "More features and storage.",
price: "$20",
},
] as const
export function CardsForms() {
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Upgrade your subscription</CardTitle>
<CardDescription className="text-balance">
You are currently on the free plan. Upgrade to the pro plan to get
access to all features.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3 md:flex-row">
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Evil Rabbit" />
</div>
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="email">Email</Label>
<Input id="email" placeholder="example@acme.com" />
</div>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-number">Card Number</Label>
<div className="grid grid-cols-2 gap-3 md:grid-cols-[1fr_80px_60px]">
<Input
id="card-number"
placeholder="1234 1234 1234 1234"
className="col-span-2 md:col-span-1"
/>
<Input id="card-number-expiry" placeholder="MM/YY" />
<Input id="card-number-cvc" placeholder="CVC" />
</div>
</div>
<fieldset className="flex flex-col gap-3">
<legend className="text-sm font-medium">Plan</legend>
<p className="text-muted-foreground text-sm">
Select the plan that best fits your needs.
</p>
<RadioGroup
defaultValue="starter"
className="grid gap-3 md:grid-cols-2"
>
{plans.map((plan) => (
<Label
className="has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-input/20 flex items-start gap-3 rounded-lg border p-3"
key={plan.id}
>
<RadioGroupItem
value={plan.id}
id={plan.name}
className="data-[state=checked]:border-primary"
/>
<div className="grid gap-1 font-normal">
<div className="font-medium">{plan.name}</div>
<div className="text-muted-foreground text-xs leading-snug text-balance">
{plan.description}
</div>
</div>
</Label>
))}
</RadioGroup>
</fieldset>
<div className="flex flex-col gap-2">
<Label htmlFor="notes">Notes</Label>
<Textarea id="notes" placeholder="Enter notes" />
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Checkbox id="terms" />
<Label htmlFor="terms" className="font-normal">
I agree to the terms and conditions
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="newsletter" defaultChecked />
<Label htmlFor="newsletter" className="font-normal">
Allow us to send you emails
</Label>
</div>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" size="sm">
Cancel
</Button>
<Button size="sm">Upgrade Plan</Button>
</CardFooter>
</Card>
)
}
+15
View File
@@ -0,0 +1,15 @@
import { CardsActivityGoal } from "@/components/cards/activity-goal"
import SoftwarePurchase01 from "@/registry/creative-tim/blocks/software-purchase-01/page"
export function CardsDemo() {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="flex flex-col gap-4">
<CardsActivityGoal />
</div>
<div className="flex flex-col gap-4 md:col-span-2 lg:col-span-2">
<SoftwarePurchase01 />
</div>
</div>
)
}
@@ -0,0 +1,134 @@
"use client"
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"
const plans = [
{
id: "starter",
name: "Starter Plan",
description: "Perfect for small businesses.",
price: "$10",
},
{
id: "pro",
name: "Pro Plan",
description: "Advanced features with more storage.",
price: "$20",
},
] as const
export function CardsPaymentMethod() {
return (
<Card>
<CardHeader>
<CardTitle>Payment Method</CardTitle>
<CardDescription>
Add a new payment method to your account.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
<div className="flex flex-col gap-3">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="First Last" />
</div>
<fieldset className="flex flex-col gap-3">
<legend className="text-sm font-medium">Plan</legend>
<p className="text-muted-foreground text-sm">
Select the plan that best fits your needs.
</p>
<RadioGroup defaultValue="starter" className="grid gap-3">
{plans.map((plan) => (
<Label
className="has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-primary/5 flex items-start gap-3 rounded-lg border p-3"
key={plan.id}
>
<RadioGroupItem
value={plan.id}
id={plan.name}
className="data-[state=checked]:border-primary"
/>
<div className="grid gap-1 font-normal">
<div className="font-medium">{plan.name}</div>
<div className="text-muted-foreground pr-2 text-xs leading-snug text-balance">
{plan.description}
</div>
</div>
</Label>
))}
</RadioGroup>
</fieldset>
<div className="flex flex-col gap-3">
<Label htmlFor="number">Card number</Label>
<Input id="number" placeholder="" />
</div>
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="month">Expires</Label>
<Select>
<SelectTrigger id="month" aria-label="Month" className="w-full">
<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="flex flex-col gap-3">
<Label htmlFor="year">Year</Label>
<Select>
<SelectTrigger id="year" aria-label="Year" className="w-full">
<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="flex flex-col gap-3">
<Label htmlFor="cvc">CVC</Label>
<Input id="cvc" placeholder="CVC" />
</div>
</div>
</CardContent>
<CardFooter>
<Button className="w-full">Continue</Button>
</CardFooter>
</Card>
)
}
+297
View File
@@ -0,0 +1,297 @@
"use client"
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { MoreHorizontalIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const data: Payment[] = [
{
id: "m5gr84i9",
amount: 316,
status: "success",
email: "ken99@example.com",
},
{
id: "3u1reuv4",
amount: 242,
status: "success",
email: "Abe45@example.com",
},
{
id: "derv1ws0",
amount: 837,
status: "processing",
email: "Monserrat44@example.com",
},
{
id: "bhqecj4p",
amount: 721,
status: "failed",
email: "carmella@example.com",
},
{
id: "k9f2m3n4",
amount: 450,
status: "pending",
email: "jason78@example.com",
},
{
id: "p5q6r7s8",
amount: 1280,
status: "success",
email: "sarah23@example.com",
},
]
export type Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}
export const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<div className="capitalize">{row.getValue("status")}</div>
),
},
{
accessorKey: "email",
header: "Email",
cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>,
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"))
// Format the amount as a dollar amount
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount)
return <div className="text-right font-medium">{formatted}</div>
},
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="size-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(payment.id)}
>
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
]
export function CardsPayments() {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const [rowSelection, setRowSelection] = React.useState({})
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
})
return (
<Card>
<CardHeader>
<CardTitle className="text-xl">Payments</CardTitle>
<CardDescription>Manage your payments.</CardDescription>
<CardAction>
<Button variant="secondary" size="sm" className="shadow-none">
Add Payment
</Button>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
className="data-[name=actions]:w-10 data-[name=amount]:w-24 data-[name=select]:w-10 data-[name=status]:w-24 [&:has([role=checkbox])]:pl-3"
data-name={header.id}
>
{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}
className="data-[name=actions]:w-10 data-[name=amount]:w-24 data-[name=select]:w-10 data-[name=status]:w-24 [&:has([role=checkbox])]:pl-3"
data-name={cell.column.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>
<div className="flex items-center justify-end gap-2">
<div className="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,97 @@
"use client"
import * as React from "react"
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 CardsReportIssue() {
const id = React.useId()
return (
<Card>
<CardHeader>
<CardTitle>Report an issue</CardTitle>
<CardDescription>
What area are you having problems with?
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
<div className="grid gap-4 sm:grid-cols-2">
<div className="flex flex-col gap-3">
<Label htmlFor={`area-${id}`}>Area</Label>
<Select defaultValue="billing">
<SelectTrigger
id={`area-${id}`}
aria-label="Area"
className="w-full"
>
<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="flex flex-col gap-3">
<Label htmlFor={`security-level-${id}`}>Security Level</Label>
<Select defaultValue="2">
<SelectTrigger
id={`security-level-${id}`}
className="w-full [&_span]:!block [&_span]:truncate"
aria-label="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="flex flex-col gap-3">
<Label htmlFor={`subject-${id}`}>Subject</Label>
<Input id={`subject-${id}`} placeholder="I need help with..." />
</div>
<div className="flex flex-col gap-3">
<Label htmlFor={`description-${id}`}>Description</Label>
<Textarea
id={`description-${id}`}
placeholder="Please include all information relevant to your issue."
className="min-h-28"
/>
</div>
</CardContent>
<CardFooter className="justify-end gap-2">
<Button variant="ghost" size="sm">
Cancel
</Button>
<Button size="sm">Submit</Button>
</CardFooter>
</Card>
)
}
+116
View File
@@ -0,0 +1,116 @@
"use client"
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 { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
const people = [
{
name: "Olivia Martin",
email: "m@example.com",
avatar: "/avatars/03.png",
},
{
name: "Isabella Nguyen",
email: "b@example.com",
avatar: "/avatars/04.png",
},
{
name: "Sofia Davis",
email: "p@example.com",
avatar: "/avatars/05.png",
},
{
name: "Ethan Thompson",
email: "e@example.com",
avatar: "/avatars/01.png",
},
]
export function CardsShare() {
return (
<Card>
<CardHeader>
<CardTitle>Share this document</CardTitle>
<CardDescription>
Anyone with the link can view this document.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<Label htmlFor="link" className="sr-only">
Link
</Label>
<Input
id="link"
value="http://example.com/link/to/document"
className="h-8"
readOnly
/>
<Button size="sm" variant="outline" className="shadow-none">
Copy Link
</Button>
</div>
<Separator className="my-4" />
<div className="flex flex-col gap-4">
<div className="text-sm font-medium">People with access</div>
<div className="grid gap-6">
{people.map((person) => (
<div
key={person.email}
className="flex items-center justify-between gap-4"
>
<div className="flex items-center gap-4">
<Avatar>
<AvatarImage src={person.avatar} alt="Image" />
<AvatarFallback>{person.name.charAt(0)}</AvatarFallback>
</Avatar>
<div>
<p className="text-sm leading-none font-medium">
{person.name}
</p>
<p className="text-muted-foreground text-sm">
{person.email}
</p>
</div>
</div>
<Select defaultValue="edit">
<SelectTrigger
className="ml-auto pr-2"
aria-label="Edit"
size="sm"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="edit">Can edit</SelectItem>
<SelectItem value="view">Can view</SelectItem>
</SelectContent>
</Select>
</div>
))}
</div>
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,99 @@
"use client"
import { Calendar, CreditCard, DollarSign, Users } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export interface SoftwarePurchaseCardProps {
softwareName?: string
startDate?: string
seats?: number
pricingType?: "per-seat" | "flat-rate" | "usage-based"
price?: string
onApprove?: () => void
onDiscard?: () => void
}
export function SoftwarePurchaseCard({
softwareName = "Enterprise Cloud Suite",
startDate = "2025-01-15",
seats = 50,
pricingType = "per-seat",
price = "$2,500",
onApprove,
onDiscard,
}: SoftwarePurchaseCardProps) {
const pricingTypeLabel = {
"per-seat": "Per Seat",
"flat-rate": "Flat Rate",
"usage-based": "Usage Based",
}[pricingType]
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="flex items-center justify-between">
{softwareName}
<Badge variant="secondary">{pricingTypeLabel}</Badge>
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex items-center gap-3 text-sm">
<Calendar className="text-muted-foreground size-4" />
<div className="flex flex-col gap-0.5">
<span className="text-muted-foreground text-xs">Start Date</span>
<span className="font-medium">
{new Date(startDate).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})}
</span>
</div>
</div>
<div className="flex items-center gap-3 text-sm">
<Users className="text-muted-foreground size-4" />
<div className="flex flex-col gap-0.5">
<span className="text-muted-foreground text-xs">Seats</span>
<span className="font-medium">{seats} users</span>
</div>
</div>
<div className="flex items-center gap-3 text-sm">
<CreditCard className="text-muted-foreground size-4" />
<div className="flex flex-col gap-0.5">
<span className="text-muted-foreground text-xs">Pricing Type</span>
<span className="font-medium">{pricingTypeLabel}</span>
</div>
</div>
<div className="flex items-center gap-3 text-sm">
<DollarSign className="text-muted-foreground size-4" />
<div className="flex flex-col gap-0.5">
<span className="text-muted-foreground text-xs">
{pricingType === "per-seat" ? "Monthly Cost" : "Price"}
</span>
<span className="text-lg font-semibold">{price}</span>
</div>
</div>
</CardContent>
<CardFooter className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={onDiscard}>
Discard
</Button>
<Button className="flex-1" onClick={onApprove}>
Approve
</Button>
</CardFooter>
</Card>
)
}
+120
View File
@@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { useMediaQuery } from "@/hooks/use-media-query"
import { ChartCopyButton } from "@/components/chart-copy-button"
import { Chart } from "@/components/chart-display"
import { getIconForLanguageExtension } from "@/components/icons"
import { OpenInV0Button } from "@/components/open-in-v0-button"
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
export function ChartCodeViewer({
chart,
className,
children,
}: {
chart: Chart
} & React.ComponentProps<"div">) {
const isDesktop = useMediaQuery("(min-width: 768px)")
const button = (
<Button
size="sm"
variant="outline"
className="text-foreground hover:bg-muted dark:text-foreground h-6 rounded-[6px] border bg-transparent px-2 text-xs shadow-none"
>
View Code
</Button>
)
const content = (
<div className="flex min-h-0 flex-1 flex-col gap-0">
<div className="chart-wrapper theme-container hidden sm:block [&_[data-chart]]:mx-auto [&_[data-chart]]:max-h-[35vh] [&>div]:rounded-none [&>div]:border-0 [&>div]:border-b [&>div]:shadow-none">
{children}
</div>
<div className="flex min-w-0 flex-1 flex-col overflow-hidden p-4">
<figure
data-rehype-pretty-code-figure=""
className="mt-0 flex h-auto min-w-0 flex-1 flex-col overflow-hidden"
>
<figcaption
className="text-foreground [&>svg]:text-foreground flex h-12 shrink-0 items-center gap-2 border-b py-2 pr-2 pl-4 [&>svg]:size-4 [&>svg]:opacity-70"
data-language="tsx"
>
{getIconForLanguageExtension("tsx")}
{chart.name}
<div className="ml-auto flex items-center gap-2">
<ChartCopyButton
event="copy_chart_code"
name={chart.name}
code={chart.files?.[0]?.content ?? ""}
/>
<OpenInV0Button name={chart.name} className="rounded-sm" />
</div>
</figcaption>
<div
dangerouslySetInnerHTML={{
__html: chart.highlightedCode,
}}
className="no-scrollbar overflow-y-auto"
/>
</figure>
</div>
</div>
)
if (!isDesktop) {
return (
<Drawer>
<DrawerTrigger asChild>{button}</DrawerTrigger>
<DrawerContent
className={cn(
"flex max-h-[80vh] flex-col sm:max-h-[90vh] [&>div.bg-muted]:shrink-0",
className
)}
>
<DrawerHeader className="sr-only">
<DrawerTitle>Code</DrawerTitle>
<DrawerDescription>View the code for the chart.</DrawerDescription>
</DrawerHeader>
<div className="flex h-full flex-col overflow-auto">{content}</div>
</DrawerContent>
</Drawer>
)
}
return (
<Sheet>
<SheetTrigger asChild>{button}</SheetTrigger>
<SheetContent
side="right"
className={cn(
"flex flex-col gap-0 border-l-0 p-0 sm:max-w-sm md:w-[700px] md:max-w-[700px] dark:border-l",
className
)}
>
<SheetHeader className="sr-only">
<SheetTitle>Code</SheetTitle>
<SheetDescription>View the code for the chart.</SheetDescription>
</SheetHeader>
{content}
</SheetContent>
</Sheet>
)
}
+63
View File
@@ -0,0 +1,63 @@
"use client"
import * as React from "react"
import { CheckIcon, ClipboardIcon } from "lucide-react"
import { Event, trackEvent } from "@/lib/events"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
export function ChartCopyButton({
event,
name,
code,
className,
...props
}: {
event: Event["name"]
name: string
code: string
} & React.ComponentProps<typeof Button>) {
const [hasCopied, setHasCopied] = React.useState(false)
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false)
}, 2000)
}, [hasCopied])
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className={cn(
"[&_svg]-h-3.5 h-7 w-7 rounded-[6px] [&_svg]:w-3.5",
className
)}
onClick={() => {
navigator.clipboard.writeText(code)
trackEvent({
name: event,
properties: {
name,
},
})
setHasCopied(true)
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
</Button>
</TooltipTrigger>
<TooltipContent className="bg-black text-white">Copy code</TooltipContent>
</Tooltip>
)
}
+54
View File
@@ -0,0 +1,54 @@
import * as React from "react"
import { registryItemSchema } from "shadcn/schema"
import { z } from "zod"
import { highlightCode } from "@/lib/highlight-code"
import { getRegistryItem } from "@/lib/registry"
import { cn } from "@/lib/utils"
import { ChartToolbar } from "@/components/chart-toolbar"
export type Chart = z.infer<typeof registryItemSchema> & {
highlightedCode: string
}
export async function ChartDisplay({
name,
children,
className,
}: { name: string } & React.ComponentProps<"div">) {
const chart = await getCachedRegistryItem(name)
const highlightedCode = await getChartHighlightedCode(
chart?.files?.[0]?.content ?? ""
)
if (!chart || !highlightedCode) {
return null
}
return (
<div
className={cn(
"themes-wrapper group relative flex flex-col overflow-hidden rounded-xl border transition-all duration-200 ease-in-out hover:z-30",
className
)}
>
<ChartToolbar
chart={{ ...chart, highlightedCode }}
className="bg-card text-card-foreground relative z-20 flex justify-end border-b px-3 py-2.5"
>
{children}
</ChartToolbar>
<div className="relative z-10 [&>div]:rounded-none [&>div]:border-none [&>div]:shadow-none">
{children}
</div>
</div>
)
}
const getCachedRegistryItem = React.cache(async (name: string) => {
return await getRegistryItem(name)
})
const getChartHighlightedCode = React.cache(async (content: string) => {
return await highlightCode(content)
})
+107
View File
@@ -0,0 +1,107 @@
"use client"
import {
AreaChartIcon,
BarChartBigIcon,
HexagonIcon,
LineChartIcon,
MousePointer2Icon,
PieChartIcon,
RadarIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { ChartCodeViewer } from "@/components/chart-code-viewer"
import { ChartCopyButton } from "@/components/chart-copy-button"
import { Chart } from "@/components/chart-display"
import { Separator } from "@/components/ui/separator"
export function ChartToolbar({
chart,
className,
children,
}: {
chart: Chart
} & React.ComponentProps<"div">) {
return (
<div className={cn("flex items-center gap-2", className)}>
<div className="text-muted-foreground flex items-center gap-1.5 pl-1 text-[13px] [&>svg]:h-[0.9rem] [&>svg]:w-[0.9rem]">
<ChartTitle chart={chart} />
</div>
<div className="ml-auto flex items-center gap-2 [&>form]:flex">
<ChartCopyButton
event="copy_chart_code"
name={chart.name}
code={chart.files?.[0]?.content ?? ""}
className="[&_svg]-h-3 text-foreground hover:bg-muted dark:text-foreground h-6 w-6 rounded-[6px] bg-transparent shadow-none [&_svg]:w-3"
/>
<Separator
orientation="vertical"
className="mx-0 hidden !h-4 md:flex"
/>
<ChartCodeViewer chart={chart}>{children}</ChartCodeViewer>
</div>
</div>
)
}
function ChartTitle({ chart }: { chart: Chart }) {
if (chart.name.includes("charts-line")) {
return (
<>
<LineChartIcon /> Line Chart
</>
)
}
if (chart.name.includes("chart-bar")) {
return (
<>
<BarChartBigIcon /> Bar Chart
</>
)
}
if (chart.name.includes("chart-pie")) {
return (
<>
<PieChartIcon /> Pie Chart
</>
)
}
if (chart.name.includes("chart-area")) {
return (
<>
<AreaChartIcon /> Area Chart
</>
)
}
if (chart.name.includes("chart-radar")) {
return (
<>
<HexagonIcon /> Radar Chart
</>
)
}
if (chart.name.includes("chart-radial")) {
return (
<>
<RadarIcon /> Radial Chart
</>
)
}
if (chart.name.includes("chart-tooltip")) {
return (
<>
<MousePointer2Icon />
Tooltip
</>
)
}
return chart.name
}
+70
View File
@@ -0,0 +1,70 @@
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
import {
ScrollArea,
ScrollBar,
} from "@/components/ui/scroll-area"
const links = [
{
name: "Area Charts",
href: "/charts/area#charts",
},
{
name: "Bar Charts",
href: "/charts/bar#charts",
},
{
name: "Line Charts",
href: "/charts/line#charts",
},
{
name: "Pie Charts",
href: "/charts/pie#charts",
},
{
name: "Radar Charts",
href: "/charts/radar#charts",
},
{
name: "Radial Charts",
href: "/charts/radial#charts",
},
{
name: "Tooltips",
href: "/charts/tooltip#charts",
},
]
export function ChartsNav({
className,
...props
}: React.ComponentProps<"div">) {
const pathname = usePathname()
return (
<div className="relative overflow-hidden">
<ScrollArea className="max-w-[600px] lg:max-w-none">
<div className={cn("flex items-center", className)} {...props}>
{links.map((link) => (
<Link
href={link.href}
key={link.href}
data-active={link.href.startsWith(pathname)}
className={cn(
"text-muted-foreground hover:text-primary data-[active=true]:text-primary flex h-7 shrink-0 items-center justify-center px-4 text-center text-base font-medium transition-colors"
)}
>
{link.name}
</Link>
))}
</div>
<ScrollBar orientation="horizontal" className="invisible" />
</ScrollArea>
</div>
)
}
+135
View File
@@ -0,0 +1,135 @@
"use client"
import * as React from "react"
import { CheckIcon, ClipboardIcon, TerminalIcon } from "lucide-react"
import { useConfig } from "@/hooks/use-config"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/components/ui/button"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
export function CodeBlockCommand({
__npm__,
__yarn__,
__pnpm__,
__bun__,
}: React.ComponentProps<"pre"> & {
__npm__?: string
__yarn__?: string
__pnpm__?: string
__bun__?: string
}) {
const [config, setConfig] = useConfig()
const [hasCopied, setHasCopied] = React.useState(false)
React.useEffect(() => {
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
const packageManager = config.packageManager || "pnpm"
const tabs = React.useMemo(() => {
return {
pnpm: __pnpm__,
npm: __npm__,
yarn: __yarn__,
bun: __bun__,
}
}, [__npm__, __pnpm__, __yarn__, __bun__])
const copyCommand = React.useCallback(() => {
const command = tabs[packageManager]
if (!command) {
return
}
copyToClipboardWithMeta(command, {
name: "copy_npm_command",
properties: {
command,
pm: packageManager,
},
})
setHasCopied(true)
}, [packageManager, tabs])
return (
<div className="overflow-x-auto">
<Tabs
value={packageManager}
className="gap-0"
onValueChange={(value) => {
setConfig({
...config,
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
})
}}
>
<div className="border-border/50 flex items-center gap-2 border-b px-3 py-1">
<div className="bg-foreground flex size-4 items-center justify-center rounded-[1px] opacity-70">
<TerminalIcon className="text-code size-3" />
</div>
<TabsList className="rounded-none bg-transparent p-0">
{Object.entries(tabs).map(([key]) => {
return (
<TabsTrigger
key={key}
value={key}
className="data-[state=active]:bg-accent data-[state=active]:border-input h-7 border border-transparent pt-0.5 data-[state=active]:shadow-none"
>
{key}
</TabsTrigger>
)
})}
</TabsList>
</div>
<div className="no-scrollbar overflow-x-auto">
{Object.entries(tabs).map(([key, value]) => {
return (
<TabsContent key={key} value={key} className="mt-0 px-4 py-3.5">
<pre>
<code
className="relative font-mono text-sm leading-none"
data-language="bash"
>
{value}
</code>
</pre>
</TabsContent>
)
})}
</div>
</Tabs>
<Tooltip>
<TooltipTrigger asChild>
<Button
data-slot="copy-button"
size="icon"
variant="ghost"
className="absolute top-2 right-2 z-10 size-7 opacity-70 hover:opacity-100 focus-visible:opacity-100"
onClick={copyCommand}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied" : "Copy to Clipboard"}
</TooltipContent>
</Tooltip>
</div>
)
}
@@ -0,0 +1,51 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { Separator } from "@/components/ui/separator"
export function CodeCollapsibleWrapper({
className,
children,
...props
}: React.ComponentProps<typeof Collapsible>) {
const [isOpened, setIsOpened] = React.useState(false)
return (
<Collapsible
open={isOpened}
onOpenChange={setIsOpened}
className={cn("group/collapsible relative md:-mx-1", className)}
{...props}
>
<CollapsibleTrigger asChild>
<div className="absolute top-1.5 right-9 z-10 flex items-center">
<Button
variant="ghost"
size="sm"
className="text-muted-foreground h-7 rounded-md px-2"
>
{isOpened ? "Collapse" : "Expand"}
</Button>
<Separator orientation="vertical" className="mx-1.5 !h-4" />
</div>
</CollapsibleTrigger>
<CollapsibleContent
forceMount
className="relative mt-6 overflow-hidden data-[state=closed]:max-h-64 [&>figure]:mt-0 [&>figure]:md:!mx-0"
>
{children}
</CollapsibleContent>
<CollapsibleTrigger className="from-code/70 to-code text-muted-foreground absolute inset-x-0 -bottom-2 flex h-20 items-center justify-center rounded-b-lg bg-gradient-to-b text-sm group-data-[state=open]/collapsible:hidden">
{isOpened ? "Collapse" : "Expand"}
</CollapsibleTrigger>
</Collapsible>
)
}
+26
View File
@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import { useConfig } from "@/hooks/use-config"
import { Tabs } from "@/components/ui/tabs"
export function CodeTabs({ children }: React.ComponentProps<typeof Tabs>) {
const [config, setConfig] = useConfig()
const installationType = React.useMemo(() => {
return config.installationType || "cli"
}, [config])
return (
<Tabs
value={installationType}
onValueChange={(value) =>
setConfig({ ...config, installationType: value as "cli" | "manual" })
}
className="relative mt-6 w-full"
>
{children}
</Tabs>
)
}
+341
View File
@@ -0,0 +1,341 @@
"use client"
import * as React from "react"
import { useRouter } from "next/navigation"
import { type DialogProps } from "@radix-ui/react-dialog"
import { IconArrowRight } from "@tabler/icons-react"
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
import { source } from "@/lib/source"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
import { useIsMac } from "@/hooks/use-is-mac"
import { useMutationObserver } from "@/hooks/use-mutation-observer"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Separator } from "@/components/ui/separator"
export function CommandMenu({
tree,
blocks,
navItems,
...props
}: DialogProps & {
tree: typeof source.pageTree
blocks?: { name: string; description: string; categories: string[] }[]
navItems?: { href: string; label: string }[]
}) {
const router = useRouter()
const isMac = useIsMac()
const [config] = useConfig()
const [open, setOpen] = React.useState(false)
const [selectedType, setSelectedType] = React.useState<
"page" | "component" | "block" | null
>(null)
const [copyPayload, setCopyPayload] = React.useState("")
const packageManager = config.packageManager || "pnpm"
const handlePageHighlight = React.useCallback(
(isComponent: boolean, item: { url: string; name?: React.ReactNode }) => {
if (isComponent) {
const componentName = item.url.split("/").pop()
setSelectedType("component")
setCopyPayload(
`${packageManager} dlx @creative-tim/ui@latest add ${componentName}`
)
} else {
setSelectedType("page")
setCopyPayload("")
}
},
[packageManager, setSelectedType, setCopyPayload]
)
const handleBlockHighlight = React.useCallback(
(block: { name: string; description: string; categories: string[] }) => {
setSelectedType("block")
setCopyPayload(
`${packageManager} dlx @creative-tim/ui@latest add ${block.name}`
)
},
[setSelectedType, setCopyPayload, packageManager]
)
const runCommand = React.useCallback((command: () => unknown) => {
setOpen(false)
command()
}, [])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || e.key === "/") {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
e.preventDefault()
setOpen((open) => !open)
}
if (e.key === "c" && (e.metaKey || e.ctrlKey)) {
runCommand(() => {
if (selectedType === "block") {
copyToClipboardWithMeta(copyPayload, {
name: "copy_npm_command",
properties: { command: copyPayload, pm: packageManager },
})
}
if (selectedType === "page" || selectedType === "component") {
copyToClipboardWithMeta(copyPayload, {
name: "copy_npm_command",
properties: { command: copyPayload, pm: packageManager },
})
}
})
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [copyPayload, runCommand, selectedType, packageManager])
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="ghost"
className={cn(
"relative h-8 w-full justify-start rounded-full border border-white/20 pl-2.5 font-normal text-white/70 shadow-none hover:bg-white/10 hover:text-white sm:pr-12 md:w-40 lg:w-56"
)}
onClick={() => setOpen(true)}
{...props}
>
<span className="hidden lg:inline-flex">Search doc...</span>
<span className="inline-flex lg:hidden">Search...</span>
<div className="absolute top-1.5 right-1.5 hidden gap-1 sm:flex mt-[-1px]">
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
<CommandMenuKbd className="aspect-square">K</CommandMenuKbd>
</div>
</Button>
</DialogTrigger>
<DialogContent
showCloseButton={false}
className="rounded-xl border-none bg-clip-padding p-2 pb-11 shadow-2xl ring-4 ring-neutral-200/80 dark:bg-neutral-900 dark:ring-neutral-800"
>
<DialogHeader className="sr-only">
<DialogTitle>Search documentation...</DialogTitle>
<DialogDescription>Search for a command to run...</DialogDescription>
</DialogHeader>
<Command
className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border"
filter={(value, search, keywords) => {
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
return 1
}
return 0
}}
>
<CommandInput placeholder="Search documentation..." />
<CommandList className="no-scrollbar min-h-80 scroll-pt-2 scroll-pb-1.5">
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
No results found.
</CommandEmpty>
{navItems && navItems.length > 0 && (
<CommandGroup
heading="Pages"
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{navItems.map((item) => (
<CommandMenuItem
key={item.href}
value={`Navigation ${item.label}`}
keywords={["nav", "navigation", item.label.toLowerCase()]}
onHighlight={() => {
setSelectedType("page")
setCopyPayload("")
}}
onSelect={() => {
runCommand(() => router.push(item.href))
}}
>
<IconArrowRight />
{item.label}
</CommandMenuItem>
))}
</CommandGroup>
)}
{tree.children.map((group) => (
<CommandGroup
key={group.$id}
heading={group.name}
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{group.type === "folder" &&
group.children.map((item) => {
if (item.type === "page") {
const isComponent = item.url.includes("/components/")
return (
<CommandMenuItem
key={item.url}
value={
item.name?.toString()
? `${group.name} ${item.name}`
: ""
}
keywords={isComponent ? ["component"] : undefined}
onHighlight={() =>
handlePageHighlight(isComponent, item)
}
onSelect={() => {
runCommand(() => router.push(item.url))
}}
>
{isComponent ? (
<div className="border-muted-foreground aspect-square size-4 rounded-full border border-dashed" />
) : (
<IconArrowRight />
)}
{item.name}
</CommandMenuItem>
)
}
return null
})}
</CommandGroup>
))}
{blocks?.length ? (
<CommandGroup
heading="Examples"
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{blocks.map((block) => (
<CommandMenuItem
key={block.name}
value={block.name}
onHighlight={() => {
handleBlockHighlight(block)
}}
keywords={[
"block",
block.name,
block.description,
...block.categories,
]}
onSelect={() => {
runCommand(() =>
router.push(
`/blocks/${block.categories[0]}#${block.name}`
)
)
}}
>
<SquareDashedIcon />
{block.description}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{block.name}
</span>
</CommandMenuItem>
))}
</CommandGroup>
) : null}
</CommandList>
</Command>
<div className="text-muted-foreground absolute inset-x-0 bottom-0 z-20 flex h-10 items-center gap-2 rounded-b-xl border-t border-t-neutral-100 bg-neutral-50 px-4 text-xs font-medium dark:border-t-neutral-700 dark:bg-neutral-800">
<div className="flex items-center gap-2">
<CommandMenuKbd>
<CornerDownLeftIcon />
</CommandMenuKbd>{" "}
{selectedType === "page" || selectedType === "component"
? "Go to Page"
: null}
</div>
{copyPayload && (
<>
<Separator orientation="vertical" className="!h-4" />
<div className="flex items-center gap-1">
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
<CommandMenuKbd>C</CommandMenuKbd>
{copyPayload}
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
)
}
function CommandMenuItem({
children,
className,
onHighlight,
...props
}: React.ComponentProps<typeof CommandItem> & {
onHighlight?: () => void
"data-selected"?: string
"aria-selected"?: string
}) {
const ref = React.useRef<HTMLDivElement>(null)
useMutationObserver(ref, (mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.attributeName === "aria-selected" &&
ref.current?.getAttribute("aria-selected") === "true"
) {
onHighlight?.()
}
})
})
return (
<CommandItem
ref={ref}
className={cn(
"data-[selected=true]:border-input data-[selected=true]:bg-input/50 h-9 rounded-md border border-transparent !px-3 font-medium",
className
)}
{...props}
>
{children}
</CommandItem>
)
}
function CommandMenuKbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
className={cn(
"pointer-events-none flex h-5 items-center justify-center gap-1 rounded-full border border-white/20 bg-white/10 px-1 font-sans text-[0.7rem] font-medium text-white/70 select-none [&_svg:not([class*='size-'])]:size-3",
className
)}
{...props}
/>
)
}
@@ -0,0 +1,85 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
export function ComponentPreviewTabs({
className,
align = "center",
hideCode = false,
component,
source,
marginOff = false,
defaultTab = "preview",
...props
}: React.ComponentProps<"div"> & {
align?: "center" | "start" | "end"
hideCode?: boolean
component: React.ReactNode
source: React.ReactNode
marginOff?: boolean
defaultTab?: "preview" | "code"
}) {
const [tab, setTab] = React.useState<"preview" | "code">(defaultTab)
return (
<div
className={cn("group relative mt-4 mb-12 flex flex-col gap-2", className)}
{...props}
>
<Tabs
className="relative mr-auto w-full"
value={tab}
onValueChange={(value) => setTab(value as "preview" | "code")}
>
<div className="flex items-center justify-between">
{!hideCode && (
<TabsList className="justify-start gap-4 rounded-none bg-transparent px-2 md:px-0">
<TabsTrigger
value="preview"
className="text-muted-foreground data-[state=active]:text-foreground px-0 text-base data-[state=active]:shadow-none dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-transparent"
>
Preview
</TabsTrigger>
<TabsTrigger
value="code"
className="text-muted-foreground data-[state=active]:text-foreground px-0 text-base data-[state=active]:shadow-none dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-transparent"
>
Code
</TabsTrigger>
</TabsList>
)}
</div>
</Tabs>
<div
data-tab={tab}
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-1"
>
<div
data-slot="preview"
data-active={tab === "preview"}
className="invisible data-[active=true]:visible"
>
<div
data-align={align}
className={cn(
"preview flex h-[450px] w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
marginOff ? "p-0" : "p-10"
)}
>
{component}
</div>
</div>
<div
data-slot="code"
data-active={tab === "code"}
className="absolute inset-0 hidden overflow-hidden data-[active=true]:block **:[figure]:!m-0 **:[pre]:h-[450px]"
>
{source}
</div>
</div>
</div>
)
}
+75
View File
@@ -0,0 +1,75 @@
import Image from "next/image"
import { ComponentPreviewTabs } from "@/components/component-preview-tabs"
import { ComponentSource } from "@/components/component-source"
import { Index } from "@/registry/__index__"
export function ComponentPreview({
name,
type,
className,
align = "center",
hideCode = false,
marginOff = false,
defaultTab = "preview",
...props
}: React.ComponentProps<"div"> & {
name: string
align?: "center" | "start" | "end"
description?: string
hideCode?: boolean
type?: "block" | "component" | "example"
marginOff?: boolean
defaultTab?: "preview" | "code"
}) {
const Component = Index[name]?.component
if (!Component) {
return (
<p className="text-muted-foreground text-sm">
Component{" "}
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
</code>{" "}
not found in registry.
</p>
)
}
if (type === "block") {
return (
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
<Image
src={`/r/${name}-light.png`}
alt={name}
width={1440}
height={900}
className="bg-background absolute top-0 left-0 z-20 w-[970px] max-w-none sm:w-[1280px] md:hidden dark:hidden md:dark:hidden"
/>
<Image
src={`/r/${name}-dark.png`}
alt={name}
width={1440}
height={900}
className="bg-background absolute top-0 left-0 z-20 hidden w-[970px] max-w-none sm:w-[1280px] md:hidden dark:block md:dark:hidden"
/>
<div className="bg-background absolute inset-0 hidden w-[1600px] md:block">
<iframe src={`/view/${name}`} className="size-full" />
</div>
</div>
)
}
return (
<ComponentPreviewTabs
className={className}
align={align}
hideCode={hideCode}
component={<Component />}
source={<ComponentSource name={name} collapsible={false} />}
marginOff={marginOff}
defaultTab={defaultTab}
{...props}
/>
)
}
+101
View File
@@ -0,0 +1,101 @@
import fs from "node:fs/promises"
import path from "node:path"
import * as React from "react"
import { highlightCode } from "@/lib/highlight-code"
import { getRegistryItem } from "@/lib/registry"
import { cn } from "@/lib/utils"
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
import { CopyButton } from "@/components/copy-button"
import { getIconForLanguageExtension } from "@/components/icons"
export async function ComponentSource({
name,
src,
title,
language,
collapsible = true,
className,
}: React.ComponentProps<"div"> & {
name?: string
src?: string
title?: string
language?: string
collapsible?: boolean
}) {
if (!name && !src) {
return null
}
let code: string | undefined
if (name) {
const item = await getRegistryItem(name)
code = item?.files?.[0]?.content
}
if (src) {
const file = await fs.readFile(path.join(process.cwd(), src), "utf-8")
code = file
}
if (!code) {
return null
}
const lang = language ?? title?.split(".").pop() ?? "tsx"
const highlightedCode = await highlightCode(code, lang)
if (!collapsible) {
return (
<div className={cn("relative", className)}>
<ComponentCode
code={code}
highlightedCode={highlightedCode}
language={lang}
title={title}
/>
</div>
)
}
return (
<CodeCollapsibleWrapper className={className}>
<ComponentCode
code={code}
highlightedCode={highlightedCode}
language={lang}
title={title}
/>
</CodeCollapsibleWrapper>
)
}
function ComponentCode({
code,
highlightedCode,
language,
title,
}: {
code: string
highlightedCode: string
language: string
title: string | undefined
}) {
return (
<figure data-rehype-pretty-code-figure="" className="[&>pre]:max-h-96">
{title && (
<figcaption
data-rehype-pretty-code-title=""
className="text-code-foreground [&_svg]:text-code-foreground flex items-center gap-2 [&_svg]:size-4 [&_svg]:opacity-70"
data-language={language}
>
{getIconForLanguageExtension(language)}
{title}
</figcaption>
)}
<CopyButton value={code} />
<div dangerouslySetInnerHTML={{ __html: highlightedCode }} />
</figure>
)
}
+66
View File
@@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import { cn } from "@/registry/creative-tim/lib/utils"
export function ComponentWrapper({
className,
name,
children,
...props
}: React.ComponentPropsWithoutRef<"div"> & { name: string }) {
return (
<ComponentErrorBoundary name={name}>
<div
id={name}
data-name={name.toLowerCase()}
className={cn(
"flex w-full scroll-mt-16 flex-col rounded-lg border",
className
)}
{...props}
>
<div className="border-b px-4 py-3">
<div className="text-sm font-medium">{getComponentName(name)}</div>
</div>
<div className="flex flex-1 items-center gap-2 p-4">{children}</div>
</div>
</ComponentErrorBoundary>
)
}
class ComponentErrorBoundary extends React.Component<
{ children: React.ReactNode; name: string },
{ hasError: boolean }
> {
constructor(props: { children: React.ReactNode; name: string }) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error(`Error in component ${this.props.name}:`, error, errorInfo)
}
render() {
if (this.state.hasError) {
return (
<div className="p-4 text-red-500">
Something went wrong in component: {this.props.name}
</div>
)
}
return this.props.children
}
}
function getComponentName(name: string) {
// convert kebab-case to title case
return name.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
}
+31
View File
@@ -0,0 +1,31 @@
import Link from "next/link"
import { source } from "@/lib/source"
export function ComponentsList() {
const components = source.pageTree.children.find(
(page) => page.$id === "components"
)
if (components?.type !== "folder") {
return
}
const list = components.children.filter(
(component) => component.type === "page"
)
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-x-8 lg:gap-x-16 lg:gap-y-6 xl:gap-x-20">
{list.map((component) => (
<Link
key={component.$id}
href={component.url}
className="text-lg font-medium underline-offset-4 hover:underline md:text-base"
>
{component.name}
</Link>
))}
</div>
)
}
+77
View File
@@ -0,0 +1,77 @@
"use client"
import * as React from "react"
import { CheckIcon, ClipboardIcon } from "lucide-react"
import { Event, trackEvent } from "@/lib/events"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
export function copyToClipboardWithMeta(value: string, event?: Event) {
navigator.clipboard.writeText(value)
if (event) {
trackEvent(event)
}
}
export function CopyButton({
value,
className,
variant = "ghost",
event,
...props
}: React.ComponentProps<typeof Button> & {
value: string
src?: string
event?: Event["name"]
}) {
const [hasCopied, setHasCopied] = React.useState(false)
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false)
}, 2000)
}, [])
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
data-slot="copy-button"
size="icon"
variant={variant}
className={cn(
"bg-code absolute top-3 right-2 z-10 size-7 hover:opacity-100 focus-visible:opacity-100",
className
)}
onClick={() => {
copyToClipboardWithMeta(
value,
event
? {
name: event,
properties: {
code: value,
},
}
: undefined
)
setHasCopied(true)
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon /> : <ClipboardIcon />}
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied" : "Copy to Clipboard"}
</TooltipContent>
</Tooltip>
)
}
+65
View File
@@ -0,0 +1,65 @@
"use client"
import { Fragment } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useBreadcrumb } from "fumadocs-core/breadcrumb"
import type { PageTree } from "fumadocs-core/server"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
export function DocsBreadcrumb({
tree,
className,
}: {
tree: PageTree.Root
className?: string
}) {
const pathname = usePathname()
const items = useBreadcrumb(pathname, tree)
if (items.length === 0) return null
return (
<Breadcrumb className={className}>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/docs" className="hover:text-accent-foreground">
Docs
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
{items.map((item, i) => (
<Fragment key={i}>
{i !== 0 && <BreadcrumbSeparator />}
{item.url ? (
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link
href={item.url}
className="hover:text-accent-foreground truncate"
>
{item.name}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
) : (
<BreadcrumbItem>
<BreadcrumbPage>{item.name}</BreadcrumbPage>
</BreadcrumbItem>
)}
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
)
}
+156
View File
@@ -0,0 +1,156 @@
"use client"
import { IconCheck, IconChevronDown, IconCopy } from "@tabler/icons-react"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Popover,
PopoverAnchor,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Separator } from "@/components/ui/separator"
function getPromptUrl(baseURL: string, url: string) {
return `${baseURL}?q=${encodeURIComponent(
`Im looking at this @creative-tim/ui documentation: ${url}.
Help me understand how to use it. Be ready to explain concepts, give examples, or help debug based on it.
`
)}`
}
const menuItems = {
markdown: (url: string) => (
<a href={`${url}.md`} target="_blank" rel="noopener noreferrer">
<svg strokeLinejoin="round" viewBox="0 0 22 16">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.5 2.25H2.5C1.80964 2.25 1.25 2.80964 1.25 3.5V12.5C1.25 13.1904 1.80964 13.75 2.5 13.75H19.5C20.1904 13.75 20.75 13.1904 20.75 12.5V3.5C20.75 2.80964 20.1904 2.25 19.5 2.25ZM2.5 1C1.11929 1 0 2.11929 0 3.5V12.5C0 13.8807 1.11929 15 2.5 15H19.5C20.8807 15 22 13.8807 22 12.5V3.5C22 2.11929 20.8807 1 19.5 1H2.5ZM3 4.5H4H4.25H4.6899L4.98715 4.82428L7 7.02011L9.01285 4.82428L9.3101 4.5H9.75H10H11V5.5V11.5H9V7.79807L7.73715 9.17572L7 9.97989L6.26285 9.17572L5 7.79807V11.5H3V5.5V4.5ZM15 8V4.5H17V8H19.5L17 10.5L16 11.5L15 10.5L12.5 8H15Z"
fill="currentColor"
/>
</svg>
View as Markdown
</a>
),
v0: (url: string) => (
<a
href={getPromptUrl("https://v0.app", url)}
target="_blank"
rel="noopener noreferrer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 147 70"
className="size-4.5 -translate-x-px"
>
<path d="M56 50.203V14h14v46.156C70 65.593 65.593 70 60.156 70c-2.596 0-5.158-1-7-2.843L0 14h19.797L56 50.203ZM147 56h-14V23.953L100.953 56H133v14H96.687C85.814 70 77 61.186 77 50.312V14h14v32.156L123.156 14H91V0h36.312C138.186 0 147 8.814 147 19.688V56Z" />
</svg>
<span className="-translate-x-[2px]">Open in v0</span>
</a>
),
chatgpt: (url: string) => (
<a
href={getPromptUrl("https://chatgpt.com", url)}
target="_blank"
rel="noopener noreferrer"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
fill="currentColor"
/>
</svg>
Open in ChatGPT
</a>
),
claude: (url: string) => (
<a
href={getPromptUrl("https://claude.ai/new", url)}
target="_blank"
rel="noopener noreferrer"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z"
fill="currentColor"
/>
</svg>
Open in Claude
</a>
),
}
export function DocsCopyPage({ page, url }: { page: string; url: string }) {
const { copyToClipboard, isCopied } = useCopyToClipboard()
const trigger = (
<Button
variant="secondary"
size="sm"
className="peer -ml-0.5 size-8 shadow-none md:size-7 md:text-[0.8rem]"
>
<IconChevronDown className="rotate-180 sm:rotate-0" />
</Button>
)
return (
<Popover>
<div className="bg-secondary group/buttons relative flex rounded-lg *:[[data-slot=button]]:focus-visible:relative *:[[data-slot=button]]:focus-visible:z-10">
<PopoverAnchor />
<Button
variant="secondary"
size="sm"
className="h-8 shadow-none md:h-7 md:text-[0.8rem]"
onClick={() => copyToClipboard(page)}
>
{isCopied ? <IconCheck /> : <IconCopy />}
Copy Page
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild className="hidden sm:flex">
{trigger}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="shadow-none">
{Object.entries(menuItems).map(([key, value]) => (
<DropdownMenuItem key={key} asChild>
{value(url)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Separator
orientation="vertical"
className="!bg-foreground/10 absolute top-0 right-8 z-0 !h-8 peer-focus-visible:opacity-0 sm:right-7 sm:!h-7"
/>
<PopoverTrigger asChild className="flex sm:hidden">
{trigger}
</PopoverTrigger>
<PopoverContent
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-sm backdrop-blur-sm"
align="start"
>
{Object.entries(menuItems).map(([key, value]) => (
<Button
variant="ghost"
size="lg"
asChild
key={key}
className="*:[svg]:text-muted-foreground w-full justify-start text-base font-normal"
>
{value(url)}
</Button>
))}
</PopoverContent>
</div>
</Popover>
)
}
+37
View File
@@ -0,0 +1,37 @@
import Link from "next/link"
import { siteConfig } from "@/lib/config"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
export function DocsSidebarCta({ className }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"group bg-surface text-surface-foreground relative flex flex-col gap-2 overflow-hidden rounded-lg p-6 text-sm",
className
)}
>
<div className="bg-surface/80 absolute inset-0" />
<div className="relative z-10 text-base leading-tight font-semibold text-balance group-hover:underline">
Integrate the blocks in your application or use them in v0, Lovable, Claude, etc.
</div>
<div className="text-muted-foreground relative z-10">
Speed up your workflow with modular, open-source components and blocksthat
integrate effortlessly through Registries and MCPs.
</div>
<Button size="sm" className="relative z-10 mt-2 w-fit">
View Blocks
</Button>
<Link
href={siteConfig.utm.blocks}
target="_blank"
rel="noreferrer"
className="absolute inset-0 z-20"
>
<span className="sr-only">Talk to an expert</span>
</Link>
</div>
)
}
+157
View File
@@ -0,0 +1,157 @@
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import type { source } from "@/lib/source"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
const TOP_LEVEL_SECTIONS = [
{ name: "Introduction", href: "/docs" },
{ name: "Registry", href: "/docs/registry" },
{ name: "Components", href: "/docs/components" },
{ name: "How It Works", href: "/docs/how-it-works" },
{ name: "Debug Logs", href: "/docs/debug-logs" },
]
const BLOCKS_SECTIONS = [
{ name: "Account", href: "/blocks/account" },
{ name: "AI Agents", href: "/blocks/ai-agents" },
{ name: "Billing", href: "/blocks/billing" },
{ name: "Blog", href: "/blocks/blog" },
{ name: "Contact", href: "/blocks/contact" },
{ name: "CRUDs", href: "/blocks/cruds" },
{ name: "Ecommerce", href: "/blocks/ecommerce" },
{ name: "FAQs", href: "/blocks/faqs" },
{ name: "Footers", href: "/blocks/footers" },
{ name: "Modals", href: "/blocks/modals" },
{ name: "Testimonials", href: "/blocks/testimonials" },
{ name: "Web3", href: "/blocks/web3" },
]
const EXCLUDED_SECTIONS = ["installation", "dark-mode", "(root)"]
const EXCLUDED_PAGES: string[] = []
export function DocsSidebar({
tree,
...props
}: React.ComponentProps<typeof Sidebar> & { tree: typeof source.pageTree }) {
const pathname = usePathname()
return (
<Sidebar
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)+2rem)] bg-transparent lg:flex"
collapsible="none"
{...props}
>
<SidebarContent className="no-scrollbar overflow-x-hidden px-2 pb-12">
<div className="h-(--top-spacing) shrink-0" />
<SidebarGroup>
<SidebarGroupLabel className="text-muted-foreground font-medium">
Getting Started
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{TOP_LEVEL_SECTIONS.map(({ name, href }) => {
return (
<SidebarMenuItem key={name}>
<SidebarMenuButton
asChild
isActive={
href === "/docs" || href === "/docs/components"
? pathname === href
: pathname.startsWith(href)
}
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={href}>
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
{name}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel className="text-muted-foreground font-medium">
Blocks
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{BLOCKS_SECTIONS.map(({ name, href }) => {
return (
<SidebarMenuItem key={name}>
<SidebarMenuButton
asChild
isActive={
href === "/blocks" || href === "/blocks/featured"
? pathname === href
: pathname.startsWith(href)
}
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={href}>
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
{name}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{tree.children.map((item) => {
if (EXCLUDED_SECTIONS.includes(item.$id ?? "")) {
return null
}
return (
<SidebarGroup key={item.$id}>
<SidebarGroupLabel className="text-muted-foreground font-medium">
{item.name}
</SidebarGroupLabel>
<SidebarGroupContent>
{item.type === "folder" && (
<SidebarMenu className="gap-0.5">
{item.children.map((item) => {
return (
item.type === "page" &&
!EXCLUDED_PAGES.includes(item.url) && (
<SidebarMenuItem key={item.url}>
<SidebarMenuButton
asChild
isActive={item.url === pathname}
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={item.url}>
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
{item.name}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
)
})}
</SidebarMenu>
)}
</SidebarGroupContent>
</SidebarGroup>
)
})}
</SidebarContent>
</Sidebar>
)
}
+126
View File
@@ -0,0 +1,126 @@
"use client"
import * as React from "react"
import { IconMenu3 } from "@tabler/icons-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
function useActiveItem(itemIds: string[]) {
const [activeId, setActiveId] = React.useState<string | null>(null)
React.useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setActiveId(entry.target.id)
}
}
},
{ rootMargin: "0% 0% -80% 0%" }
)
for (const id of itemIds ?? []) {
const element = document.getElementById(id)
if (element) {
observer.observe(element)
}
}
return () => {
for (const id of itemIds ?? []) {
const element = document.getElementById(id)
if (element) {
observer.unobserve(element)
}
}
}
}, [itemIds])
return activeId
}
export function DocsTableOfContents({
toc,
variant = "list",
className,
}: {
toc: {
title?: React.ReactNode
url: string
depth: number
}[]
variant?: "dropdown" | "list"
className?: string
}) {
const [open, setOpen] = React.useState(false)
const itemIds = React.useMemo(
() => toc.map((item) => item.url.replace("#", "")),
[toc]
)
const activeHeading = useActiveItem(itemIds)
if (!toc?.length) {
return null
}
if (variant === "dropdown") {
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={cn("h-8 md:h-7", className)}
>
<IconMenu3 /> On This Page
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="no-scrollbar max-h-[70svh]"
>
{toc.map((item) => (
<DropdownMenuItem
key={item.url}
asChild
onClick={() => {
setOpen(false)
}}
data-depth={item.depth}
className="data-[depth=3]:pl-6 data-[depth=4]:pl-8"
>
<a href={item.url}>{item.title}</a>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
return (
<div className={cn("flex flex-col gap-2 p-4 pt-0 text-sm", className)}>
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs">
On This Page
</p>
{toc.map((item) => (
<a
key={item.url}
href={item.url}
className="text-muted-foreground hover:text-foreground data-[active=true]:text-foreground text-[0.8rem] no-underline transition-colors data-[depth=3]:pl-4 data-[depth=4]:pl-6"
data-active={item.url === `#${activeHeading}`}
data-depth={item.depth}
>
{item.title}
</a>
))}
</div>
)
}
+23
View File
@@ -0,0 +1,23 @@
"use client"
import { cn } from "@/lib/utils"
import {
ScrollArea,
ScrollBar,
} from "@/components/ui/scroll-area"
export function ExamplesNav({
className,
...props
}: React.ComponentProps<"div">) {
// const pathname = usePathname()
return (
<div className={cn("flex items-center", className)} {...props}>
<ScrollArea className="max-w-[96%] md:max-w-[600px] lg:max-w-none">
<div className="flex items-center"></div>
<ScrollBar orientation="horizontal" className="invisible" />
</ScrollArea>
</div>
)
}
+469
View File
@@ -0,0 +1,469 @@
import Link from "next/link"
import { Badge } from "@/components/ui/badge"
import { Card } from "@/components/ui/card"
interface ExampleCategory {
id: string
title: string
description: string
examples: {
name: string
blockCount: number
category: string
thumbnail?: string
comingSoon?: boolean
}[]
}
const categories: ExampleCategory[] = [
{
id: "web3",
title: "Web 3.0",
description:
"Innovative sections built for decentralized applications, blockchain projects, and crypto platforms.",
examples: [
{
name: "Web 3.0 Login",
blockCount: 5,
category: "web3",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/login-thumbnail.jpg",
comingSoon: true,
},
{
name: "Web 3.0 Charts",
blockCount: 5,
category: "web3",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/web3-charts-thumbnail.jpg",
comingSoon: true,
},
{
name: "Web 3.0 Cards",
blockCount: 5,
category: "web3",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/collections-thumbnail.jpg",
},
],
},
{
id: "application-ui",
title: "Application & Admin UI",
description:
"Fully coded interface for stunning dashboards, admin panels, and web apps.",
examples: [
{
name: "Widgets",
blockCount: 7,
category: "cruds",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/widgets-thumbnail.jpg",
comingSoon: true,
},
{
name: "Charts",
blockCount: 6,
category: "cruds",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/charts-thumbnail.jpg",
comingSoon: true,
},
{
name: "Tables",
blockCount: 10,
category: "cruds",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/tables-thumbnail.jpg",
comingSoon: true,
},
{
name: "Modals",
blockCount: 5,
category: "modals",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/modals-thumbnail.jpg",
},
{
name: "Account",
blockCount: 7,
category: "account",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/account-thumbnail.jpg",
},
{
name: "Billing",
blockCount: 5,
category: "billing",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/billing-thumbnail.jpg",
},
{
name: "Tables Headers",
blockCount: 6,
category: "cruds",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/table-headers-thumbnail.jpg",
comingSoon: true,
},
{
name: "Tables Footers",
blockCount: 6,
category: "cruds",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/table-footers-thumbnail.jpg",
comingSoon: true,
},
{
name: "KPI Cards",
blockCount: 7,
category: "cruds",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/kpi-cards-thumbnail.jpg",
comingSoon: true,
},
{
name: "Sidebars",
blockCount: 9,
category: "sidebars",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/sidebar-thumbnail.jpg",
comingSoon: true,
},
{
name: "Dropdowns",
blockCount: 6,
category: "dropdowns",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/dropdown-filter-thumbnail.jpg",
comingSoon: true,
},
{
name: "User Profile",
blockCount: 5,
category: "user-profile",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/user-profile-thumbnail.jpg",
comingSoon: true,
},
],
},
{
id: "marketing",
title: "Marketing & Presentation",
description:
"Optimized for showcasing products and services, perfect for landing pages and marketing websites.",
examples: [
{
name: "Hero Sections",
blockCount: 18,
category: "hero-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/headers-thumbnail.jpg",
comingSoon: true,
},
{
name: "Testimonial Sections",
blockCount: 17,
category: "testimonials",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/testimonial-thumbnail.jpg",
},
{
name: "Popup Sections",
blockCount: 7,
category: "popups",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/popup-thumbnail.jpg",
comingSoon: true,
},
{
name: "Authentication",
blockCount: 6,
category: "authentication",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/authentication-thumbnail.jpg",
comingSoon: true,
},
{
name: "Onboarding Sections",
blockCount: 5,
category: "onboarding",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/onboarding-thumbnail.jpg",
comingSoon: true,
},
{
name: "Navbars",
blockCount: 10,
category: "navbars",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/navbars-thumbnail.jpg",
comingSoon: true,
},
{
name: "Contact Sections",
blockCount: 15,
category: "contact",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/contact-us-thumbnail.jpg",
},
{
name: "Team Sections",
blockCount: 17,
category: "team-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/team-thumbnail.jpg",
comingSoon: true,
},
{
name: "Newsletter",
blockCount: 17,
category: "newsletter",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/newsletter-thumbnail.jpg",
comingSoon: true,
},
{
name: "Footers",
blockCount: 16,
category: "footers",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/footer-thumbnail.jpg",
},
{
name: "Coming Soon Sections",
blockCount: 4,
category: "coming-soon",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/coming-soon-thumbnail.jpg",
comingSoon: true,
},
],
},
{
id: "content-ui",
title: "Content UI",
description:
"Versatile UI sections for blogs, articles, and multimedia-rich content.",
examples: [
{
name: "FAQs",
blockCount: 6,
category: "faqs",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/faq-thumbnail.jpg",
},
{
name: "Feature Sections",
blockCount: 18,
category: "feature-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/features-thumbnail.jpg",
comingSoon: true,
},
{
name: "Stats Sections",
blockCount: 10,
category: "stats-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/stats-thumbnail.jpg",
comingSoon: true,
},
{
name: "Content Sections",
blockCount: 16,
category: "content-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/content-section-thumbnail.jpg",
comingSoon: true,
},
{
name: "Cards",
blockCount: 5,
category: "cards",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/cards-thumbnail.jpg",
comingSoon: true,
},
{
name: "Error Sections",
blockCount: 7,
category: "error-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/Error404-thumbnail.jpg",
comingSoon: true,
},
{
name: "Maintenance Sections",
blockCount: 4,
category: "maintenance-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/Error500-thumbnail.jpg",
comingSoon: true,
},
{
name: "Blog",
blockCount: 15,
category: "blog",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/blog-posts-thumbnail.jpg",
},
{
name: "Logo Sections",
blockCount: 7,
category: "logo-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/logo-areas-thumbnail.jpg",
comingSoon: true,
},
{
name: "Calendar Sections",
blockCount: 3,
category: "calendar-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/calendar-thumbnail.jpg",
comingSoon: true,
},
],
},
{
id: "ecommerce",
title: "Ecommerce UI",
description:
"Ready-to-use blocks for product listings, shopping carts, and checkout flows.",
examples: [
{
name: "Banner Sections",
blockCount: 7,
category: "banner-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/banner-thumbnail.jpg",
comingSoon: true,
},
{
name: "Ecommerce Sections",
blockCount: 14,
category: "ecommerce",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/ecommerce-thumbnail.jpg",
},
{
name: "Product List Sections",
blockCount: 5,
category: "product-list",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/product-list-thumbnail.jpg",
comingSoon: true,
},
{
name: "Customer Overview Sections",
blockCount: 3,
category: "customer-overview",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/customer-overview-thumbnail.jpg",
comingSoon: true,
},
{
name: "Pricing Sections",
blockCount: 12,
category: "pricing",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/pricing-thumbnail.jpg",
comingSoon: true,
},
{
name: "Categories",
blockCount: 5,
category: "categories",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/categories-thumbnail.jpg",
comingSoon: true,
},
{
name: "Order Sections",
blockCount: 7,
category: "order-sections",
thumbnail:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/refs/heads/master/david-ui/thumbs/order-tracking-thumbnail.jpg",
comingSoon: true,
},
],
},
]
export function ExamplesPreview() {
return (
<section className="container space-y-16 py-12 md:py-20">
{categories.map((category) => (
<div key={category.id} className="space-y-6">
<div>
<h3 className="mb-2 text-2xl font-bold tracking-tight">
{category.title}
</h3>
<p className="text-muted-foreground max-w-4xl text-sm sm:text-base">
{category.description}
</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{category.examples.map((example) => {
const CardContent = (
<Card className="bg-card overflow-hidden border transition-all hover:shadow-md py-0 gap-4">
<div className="bg-muted/30 relative aspect-[16/10] overflow-hidden">
{example.thumbnail ? (
<img
src={example.thumbnail}
alt={example.name}
className="object-cover"
/>
) : (
<div className="from-muted/80 to-muted/40 absolute inset-0 bg-gradient-to-br" />
)}
{example.comingSoon && (
<div className="absolute inset-0 flex items-center justify-center bg-black/40">
<Badge
variant="secondary"
className="text-foreground bg-white px-4 py-1.5 font-medium dark:bg-black"
>
Soon
</Badge>
</div>
)}
</div>
<div className="px-4 pb-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium">{example.name}</h4>
<span className="text-muted-foreground text-xs">
{example.blockCount} Blocks
</span>
</div>
</div>
</Card>
)
if (example.comingSoon) {
return (
<div key={example.name} className="cursor-not-allowed">
{CardContent}
</div>
)
}
return (
<Link
key={example.name}
href={`/blocks/${example.category}`}
className="group"
>
{CardContent}
</Link>
)
})}
</div>
</div>
))}
</section>
)
}
+41
View File
@@ -0,0 +1,41 @@
import Link from "next/link"
import { siteConfig } from "@/lib/config"
import { Icons } from "@/components/icons"
import { Button } from "@/components/ui/button"
export function GitHubLink() {
return (
<Button
asChild
size="sm"
variant="ghost"
className="h-8 text-white/70 shadow-none hover:bg-white/10 hover:text-white"
>
<Link href={siteConfig.links.github} target="_blank" rel="noreferrer">
<Icons.gitHub />
{/* <React.Suspense fallback={<Skeleton className="h-4 w-8" />}>
<StarsCount />
</React.Suspense> */}
</Link>
</Button>
)
}
export async function StarsCount() {
const data = await fetch(
"https://api.github.com/repos/creativetimofficial/ui",
{
next: { revalidate: 86400 }, // Cache for 1 day (86400 seconds)
}
)
const json = await data.json()
return (
<span className="text-muted-foreground w-8 text-xs tabular-nums">
{json.stargazers_count >= 1000
? `${(json.stargazers_count / 1000).toFixed(1)}k`
: json.stargazers_count.toLocaleString()}
</span>
)
}
+220
View File
@@ -0,0 +1,220 @@
import { FileIcon } from "lucide-react"
type IconProps = React.HTMLAttributes<SVGElement>
export const Icons = {
logo: (props: IconProps) => (
<svg
viewBox="0 0 876 876"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M468 292H528V584H468V292Z" fill="currentColor" />
<path d="M348 292H408V584H348V292Z" fill="currentColor" />
</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>
),
json: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12.043 23.968c.479-.004.953-.029 1.426-.094a11.805 11.805 0 0 0 3.146-.863 12.404 12.404 0 0 0 3.793-2.542 11.977 11.977 0 0 0 2.44-3.427 11.794 11.794 0 0 0 1.02-3.476c.149-1.16.135-2.346-.045-3.499a11.96 11.96 0 0 0-.793-2.788 11.197 11.197 0 0 0-.854-1.617c-1.168-1.837-2.861-3.314-4.81-4.3a12.835 12.835 0 0 0-2.172-.87h-.005c.119.063.24.132.345.201.12.074.239.146.351.225a8.93 8.93 0 0 1 1.559 1.33c1.063 1.145 1.797 2.548 2.218 4.041.284.982.434 1.998.495 3.017.044.743.044 1.491-.047 2.229-.149 1.27-.554 2.51-1.228 3.596a7.475 7.475 0 0 1-1.903 2.084c-1.244.928-2.877 1.482-4.436 1.114a3.916 3.916 0 0 1-.748-.258 4.692 4.692 0 0 1-.779-.45 6.08 6.08 0 0 1-1.244-1.105 6.507 6.507 0 0 1-1.049-1.747 7.366 7.366 0 0 1-.494-2.54c-.03-1.273.225-2.553.854-3.67a6.43 6.43 0 0 1 1.663-1.918c.225-.178.464-.333.704-.479l.016-.007a5.121 5.121 0 0 0-1.441-.12 4.963 4.963 0 0 0-1.228.24c-.359.12-.704.27-1.019.45a6.146 6.146 0 0 0-.733.494c-.211.18-.42.36-.615.555-1.123 1.153-1.768 2.682-2.022 4.256-.15.973-.15 1.96-.091 2.95.105 1.395.391 2.787.945 4.062a8.518 8.518 0 0 0 1.348 2.173 8.14 8.14 0 0 0 3.132 2.23 7.934 7.934 0 0 0 2.113.54c.074.015.149.015.209.015zm-2.934-.398a4.102 4.102 0 0 1-.45-.228 8.5 8.5 0 0 1-2.038-1.534c-1.094-1.137-1.827-2.566-2.247-4.08a15.184 15.184 0 0 1-.495-3.172 12.14 12.14 0 0 1 .046-2.082c.135-1.257.495-2.501 1.124-3.58a6.889 6.889 0 0 1 1.783-2.053 6.23 6.23 0 0 1 1.633-.9 5.363 5.363 0 0 1 3.522-.045c.029 0 .029 0 .045.03.015.015.045.015.06.03.045.016.104.045.165.074.239.12.479.271.704.42a6.294 6.294 0 0 1 2.097 2.502c.42.914.615 1.934.631 2.938.014 1.079-.18 2.157-.645 3.146a6.42 6.42 0 0 1-2.638 2.832c.09.03.18.045.271.075.225.044.449.074.688.074 1.468.045 2.892-.66 3.94-1.647.195-.18.375-.375.54-.585.225-.27.435-.54.614-.823.239-.375.435-.75.614-1.154a8.112 8.112 0 0 0 .509-1.664c.196-1.004.211-2.022.149-3.026-.135-2.022-.673-4.045-1.842-5.724a9.054 9.054 0 0 0-.555-.719 9.868 9.868 0 0 0-1.063-1.034 8.477 8.477 0 0 0-1.363-.915 9.927 9.927 0 0 0-1.692-.598l-.3-.06c-.209-.03-.42-.044-.634-.06a8.453 8.453 0 0 0-1.015.016c-.704.045-1.412.16-2.112.337C5.799 1.227 2.863 3.566 1.3 6.67A11.834 11.834 0 0 0 .238 9.801a11.81 11.81 0 0 0-.104 3.775c.12 1.02.374 2.023.778 2.977.227.57.511 1.124.825 1.648 1.094 1.783 2.683 3.236 4.51 4.24.688.39 1.408.69 2.157.944.226.074.45.15.689.21z"
/>
</svg>
),
ts: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path d="M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z" />
</svg>
),
css: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path d="M0 0v20.16A3.84 3.84 0 0 0 3.84 24h16.32A3.84 3.84 0 0 0 24 20.16V3.84A3.84 3.84 0 0 0 20.16 0Zm14.256 13.08c1.56 0 2.28 1.08 2.304 2.64h-1.608c.024-.288-.048-.6-.144-.84-.096-.192-.288-.264-.552-.264-.456 0-.696.264-.696.84-.024.576.288.888.768 1.08.72.288 1.608.744 1.92 1.296q.432.648.432 1.656c0 1.608-.912 2.592-2.496 2.592-1.656 0-2.4-1.032-2.424-2.688h1.68c0 .792.264 1.176.792 1.176.264 0 .456-.072.552-.24.192-.312.24-1.176-.048-1.512-.312-.408-.912-.6-1.32-.816q-.828-.396-1.224-.936c-.24-.36-.36-.888-.36-1.536 0-1.44.936-2.472 2.424-2.448m5.4 0c1.584 0 2.304 1.08 2.328 2.64h-1.608c0-.288-.048-.6-.168-.84-.096-.192-.264-.264-.528-.264-.48 0-.72.264-.72.84s.288.888.792 1.08c.696.288 1.608.744 1.92 1.296.264.432.408.984.408 1.656.024 1.608-.888 2.592-2.472 2.592-1.68 0-2.424-1.056-2.448-2.688h1.68c0 .744.264 1.176.792 1.176.264 0 .456-.072.552-.24.216-.312.264-1.176-.048-1.512-.288-.408-.888-.6-1.32-.816-.552-.264-.96-.576-1.2-.936s-.36-.888-.36-1.536c-.024-1.44.912-2.472 2.4-2.448m-11.031.018c.711-.006 1.419.198 1.839.63.432.432.672 1.128.648 1.992H9.336c.024-.456-.096-.792-.432-.96-.312-.144-.768-.048-.888.24-.12.264-.192.576-.168.864v3.504c0 .744.264 1.128.768 1.128a.65.65 0 0 0 .552-.264c.168-.24.192-.552.168-.84h1.776c.096 1.632-.984 2.712-2.568 2.688-1.536 0-2.496-.864-2.472-2.472v-4.032c0-.816.24-1.44.696-1.848.432-.408 1.146-.624 1.857-.63" />
</svg>
),
bash: (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
<path d="M21.038 4.9 13.461.402a2.86 2.86 0 0 0-2.923.001L2.961 4.9A3.023 3.023 0 0 0 1.5 7.503v8.995c0 1.073.557 2.066 1.462 2.603l7.577 4.497a2.86 2.86 0 0 0 2.922 0l7.577-4.497a3.023 3.023 0 0 0 1.462-2.603V7.503A3.021 3.021 0 0 0 21.038 4.9zM15.17 18.946l.013.646c.001.078-.05.167-.111.198l-.383.22c-.061.031-.111-.007-.112-.085l-.007-.635c-.328.136-.66.169-.872.084-.04-.016-.057-.075-.041-.142l.139-.584a.24.24 0 0 1 .069-.121.163.163 0 0 1 .036-.026c.022-.011.043-.014.062-.006.229.077.521.041.802-.101.357-.181.596-.545.592-.907-.003-.328-.181-.465-.613-.468-.55.001-1.064-.107-1.072-.917-.007-.667.34-1.361.889-1.8l-.007-.652c-.001-.08.048-.168.111-.2l.37-.236c.061-.031.111.007.112.087l.006.653c.273-.109.511-.138.726-.088.047.012.067.076.048.151l-.144.578a.255.255 0 0 1-.065.116.161.161 0 0 1-.038.028.083.083 0 0 1-.057.009c-.098-.022-.332-.073-.699.113-.385.195-.52.53-.517.778.003.297.155.387.681.396.7.012 1.003.318 1.01 1.023.007.689-.362 1.433-.928 1.888zm3.973-1.087c0 .06-.008.116-.058.145l-1.916 1.164c-.05.029-.09.004-.09-.056v-.494c0-.06.037-.093.087-.122l1.887-1.129c.05-.029.09-.004.09.056v.436zm1.316-11.062-7.168 4.427c-.894.523-1.553 1.109-1.553 2.187v8.833c0 .645.26 1.063.66 1.184a2.304 2.304 0 0 1-.398.039c-.42 0-.833-.114-1.197-.33L3.226 18.64a2.494 2.494 0 0 1-1.201-2.142V7.503c0-.881.46-1.702 1.201-2.142L10.803.863a2.342 2.342 0 0 1 2.394 0l7.577 4.498a2.479 2.479 0 0 1 1.164 1.732c-.252-.536-.818-.682-1.479-.296z" />
</svg>
),
v0: (props: IconProps) => (
<svg
viewBox="0 0 40 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M23.3919 0H32.9188C36.7819 0 39.9136 3.13165 39.9136 6.99475V16.0805H36.0006V6.99475C36.0006 6.90167 35.9969 6.80925 35.9898 6.71766L26.4628 16.079C26.4949 16.08 26.5272 16.0805 26.5595 16.0805H36.0006V19.7762H26.5595C22.6964 19.7762 19.4788 16.6139 19.4788 12.7508V3.68923H23.3919V12.7508C23.3919 12.9253 23.4054 13.0977 23.4316 13.2668L33.1682 3.6995C33.0861 3.6927 33.003 3.68923 32.9188 3.68923H23.3919V0Z"
fill="currentColor"
></path>
<path
d="M13.7688 19.0956L0 3.68759H5.53933L13.6231 12.7337V3.68759H17.7535V17.5746C17.7535 19.6705 15.1654 20.6584 13.7688 19.0956Z"
fill="currentColor"
></path>
</svg>
),
orb: (props: IconProps) => (
<svg
width="20px"
height="20px"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
color="currentColor"
{...props}
>
<circle
cx="10.2612"
cy="10.0003"
r="5.55024"
stroke="currentColor"
strokeWidth="1.3"
></circle>
<path
d="M5.69092 12.9584C10.8447 12.9584 10.261 7.93345 15.5893 8.31403"
stroke="currentColor"
strokeWidth="1.3"
></path>
<path
d="M14.0162 14.1629C10.5909 14.5435 9.11942 9.83637 4.59829 9.83636"
stroke="currentColor"
strokeWidth="1.3"
></path>
</svg>
),
}
export function getIconForLanguageExtension(language: string) {
switch (language) {
case "json":
return <Icons.json />
case "css":
return <Icons.css className="fill-foreground" />
case "js":
case "jsx":
case "ts":
case "tsx":
case "typescript":
return <Icons.ts className="fill-foreground" />
default:
return <FileIcon />
}
}
+33
View File
@@ -0,0 +1,33 @@
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
export function MainNav({
items,
className,
...props
}: React.ComponentProps<"nav"> & {
items: { href: string; label: string }[]
}) {
const pathname = usePathname()
return (
<nav className={cn("flex items-center gap-6", className)} {...props}>
{items.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"text-sm font-medium transition-colors hover:text-white",
pathname === item.href ? "text-white" : "text-white/70"
)}
>
{item.label}
</Link>
))}
</nav>
)
}
+142
View File
@@ -0,0 +1,142 @@
"use client"
import * as React from "react"
import Link, { LinkProps } from "next/link"
import { useRouter } from "next/navigation"
import { source } from "@/lib/source"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
export function MobileNav({
tree,
items,
className,
}: {
tree: typeof source.pageTree
items: { href: string; label: string }[]
className?: string
}) {
const [open, setOpen] = React.useState(false)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
className={cn(
"extend-touch-target h-8 touch-manipulation items-center justify-start gap-2 pr-2 pl-0 hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 active:bg-transparent dark:hover:bg-transparent",
className
)}
>
<span className="flex h-8 items-center text-base leading-none font-medium text-white">
Menu
</span>
<div className="relative flex h-8 w-4 items-center justify-center">
<div className="relative size-4">
<span
className={cn(
"absolute left-0 block h-0.5 w-4 bg-white transition-all duration-100",
open ? "top-[0.4rem] -rotate-45" : "top-1"
)}
/>
<span
className={cn(
"absolute left-0 block h-0.5 w-4 bg-white transition-all duration-100",
open ? "top-[0.4rem] rotate-45" : "top-2.5"
)}
/>
</div>
<span className="sr-only">Toggle Menu</span>
</div>
</Button>
</PopoverTrigger>
<PopoverContent
className="bg-background/90 no-scrollbar h-(--radix-popper-available-height) w-(--radix-popper-available-width) overflow-y-auto rounded-none border-none p-0 shadow-none backdrop-blur duration-100"
align="start"
side="bottom"
alignOffset={-16}
sideOffset={14}
>
<div className="flex flex-col gap-12 overflow-auto px-6 py-6">
<div className="flex flex-col gap-4">
<div className="text-muted-foreground text-sm font-medium">
Menu
</div>
<div className="flex flex-col gap-3">
<MobileLink href="/" onOpenChange={setOpen}>
Home
</MobileLink>
{items.map((item, index) => (
<MobileLink key={index} href={item.href} onOpenChange={setOpen}>
{item.label}
</MobileLink>
))}
</div>
</div>
<div className="flex flex-col gap-8">
{tree?.children?.map((group, index) => {
if (group.type === "folder") {
return (
<div key={index} className="flex flex-col gap-4">
<div className="text-muted-foreground text-sm font-medium">
{group.name}
</div>
<div className="flex flex-col gap-3">
{group.children.map((item) => {
if (item.type === "page") {
return (
<MobileLink
key={`${item.url}-${index}`}
href={item.url}
onOpenChange={setOpen}
>
{item.name}
</MobileLink>
)
}
})}
</div>
</div>
)
}
})}
</div>
</div>
</PopoverContent>
</Popover>
)
}
function MobileLink({
href,
onOpenChange,
className,
children,
...props
}: LinkProps & {
onOpenChange?: (open: boolean) => void
children: React.ReactNode
className?: string
}) {
const router = useRouter()
return (
<Link
href={href}
onClick={() => {
router.push(href.toString())
onOpenChange?.(false)
}}
className={cn("text-2xl font-medium", className)}
{...props}
>
{children}
</Link>
)
}
+51
View File
@@ -0,0 +1,51 @@
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import { useMetaColor } from "@/hooks/use-meta-color"
import { Button } from "@/components/ui/button"
export function ModeSwitcher() {
const { setTheme, resolvedTheme } = useTheme()
const { setMetaColor, metaColor } = useMetaColor()
React.useEffect(() => {
setMetaColor(metaColor)
}, [metaColor, setMetaColor])
const toggleTheme = React.useCallback(() => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}, [resolvedTheme, setTheme])
return (
<Button
variant="ghost"
size="icon"
className="group/toggle extend-touch-target size-8 text-white/70 hover:bg-white/10 hover:text-white"
onClick={toggleTheme}
title="Toggle theme"
>
<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"
className="size-4.5"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 3l0 18" />
<path d="M12 9l4.65 -4.65" />
<path d="M12 14.3l7.37 -7.37" />
<path d="M12 19.6l8.85 -8.85" />
</svg>
<span className="sr-only">Toggle theme</span>
</Button>
)
}
+37
View File
@@ -0,0 +1,37 @@
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
} from "@/components/ui/navigation-menu"
export function NavHeader() {
const pathname = usePathname()
return (
<NavigationMenu className="hidden sm:flex">
<NavigationMenuList className="gap-2 *:data-[slot=navigation-menu-item]:h-7 **:data-[slot=navigation-menu-link]:py-1 **:data-[slot=navigation-menu-link]:font-medium">
<NavigationMenuItem>
<NavigationMenuLink asChild data-active={pathname === "/"}>
<Link href="/">Home</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild data-active={pathname === "/charts"}>
<Link href="/charts">Charts</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild data-active={pathname === "/forms"}>
<Link href="/forms">Forms</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
)
}
+114
View File
@@ -0,0 +1,114 @@
"use client"
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} 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">
<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-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown 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-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}
+28
View File
@@ -0,0 +1,28 @@
import { cn } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { Button } from "@/components/ui/button"
export function OpenInV0Button({
name,
className,
...props
}: React.ComponentProps<typeof Button> & {
name: string
}) {
return (
<Button
variant="secondary"
size="sm"
asChild
className={cn("h-[1.8rem] gap-1", className)}
{...props}
>
<a
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${process.env.NEXT_PUBLIC_APP_URL}/ui/r/${name}.json`}
target="_blank"
>
Open in <Icons.v0 className="size-5" />
</a>
</Button>
)
}
+61
View File
@@ -0,0 +1,61 @@
import { cn } from "@/lib/utils"
function PageHeader({
className,
children,
...props
}: React.ComponentProps<"section">) {
return (
<section className={cn("border-grid", className)} {...props}>
<div className="container-wrapper">
<div className="container flex flex-col items-center gap-2 pt-20 pb-4 text-center md:pt-32 md:pb-4">
{children}
</div>
</div>
</section>
)
}
function PageHeaderHeading({
className,
...props
}: React.ComponentProps<"h1">) {
return (
<h1
className={cn(
"text-primary leading-tighter max-w-2xl text-4xl font-semibold tracking-tight text-balance lg:leading-[1.1] lg:font-semibold xl:text-5xl xl:tracking-tighter",
className
)}
{...props}
/>
)
}
function PageHeaderDescription({
className,
...props
}: React.ComponentProps<"p">) {
return (
<p
className={cn(
"text-foreground max-w-3xl text-base text-balance sm:text-lg",
className
)}
{...props}
/>
)
}
function PageActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex w-full items-center justify-center gap-2 pt-2 **:data-[slot=button]:shadow-none",
className
)}
{...props}
/>
)
}
export { PageActions, PageHeader, PageHeaderDescription, PageHeaderHeading }
+15
View File
@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
export function PageNav({
children,
className,
...props
}: React.ComponentProps<"div">) {
return (
<div className={cn("container-wrapper scroll-mt-24", className)} {...props}>
<div className="container flex items-center justify-between gap-4 py-4">
{children}
</div>
</div>
)
}
+547
View File
@@ -0,0 +1,547 @@
"use client"
import * as React from "react"
import { format } from "date-fns"
import {
Archive,
Briefcase,
CalendarIcon,
CheckCircle,
ChevronDown,
ChevronUp,
Github,
Heart,
Home,
Mail,
Phone,
Quote,
Shield,
ShoppingBag,
Star,
User,
} from "lucide-react"
import { cn } from "@/lib/utils"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch"
export function ShowcaseMasonry() {
const [date, setDate] = React.useState<Date>(new Date(2024, 9, 10))
const [isFavorite, setIsFavorite] = React.useState(false)
const [isGithubActive, setIsGithubActive] = React.useState(false)
return (
<section className="container py-12 md:py-20">
<div className="columns-1 gap-4 space-y-4 md:columns-2 lg:columns-3">
{/* Order Summary Card */}
<div className="break-inside-avoid">
<div className="bg-muted/30 rounded-xl border p-6">
<div className="mb-6 flex items-center gap-3">
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-full">
<ShoppingBag className="text-primary h-5 w-5" />
</div>
<h2 className="text-xl font-bold">Order Summary</h2>
</div>
<div className="space-y-4">
<div className="bg-background hover:bg-muted/50 flex items-start gap-4 rounded-lg border p-3 transition-colors">
<div className="relative h-20 w-20 overflow-hidden rounded-md border">
<img
src="https://v3.material-tailwind.com/coat-1.png"
alt="Classic Suit"
className="h-full w-full object-cover p-1"
/>
</div>
<div className="flex-1 space-y-1">
<p className="leading-tight font-semibold">Classic Suit</p>
<p className="text-muted-foreground text-sm">Silk · Size M</p>
<p className="text-muted-foreground text-xs">Qty: 1</p>
</div>
<p className="font-semibold">$1,300</p>
</div>
<div className="bg-background hover:bg-muted/50 flex items-start gap-4 rounded-lg border p-3 transition-colors">
<div className="relative h-20 w-20 overflow-hidden rounded-md border">
<img
src="https://v3.material-tailwind.com/coat-2.png"
alt="Premium Suit"
className="h-full w-full object-cover p-1"
/>
</div>
<div className="flex-1 space-y-1">
<p className="leading-tight font-semibold">Premium Suit</p>
<p className="text-muted-foreground text-sm">
Linen · Size M
</p>
<p className="text-muted-foreground text-xs">Qty: 1</p>
</div>
<p className="font-semibold">$790</p>
</div>
</div>
<Separator className="my-6" />
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-medium">$2,090</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Shipping estimate</span>
<span className="font-medium">$0</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Tax estimate</span>
<span className="font-medium">$5</span>
</div>
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between">
<span className="text-lg font-bold">Total</span>
<span className="text-2xl font-bold">$2,095</span>
</div>
</div>
</div>
{/* Transaction History Card */}
<div className="break-inside-avoid">
<div className="dark:bg-card rounded-xl border bg-white p-6 shadow-sm">
<div className="mb-6 flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<h2 className="font-semibold">History Transactions</h2>
<p className="text-muted-foreground mt-1 text-xs sm:text-sm">
Track and monitor your financial activity.
</p>
</div>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className={cn(
"shrink-0 text-xs sm:text-sm",
!date && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-1.5 h-3.5 w-3.5 sm:h-4 sm:w-4" />
<span className="hidden sm:inline">
{date ? format(date, "MMM d, yyyy") : "Select"}
</span>
<span className="sm:hidden">
{date ? format(date, "M/d") : "Date"}
</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<Calendar
mode="single"
selected={date}
onSelect={(day) => day && setDate(day)}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div>
<p className="text-muted-foreground mb-3 text-xs font-semibold sm:text-sm">
March 2023
</p>
<div className="space-y-2">
<div className="flex items-center gap-2 rounded-lg border p-3 sm:gap-4 sm:p-4">
<div className="bg-card text-card-foreground hidden h-10 w-10 shrink-0 items-center justify-center rounded-xl border shadow-sm sm:flex sm:h-12 sm:w-12">
<ChevronDown className="h-4 w-4 text-red-600 sm:h-5 sm:w-5" />
</div>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-semibold sm:text-base">
Netflix
</p>
<p className="text-muted-foreground truncate text-xs sm:text-sm">
27 March 2026, at 12:30 PM
</p>
</div>
<p className="shrink-0 text-xs font-semibold text-red-600 sm:text-sm">
- $2,500.00
</p>
</div>
<div className="flex items-center gap-2 rounded-lg border p-3 sm:gap-4 sm:p-4">
<div className="bg-card text-card-foreground hidden h-10 w-10 shrink-0 items-center justify-center rounded-xl border shadow-sm sm:flex sm:h-12 sm:w-12">
<ChevronUp className="h-4 w-4 text-green-600 sm:h-5 sm:w-5" />
</div>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-semibold sm:text-base">Apple</p>
<p className="text-muted-foreground truncate text-xs sm:text-sm">
27 March 2026, at 04:30 AM
</p>
</div>
<p className="shrink-0 text-xs font-semibold text-green-600 sm:text-sm">
+ $2,000.00
</p>
</div>
</div>
</div>
</div>
</div>
{/* Product Card */}
<div className="break-inside-avoid">
<div className="group bg-card relative overflow-hidden rounded-lg border transition-all hover:shadow-lg">
<Badge
variant="secondary"
className="absolute top-3 left-3 z-10 bg-white dark:bg-gray-900"
>
Exclusive
</Badge>
<button
onClick={() => setIsFavorite(!isFavorite)}
className="absolute top-3 right-3 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white/90 backdrop-blur-sm transition-colors hover:bg-white dark:bg-gray-900/90 dark:hover:bg-gray-900"
>
<Heart
className={`h-4 w-4 transition-colors ${
isFavorite
? "fill-red-500 text-red-500"
: "text-gray-600 dark:text-gray-400"
}`}
/>
</button>
<div className="bg-muted/30 aspect-square overflow-hidden">
<img
src="https://images.unsplash.com/photo-1574015974293-817f0ebebb74?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=973"
alt="Cable-knit cashmere cardigan"
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
<div className="border-t p-4">
<div className="mb-2 flex items-start justify-between gap-2">
<div className="flex-1">
<p className="text-sm font-semibold">Zegna</p>
<p className="text-muted-foreground mt-1 text-sm leading-tight">
Cable-knit cashmere cardigan
</p>
</div>
</div>
<p className="mt-2 font-semibold">3,450</p>
</div>
</div>
</div>
{/* Account Integration Card */}
<div className="break-inside-avoid">
<Card className="bg-card border p-6">
<div className="mb-6 border-b pb-6">
<div className="flex items-center gap-3">
<div className="bg-primary/10 flex h-12 w-12 items-center justify-center rounded-lg">
<Shield className="text-primary h-6 w-6" />
</div>
<div>
<h2 className="text-xl font-semibold tracking-tight">
Third-Party Integrations
</h2>
<p className="text-muted-foreground mt-1 text-sm">
Manage and configure connections to external services
</p>
</div>
</div>
</div>
<Accordion type="single" collapsible defaultValue="github">
<AccordionItem
value="github"
className="border-border rounded-lg border"
>
<div className="flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-1 items-center gap-3">
<div className="bg-muted/50 flex h-10 w-10 shrink-0 items-center justify-center rounded-lg">
<Github
className={`h-5 w-5 ${isGithubActive ? "text-primary" : "text-muted-foreground"}`}
/>
</div>
<div className="min-w-0 flex-1 text-left">
<div className="mb-1 flex flex-wrap items-center gap-2">
<h3 className="text-sm font-semibold">GitHub</h3>
<Badge
variant="outline"
className="text-muted-foreground text-xs"
>
Development
</Badge>
</div>
<p className="text-muted-foreground line-clamp-1 text-xs">
Connect your GitHub account to sync repositories
</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Switch
id="github-toggle"
checked={isGithubActive}
onCheckedChange={setIsGithubActive}
/>
<Label
htmlFor="github-toggle"
className="cursor-pointer text-xs"
>
{isGithubActive ? "Enabled" : "Enable"}
</Label>
</div>
<AccordionTrigger className="hover:bg-muted/50 rounded p-2">
<span className="sr-only">Toggle details</span>
</AccordionTrigger>
</div>
</div>
<AccordionContent className="border-t px-4 pt-4 pb-4">
<div className="space-y-3">
<p className="text-muted-foreground text-xs leading-relaxed">
You haven&apos;t added your GitHub account or you
aren&apos;t authorized. Connect your account to enable
repository syncing and collaboration features.
</p>
<Button size="sm" variant="outline" className="w-full">
<Github className="mr-2 h-4 w-4" />
Connect GitHub Account
</Button>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</Card>
</div>
{/* Order Timeline Card */}
<div className="break-inside-avoid">
<div className="bg-card rounded-xl border p-6">
<h3 className="mb-6 text-lg font-semibold">Order Timeline</h3>
<div className="space-y-6">
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="bg-primary flex h-10 w-10 items-center justify-center rounded-full shadow-md">
<Home className="h-5 w-5 text-white" />
</div>
<div className="bg-primary my-2 w-0.5 flex-1" />
</div>
<div className="pb-6">
<div className="mb-1 flex items-center gap-2">
<p className="font-semibold">Order Placed</p>
<Badge
variant="outline"
className="border-green-500/30 bg-green-50 text-green-700 dark:bg-green-950/30 dark:text-green-400"
>
Complete
</Badge>
</div>
<p className="text-muted-foreground text-sm">
Your order was placed on April 1, 2024
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="bg-primary flex h-10 w-10 items-center justify-center rounded-full shadow-md">
<CheckCircle className="h-5 w-5 text-white" />
</div>
<div className="bg-primary my-2 w-0.5 flex-1" />
</div>
<div className="pb-6">
<div className="mb-1 flex items-center gap-2">
<p className="font-semibold">Order Confirmed</p>
<Badge
variant="outline"
className="border-green-500/30 bg-green-50 text-green-700 dark:bg-green-950/30 dark:text-green-400"
>
Complete
</Badge>
</div>
<p className="text-muted-foreground text-sm">
Your order has been confirmed on April 2, 2024
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="bg-primary flex h-10 w-10 items-center justify-center rounded-full shadow-md">
<Archive className="h-5 w-5 text-white" />
</div>
</div>
<div>
<div className="mb-1 flex items-center gap-2">
<p className="font-semibold">Order Shipped</p>
<Badge
variant="outline"
className="border-blue-500/30 bg-blue-50 text-blue-700 dark:bg-blue-950/30 dark:text-blue-400"
>
In Progress
</Badge>
</div>
<p className="text-muted-foreground text-sm">
Your order has been shipped on April 3, 2024
</p>
</div>
</div>
</div>
</div>
</div>
{/* Personal Information Card */}
<div className="break-inside-avoid">
<Card className="bg-card border p-6">
<div className="mb-6 border-b pb-4">
<h3 className="text-lg font-semibold tracking-tight">
Personal Information
</h3>
<p className="text-muted-foreground mt-1 text-sm">
Manage your personal details
</p>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label
htmlFor="firstName"
className="flex items-center gap-2"
>
<User className="text-muted-foreground h-4 w-4" />
First Name
</Label>
<Input
id="firstName"
placeholder="Emma"
defaultValue="Emma"
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName" className="flex items-center gap-2">
<User className="text-muted-foreground h-4 w-4" />
Last Name
</Label>
<Input
id="lastName"
placeholder="Roberts"
defaultValue="Roberts"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email" className="flex items-center gap-2">
<Mail className="text-muted-foreground h-4 w-4" />
Email Address
</Label>
<Input
id="email"
type="email"
placeholder="emma@mail.com"
defaultValue="emma@mail.com"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="phone" className="flex items-center gap-2">
<Phone className="text-muted-foreground h-4 w-4" />
Phone Number
</Label>
<Input
id="phone"
placeholder="+1 (555) 123-4567"
defaultValue="+1 (555) 123-4567"
/>
</div>
<div className="space-y-2">
<Label
htmlFor="profession"
className="flex items-center gap-2"
>
<Briefcase className="text-muted-foreground h-4 w-4" />
Profession
</Label>
<Select defaultValue="ui-ux">
<SelectTrigger id="profession">
<SelectValue placeholder="Select Profession" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ui-ux">UI/UX Designer</SelectItem>
<SelectItem value="frontend">
Frontend Developer
</SelectItem>
<SelectItem value="backend">Backend Developer</SelectItem>
<SelectItem value="fullstack">
Fullstack Developer
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</Card>
</div>
{/* Testimonial Card */}
<div className="break-inside-avoid">
<Card className="group border-border/50 hover:border-border transition-all hover:shadow-lg">
<CardContent className="p-6">
<div className="mb-4 flex items-center gap-4">
<div className="relative shrink-0">
<img
src="https://images.unsplash.com/photo-1716662318479-a9c0f1cd1a0e?auto=format&fit=crop&q=80&w=400&h=400"
alt="Sarah Johnson profile"
className="border-border h-14 w-14 rounded-full border-2 object-cover transition-transform group-hover:scale-105"
/>
<div className="bg-background absolute -right-1 -bottom-1 rounded-full p-1 shadow-md">
<Quote className="text-primary h-3 w-3" />
</div>
</div>
<div className="min-w-0 flex-1 text-left">
<h3 className="font-semibold">Sarah Johnson</h3>
<p className="text-muted-foreground text-sm">
Product Designer
</p>
</div>
<div className="flex shrink-0 gap-0.5">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className="h-3.5 w-3.5 fill-yellow-400 text-yellow-400"
/>
))}
</div>
</div>
<blockquote className="text-muted-foreground text-left text-sm leading-relaxed">
&quot;The attention to detail and component quality is
outstanding. These UI blocks have significantly accelerated our
design workflow.&quot;
</blockquote>
</CardContent>
</Card>
</div>
</div>
</section>
)
}
+33
View File
@@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import { GalleryHorizontalIcon } from "lucide-react"
import { trackEvent } from "@/lib/events"
import { cn } from "@/lib/utils"
import { useLayout } from "@/hooks/use-layout"
import { Button } from "@/components/ui/button"
export function SiteConfig({ className }: React.ComponentProps<typeof Button>) {
const { layout, setLayout } = useLayout()
return (
<Button
variant="ghost"
size="icon"
onClick={() => {
const newLayout = layout === "fixed" ? "full" : "fixed"
setLayout(newLayout)
trackEvent({
name: "set_layout",
properties: { layout: newLayout },
})
}}
className={cn("size-8", className)}
title="Toggle layout"
>
<span className="sr-only">Toggle layout</span>
<GalleryHorizontalIcon />
</Button>
)
}
+37
View File
@@ -0,0 +1,37 @@
import Link from "next/link"
import { Heart } from "lucide-react"
import { siteConfig } from "@/lib/config"
export function SiteFooter() {
return (
<footer className="group-has-[.section-soft]/body:bg-surface/40 3xl:fixed:bg-transparent group-has-[.docs-nav]/body:pb-20 group-has-[.docs-nav]/body:sm:pb-0 dark:bg-transparent">
<div className="container-wrapper px-4 xl:px-6">
<div className="flex h-(--footer-height) items-center justify-between">
<div className="text-muted-foreground flex w-full items-center justify-center gap-1 px-1 text-center text-xs leading-loose sm:text-sm">
Made with{" "}
<Heart className="h-3 w-3 fill-red-500 text-red-500 sm:h-4 sm:w-4" />{" "}
by{" "}
<Link
href={siteConfig.utm.main}
rel="noreferrer"
className="font-medium underline underline-offset-4"
>
Creative Tim
</Link>
. Open source on{" "}
<a
href={siteConfig.links.github}
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-4"
>
GitHub
</a>
.
</div>
</div>
</div>
</footer>
)
}
+75
View File
@@ -0,0 +1,75 @@
import Link from "next/link"
import { ArrowRight } from "lucide-react"
import { siteConfig } from "@/lib/config"
import { source } from "@/lib/source"
import { CommandMenu } from "@/components/command-menu"
import { GitHubLink } from "@/components/github-link"
import { MainNav } from "@/components/main-nav"
import { MobileNav } from "@/components/mobile-nav"
import { ModeSwitcher } from "@/components/mode-switcher"
import blocks from "@/registry/__blocks__.json"
import { Button } from "@/components/ui/button"
export function SiteHeader() {
const pageTree = source.pageTree
const assetPrefix = process.env.NEXT_PUBLIC_ASSET_PREFIX
return (
<header className="sticky top-0 z-50 w-full pt-4">
<div className="container px-6">
<div className="mx-auto rounded-full border border-white/10 bg-black/80 backdrop-blur-sm">
<div className="flex h-12 items-center justify-between px-2">
{/* Left: Logo + Brand + Nav */}
<div className="flex items-center gap-3">
<Link href="/" className="flex items-center gap-2">
<img
src={`${assetPrefix}/apple-touch-icon-square.jpg`}
alt="Creative Tim UI"
className="h-8 w-8 rounded-full"
/>
<span className="inline-block text-sm font-semibold text-white md:text-base">
{siteConfig.name}
</span>
</Link>
{/* Vertical Line Separator */}
<div className="hidden h-6 w-px bg-white/20 lg:block" />
{/* Navigation Links */}
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
</div>
{/* Right: Actions */}
<div className="flex items-center gap-2">
<div className="hidden md:flex">
<CommandMenu
tree={pageTree}
navItems={siteConfig.navItems}
blocks={blocks}
/>
</div>
<GitHubLink />
<ModeSwitcher />
<Button
asChild
size="sm"
className="hidden rounded-full bg-white text-black hover:bg-white/90 sm:inline-flex"
>
<Link href="/docs" className="flex items-center gap-1">
Get Started
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<MobileNav
tree={pageTree}
items={siteConfig.navItems}
className="flex lg:hidden"
/>
</div>
</div>
</div>
</div>
</header>
)
}
@@ -0,0 +1,21 @@
const SHOW = true
export function TailwindIndicator() {
if (process.env.NODE_ENV === "production" || !SHOW) {
return null
}
return (
<div
data-tailwind-indicator=""
className="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white"
>
<div className="block sm:hidden">xs</div>
<div className="hidden sm:block md:hidden">sm</div>
<div className="hidden md:block lg:hidden">md</div>
<div className="hidden lg:block xl:hidden">lg</div>
<div className="hidden xl:block 2xl:hidden">xl</div>
<div className="hidden 2xl:block">2xl</div>
</div>
)
}
@@ -0,0 +1,178 @@
"use client"
import { useEffect, useState } from "react"
import { ChevronLeft, ChevronRight, Quote } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import { Button } from "@/components/ui/button"
const testimonials = [
{
name: "Eugen Tudorache",
role: "CTO",
company: "Updivision",
image: "/ui/eugen.jpg",
quote:
"Creative Tim UI has completely transformed how we build interfaces. The components are beautifully designed and incredibly easy to customize.",
},
{
name: "Robert Tatoi",
role: "Founder",
company: "PlayVertical",
image: "/ui/robert.jpg",
quote:
"The quality of these components is outstanding. They've saved us countless hours and our product looks better than ever.",
},
{
name: "Fredy Andrei",
role: "CEO",
company: "Simmmple",
image: "/ui/freddy.jpg",
quote:
"Finally, a component library that doesn't compromise on design or flexibility. The attention to detail is exceptional.",
},
{
name: "Rares Toma",
role: "CEO",
company: "Loopple",
image: "/ui/rares.jpg",
quote:
"Our development velocity has increased significantly. The documentation is clear and the components are production-ready.",
},
]
export function TestimonialsSection() {
const [currentIndex, setCurrentIndex] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % testimonials.length)
}, 5000)
return () => clearInterval(timer)
}, [])
const goToPrevious = () => {
setCurrentIndex(
(prev) => (prev - 1 + testimonials.length) % testimonials.length
)
}
const goToNext = () => {
setCurrentIndex((prev) => (prev + 1) % testimonials.length)
}
const currentTestimonial = testimonials[currentIndex]
return (
<section className="container py-12 md:py-20">
<div className="mx-auto max-w-5xl">
<div className="mb-12 text-center">
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">
Loved by Developers & Designers
</h2>
<p className="text-muted-foreground mx-auto max-w-3xl text-base sm:text-lg">
Join thousands of professionals who trust Creative Tim UI to build
beautiful, modern interfaces faster.
</p>
</div>
<div className="relative">
<div className="border-border/50 bg-muted/30 relative overflow-hidden rounded-3xl border backdrop-blur-sm">
<div className="from-primary/5 to-primary/5 absolute inset-0 bg-gradient-to-br via-transparent" />
<div className="relative px-8 py-12 md:px-16 md:py-20">
<AnimatePresence mode="wait">
<motion.div
key={currentIndex}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5 }}
className="flex flex-col items-center text-center"
>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="bg-primary/10 mb-8 rounded-full p-4"
>
<Quote className="text-primary h-8 w-8 md:h-10 md:w-10" />
</motion.div>
<motion.blockquote
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
className="mb-10 text-2xl leading-normal font-medium md:text-3xl lg:text-4xl"
>
{currentTestimonial.quote}
</motion.blockquote>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.4 }}
className="flex flex-col items-center gap-4 sm:flex-row"
>
<img
src={currentTestimonial.image}
alt={currentTestimonial.name}
className="border-border h-16 w-16 rounded-full border-2 object-cover"
/>
<div className="text-center sm:text-left">
<div className="text-lg font-semibold">
{currentTestimonial.name}
</div>
<div className="text-muted-foreground text-sm">
{currentTestimonial.role} at{" "}
{currentTestimonial.company}
</div>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
</div>
</div>
{/* Navigation Buttons */}
<div className="mt-8 flex items-center justify-center gap-4">
<Button
variant="outline"
size="icon"
onClick={goToPrevious}
className="hover:bg-primary/10 h-10 w-10 rounded-full"
>
<ChevronLeft className="h-5 w-5" />
</Button>
{/* Dots Indicator */}
<div className="flex gap-2">
{testimonials.map((_, index) => (
<button
key={index}
onClick={() => setCurrentIndex(index)}
className={`h-2 rounded-full transition-all ${
index === currentIndex
? "bg-primary w-8"
: "bg-muted-foreground/30 hover:bg-muted-foreground/50 w-2"
}`}
aria-label={`Go to testimonial ${index + 1}`}
/>
))}
</div>
<Button
variant="outline"
size="icon"
onClick={goToNext}
className="hover:bg-primary/10 h-10 w-10 rounded-full"
>
<ChevronRight className="h-5 w-5" />
</Button>
</div>
</div>
</div>
</section>
)
}
+22
View File
@@ -0,0 +1,22 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
{...props}
>
{children}
</NextThemesProvider>
)
}
+109
View File
@@ -0,0 +1,109 @@
"use client"
import { cn } from "@/lib/utils"
import { useThemeConfig } from "@/components/active-theme"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const DEFAULT_THEMES = [
{
name: "Default",
value: "default",
},
{
name: "Scaled",
value: "scaled",
},
{
name: "Mono",
value: "mono",
},
]
const COLOR_THEMES = [
{
name: "Blue",
value: "blue",
},
{
name: "Green",
value: "green",
},
{
name: "Amber",
value: "amber",
},
{
name: "Rose",
value: "rose",
},
{
name: "Purple",
value: "purple",
},
{
name: "Orange",
value: "orange",
},
{
name: "Teal",
value: "teal",
},
]
export function ThemeSelector({ className }: React.ComponentProps<"div">) {
const { activeTheme, setActiveTheme } = useThemeConfig()
return (
<div className={cn("flex items-center gap-2", className)}>
<Label htmlFor="theme-selector" className="sr-only">
Theme
</Label>
<Select value={activeTheme} onValueChange={setActiveTheme}>
<SelectTrigger
id="theme-selector"
size="sm"
className="bg-secondary text-secondary-foreground border-secondary justify-start shadow-none *:data-[slot=select-value]:w-12"
>
<span className="font-medium">Theme:</span>
<SelectValue placeholder="Select a theme" />
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
{DEFAULT_THEMES.map((theme) => (
<SelectItem
key={theme.name}
value={theme.value}
className="data-[state=checked]:opacity-50"
>
{theme.name}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Colors</SelectLabel>
{COLOR_THEMES.map((theme) => (
<SelectItem
key={theme.name}
value={theme.value}
className="data-[state=checked]:opacity-50"
>
{theme.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
)
}
+66
View File
@@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+157
View File
@@ -0,0 +1,157 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
+66
View File
@@ -0,0 +1,66 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }
+11
View File
@@ -0,0 +1,11 @@
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
}
export { AspectRatio }
+53
View File
@@ -0,0 +1,53 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }
+46
View File
@@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }
+109
View File
@@ -0,0 +1,109 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
+58
View File
@@ -0,0 +1,58 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
+213
View File
@@ -0,0 +1,213 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toISOString().split("T")[0]}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

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