chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
target/
|
||||
.next/
|
||||
build/
|
||||
dist/
|
||||
|
||||
/templates/
|
||||
/fixtures/
|
||||
@@ -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
@@ -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/*
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
dist
|
||||
node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
apps/www/pages/api/registry.json
|
||||
**/fixtures
|
||||
+143
@@ -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
@@ -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.
|
||||
@@ -0,0 +1,205 @@
|
||||
[](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).
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`creativetimofficial/ui`
|
||||
- 原始仓库:https://github.com/creativetimofficial/ui
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
dist
|
||||
node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
registry/__index__.tsx
|
||||
content/docs/components/calendar.mdx
|
||||
@@ -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'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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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. */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 ?? ""),
|
||||
}))
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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(
|
||||
`I’m 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 />
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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't added your GitHub account or you
|
||||
aren'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">
|
||||
"The attention to detail and component quality is
|
||||
outstanding. These UI blocks have significantly accelerated our
|
||||
design workflow."
|
||||
</blockquote>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
Reference in New Issue
Block a user