a
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
# ============================================================
|
||||
# Python
|
||||
# ============================================================
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*$py.class
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
.venv-*/
|
||||
venv/
|
||||
env/
|
||||
.env/
|
||||
.virtualenv/
|
||||
pipenv/
|
||||
|
||||
# Distribution / packaging
|
||||
*.egg-info/
|
||||
*.egg
|
||||
dist/
|
||||
build/
|
||||
*.whl
|
||||
|
||||
# Unit test / coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
coverage.xml
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Jupyter Notebook checkpoints
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# Serverless
|
||||
.serverless/
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Node / JavaScript / TypeScript
|
||||
# ============================================================
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
# Next.js build output
|
||||
.next/
|
||||
out/
|
||||
|
||||
# Nuxt.js
|
||||
.nuxt/
|
||||
|
||||
# SvelteKit
|
||||
.svelte-kit/
|
||||
|
||||
# Turborepo
|
||||
.turbo/
|
||||
|
||||
# Vercel
|
||||
.vercel/
|
||||
|
||||
# Build output
|
||||
storybook-static/
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Environment / Secrets
|
||||
# ============================================================
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.sample
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Databases
|
||||
# ============================================================
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Logs
|
||||
# ============================================================
|
||||
*.log
|
||||
logs/
|
||||
!logs/.gitkeep
|
||||
|
||||
|
||||
# ============================================================
|
||||
# OS generated files
|
||||
# ============================================================
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Editors / IDEs
|
||||
# ============================================================
|
||||
# VS Code
|
||||
.vscode/
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# JetBrains / IntelliJ IDEA
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
out/
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
\#*\#
|
||||
.#*
|
||||
|
||||
# Sublime Text
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Eclipse
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
.metadata/
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Backup / SCM
|
||||
# ============================================================
|
||||
# Local backup directories
|
||||
backups/
|
||||
backup_logs/
|
||||
memory/
|
||||
memory_backups/
|
||||
|
||||
# File-level backups
|
||||
*.bak
|
||||
*.backup
|
||||
*.orig
|
||||
.svn/
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Media / Binary artifacts
|
||||
# ============================================================
|
||||
# Keep track if needed; uncomment to ignore uploaded assets
|
||||
# uploads/
|
||||
# media/
|
||||
# static/uploads/
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Misc
|
||||
# ============================================================
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Compressed
|
||||
*.7z
|
||||
*.gz
|
||||
*.rar
|
||||
*.tar
|
||||
*.xz
|
||||
*.zip
|
||||
|
||||
# Memory dumps / core
|
||||
core
|
||||
*.core
|
||||
@@ -0,0 +1,13 @@
|
||||
# Nanocode Repository
|
||||
|
||||
This repository is for nanobot automation projects.
|
||||
|
||||
## Purpose
|
||||
- Store configuration files for nanobot
|
||||
- Backup important scripts and tools
|
||||
- Version control for automation workflows
|
||||
|
||||
## Initial Commit
|
||||
Created by nanobot on 2026-03-19
|
||||
|
||||
> only for nanobot
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
github: Logging-Studio
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
name: "Deploy to VPS"
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: DockerHub login
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push to DockerHub
|
||||
run: |
|
||||
docker build -t devarifhossain/retroui:1.2.0 ./
|
||||
docker push devarifhossain/retroui:1.2.0
|
||||
|
||||
# - name: Set up SSH
|
||||
# uses: webfactory/ssh-agent@v0.9.0
|
||||
# with:
|
||||
# ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
# - name: Add VPS to known_hosts
|
||||
# run: |
|
||||
# ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
# - name: Deploy to VPS
|
||||
# run: |
|
||||
# ssh ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} "cd /root/retroui && git pull && docker compose -f ./infra/docker-compose.prod.yml up web --build -d"
|
||||
@@ -0,0 +1,46 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
.venv-*/
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# contentlayer
|
||||
.contentlayer
|
||||
|
||||
package-lock.json
|
||||
.env
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 80
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# Contributing to RetroUI
|
||||
|
||||
Thank you for your interest in contributing to RetroUI 🙏. I hope this guide to help you get started.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Fork this repository
|
||||
|
||||
2. Clone the forked repository to your local machine.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<YOUR_USERNAME>/retroui.git
|
||||
```
|
||||
|
||||
3. Navigate to the project directory
|
||||
|
||||
```bash
|
||||
cd retroui
|
||||
```
|
||||
|
||||
4. Create a new branch
|
||||
|
||||
```bash
|
||||
git switch -c new-branch-name
|
||||
```
|
||||
|
||||
5. install dependencies
|
||||
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
|
||||
6. Run the project
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Adding a New Component
|
||||
|
||||
1. Create the main component in `/components/retroui/NewComponent/NewComponent.tsx`
|
||||
|
||||
```ts
|
||||
export function NewComponent() {
|
||||
return <div>This is your component.</div>;
|
||||
}
|
||||
```
|
||||
|
||||
2. Export your component in `/components/retroui/NewComponent/index.ts` and `/components/retroui/index.ts`
|
||||
|
||||
```ts
|
||||
export * from "./NewComponent";
|
||||
```
|
||||
|
||||
3. Create a preview to showcase your component in `preview/components/new-component.tsx`
|
||||
|
||||
```typescript
|
||||
export default function NewComponentStyle() {
|
||||
return (
|
||||
<span className="px-2.5 py-1 text-sm font-semibold border-2 border-red-600 text-red-600">
|
||||
Error
|
||||
</span>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
4. Add your component preview to the list of components registry in `config/components.ts`
|
||||
|
||||
```typescript
|
||||
export const componentConfig = {
|
||||
registry: {
|
||||
...
|
||||
"new-component": {
|
||||
name: "new-component",
|
||||
filePath: "preview/components/new-component.tsx",
|
||||
preview: lazy(() => import("@/preview/components/new-component")),
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
5. Add documentation for your component in `content/docs/components/...mdx`
|
||||
|
||||
```md
|
||||
---
|
||||
title: Badge
|
||||
description: The component that looks like a button but isn't clickable!
|
||||
lastUpdated: 08 Oct, 2024
|
||||
---
|
||||
|
||||
...
|
||||
|
||||
### New Component
|
||||
|
||||
<ComponentShowcase name="new-component" /> // name of the component in the registry
|
||||
```
|
||||
|
||||
## Ask for Help
|
||||
|
||||
For any help or questions, please open a new GitHub issue.
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM node:22.7.0-slim
|
||||
|
||||
WORKDIR /retroui
|
||||
|
||||
COPY ./package.json ./pnpm-lock.yaml ./
|
||||
RUN npm i pnpm -g \
|
||||
&& pnpm install --frozen-lockfile --no-cache \
|
||||
&& pnpm store prune
|
||||
|
||||
COPY . .
|
||||
RUN pnpm run build
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "run", "start" ]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Arif Hossain
|
||||
|
||||
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,21 @@
|
||||
# Retro UI
|
||||
|
||||
RetroUI is a retro-styled Tailwind CSS component library for modern web apps.
|
||||
|
||||

|
||||
|
||||
### [Why Another UI Library?](https://www.youtube.com/watch?v=7goHwy6k3gU)
|
||||
|
||||
### [Documentation](https://retroui.dev/docs)
|
||||
|
||||
### [Contributing Guide](./CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENCE.md)
|
||||
|
||||
<br />
|
||||
|
||||
## Supported By
|
||||
|
||||
<a href="https://vercel.com/oss"><img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" /></a>
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from "react";
|
||||
import { allDocs } from "contentlayer/generated";
|
||||
import { notFound } from "next/navigation";
|
||||
import { format } from "date-fns";
|
||||
import MDX from "@/components/MDX";
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
import { Metadata } from "next";
|
||||
import { MoveUpRightIcon } from "lucide-react";
|
||||
import { generateToc } from "@/lib/toc";
|
||||
import TableOfContents from "@/components/TableOfContents";
|
||||
|
||||
interface IProps {
|
||||
params: { slug: string[] };
|
||||
}
|
||||
|
||||
function getDocParams({ params }: IProps) {
|
||||
const slug = `/docs${params.slug ? `/${params.slug.join("/")}` : ""}`;
|
||||
const doc = allDocs.find((doc) => doc.url === slug);
|
||||
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: IProps): Promise<Metadata> {
|
||||
const doc = getDocParams({ params });
|
||||
|
||||
if (!doc) {
|
||||
return {
|
||||
title: "Not Found | Retro UI",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${doc.title} | Retro UI`,
|
||||
description: doc.description,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function page({ params }: IProps) {
|
||||
const doc = getDocParams({ params });
|
||||
|
||||
if (!doc) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const toc = await generateToc(doc.body.raw);
|
||||
return (
|
||||
<>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 space-y-12 py-12 px-4 max-w-2xl mx-auto w-full">
|
||||
<div className="border-b pb-6">
|
||||
<Text as="h1" className="text-4xl">
|
||||
{doc.title}
|
||||
</Text>
|
||||
<p className="text-lg text-muted-foreground mt-2">
|
||||
{doc.description}
|
||||
</p>
|
||||
{doc.links && (
|
||||
<div className="flex space-x-4 text-sm mt-4 text-black">
|
||||
{doc.links?.api_reference && (
|
||||
<a
|
||||
className="flex items-center bg-gray-200 px-1.5 py-.5"
|
||||
href={doc.links.api_reference}
|
||||
target="_blank"
|
||||
>
|
||||
API Reference <MoveUpRightIcon className="h-3 w-3 ml-1" />
|
||||
</a>
|
||||
)}
|
||||
{doc.links && doc.links?.source && (
|
||||
<a
|
||||
className="flex items-center bg-gray-200 px-1.5 py-.5"
|
||||
href={doc.links.source}
|
||||
target="_blank"
|
||||
>
|
||||
Source <MoveUpRightIcon className="h-3 w-3 ml-1" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<MDX code={doc.body.code} />
|
||||
</div>
|
||||
<p className="text-right">
|
||||
Last Updated: {format(doc.lastUpdated, "dd MMM, yyy")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Table of Contents */}
|
||||
<div className="hidden lg:block lg:w-60 flex-shrink-0 sticky top-36 self-start space-y-6">
|
||||
<TableOfContents toc={toc} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import SideNav from "@/components/SideNav";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Getting Started | RetroUI",
|
||||
};
|
||||
|
||||
export default function ComponentLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div className="relative max-w-7xl mx-auto">
|
||||
<div className="max-lg:px-4">
|
||||
<div className="flex lg:gap-20 items-start">
|
||||
{/* Sidebar */}
|
||||
<div className="hidden lg:block w-60 flex-shrink-0 sticky top-28 self-start">
|
||||
<SideNav />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { format } from "date-fns";
|
||||
import { Avatar, Badge, Button, Text } from "@/components/retroui";
|
||||
import { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
import { JSXConverters } from "@/components/RichTextConverter";
|
||||
|
||||
interface IProps {
|
||||
params: { slug: string[] };
|
||||
}
|
||||
|
||||
type Post = {
|
||||
id: number,
|
||||
title: string,
|
||||
slug: string,
|
||||
publishedAt: string,
|
||||
content: any,
|
||||
excerpt: string,
|
||||
tags: {
|
||||
name: string,
|
||||
slug: string
|
||||
}[],
|
||||
featuredImage: {
|
||||
url: string,
|
||||
alt: string
|
||||
},
|
||||
}
|
||||
|
||||
async function getBlogParams({ params }: IProps) {
|
||||
const res = await fetch(`https://cms.retroui.dev/api/posts/slug/${params.slug}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
next: {
|
||||
revalidate: 60,
|
||||
},
|
||||
})
|
||||
const post = await res.json()
|
||||
return post;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: IProps): Promise<Metadata> {
|
||||
const blog: Post = await getBlogParams({ params });
|
||||
|
||||
if (!blog) {
|
||||
return {
|
||||
title: "Not Found | Retro UI",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${blog.title} | RetroUI Blogs`,
|
||||
description: blog.excerpt,
|
||||
alternates: {
|
||||
canonical: `https://www.retroui.dev/blogs/${blog.slug}`,
|
||||
},
|
||||
openGraph: {
|
||||
type: "article",
|
||||
images: blog.featuredImage.url,
|
||||
title: `${blog.title} | RetroUI Blogs`,
|
||||
publishedTime: blog.publishedAt,
|
||||
authors: ["Arif Hossain"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function page({ params }: IProps) {
|
||||
const blog: Post | null = await getBlogParams({ params });
|
||||
|
||||
if (!blog) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="max-w-3xl mt-8 mx-auto">
|
||||
<div className="border-b border-black pb-6 mb-6">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Text className="text-muted-foreground text-sm font-medium">
|
||||
{format(new Date(blog.publishedAt), "dd, MMMM yyyy")}
|
||||
</Text>
|
||||
<Text>|</Text>
|
||||
<div className="flex items-center gap-3">
|
||||
{blog.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag.slug}
|
||||
size="sm"
|
||||
variant="surface"
|
||||
className={`bg-${["red", "green", "blue", "yellow", "purple", "pink"][
|
||||
blog.tags.indexOf(tag) % 6
|
||||
]
|
||||
}-300`}
|
||||
>
|
||||
{tag.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Text as="h1" className="mb-2">
|
||||
{blog.title}
|
||||
</Text>
|
||||
<Text className="text-muted-foreground mb-12">
|
||||
{blog.excerpt}
|
||||
</Text>
|
||||
<Image
|
||||
src={`https://cms.retroui.dev${blog.featuredImage.url}`}
|
||||
alt={blog.featuredImage.alt}
|
||||
width={1200}
|
||||
height={800}
|
||||
className="mb-8"
|
||||
/>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex gap-4">
|
||||
<Avatar>
|
||||
<Avatar.Image src="https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/arif.jpg" alt="Arif Hossain" />
|
||||
<Avatar.Fallback>AH</Avatar.Fallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<Text as="h5">Arif Hossain</Text>
|
||||
<Link
|
||||
href={`https://x.com/@ariflogs`}
|
||||
target="_blank"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
@ariflogs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`https://x.com/share?url=${`https://www.retroui.dev/blogs/${blog.slug}`}&text=${blog.title}.%0ACheck it out 👉`}
|
||||
>
|
||||
<Button size="sm" variant="outline">
|
||||
Share on X
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<RichText
|
||||
data={blog.content}
|
||||
converters={JSXConverters}
|
||||
/>
|
||||
|
||||
<hr className="my-12" />
|
||||
|
||||
<Button asChild aria-label="Return to all blog posts" variant="secondary">
|
||||
<Link href="/blogs" className="inline-flex">← Back to blogs</Link>
|
||||
</Button>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Metadata } from "next";
|
||||
import Footer from "@/components/footer";
|
||||
export const metadata: Metadata = {
|
||||
title: "RetroUI Blogs",
|
||||
};
|
||||
|
||||
export default function BlogsLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-6xl mx-auto py-12 px-4 lg:px-0">{children}</div>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { allBlogs } from "@/.contentlayer/generated";
|
||||
import { Avatar, Card, Text } from "@/components/retroui";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { format } from "date-fns";
|
||||
import { sdk } from "@/lib/payload"
|
||||
|
||||
type Post = {
|
||||
id: number,
|
||||
title: string,
|
||||
slug: string,
|
||||
publishedAt: string,
|
||||
excerpt: string,
|
||||
featuredImage: {
|
||||
url: string,
|
||||
alt: string
|
||||
},
|
||||
}
|
||||
|
||||
async function page() {
|
||||
const res = await fetch("https://cms.retroui.dev/api/posts/published", {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
next: {
|
||||
revalidate: 60,
|
||||
},
|
||||
})
|
||||
const posts = await res.json()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text as="h1" className="mb-12">
|
||||
All RetroUI Blogs
|
||||
</Text>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
{posts.docs.map((blog: Post) => (
|
||||
<Link href={`/blogs/${blog.slug}`} key={blog.id}>
|
||||
<Card className="shadow-none hover:-translate-x-2 hover:-translate-y-2 hover:shadow-lg">
|
||||
<Card.Header>
|
||||
<Image
|
||||
src={`https://cms.retroui.dev${blog.featuredImage?.url}`}
|
||||
alt={blog.title}
|
||||
width={600}
|
||||
height={400}
|
||||
className="mb-6 h-50"
|
||||
/>
|
||||
<Text as="p" className="mb-2 text-sm text-muted-foreground">
|
||||
{format(new Date(blog.publishedAt), "dd, MMMM yyyy")}
|
||||
</Text>
|
||||
<Card.Title className="line-clamp-2 text-2xl">{blog.title}</Card.Title>
|
||||
|
||||
<Card.Description className="line-clamp-2 text-sm">{blog.excerpt}</Card.Description>
|
||||
</Card.Header>
|
||||
{/* <Card.Content className="pt-0">
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar className="h-8 w-8 border">
|
||||
<Avatar.Image src="https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/arif.jpg" alt="Arif Hossain" />
|
||||
<Avatar.Fallback>AH</Avatar.Fallback>
|
||||
</Avatar>
|
||||
<Text className="text-sm">Arif Hossain</Text>
|
||||
</div>
|
||||
</Card.Content> */}
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default page;
|
||||
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Text,
|
||||
Input,
|
||||
Textarea,
|
||||
Card,
|
||||
Avatar,
|
||||
Badge,
|
||||
CommandDisplay,
|
||||
} from "@/components/retroui";
|
||||
import AccordionStyleDefault from "@/preview/components/accordion-style-default";
|
||||
import AlertStyleDefaultIcon from "@/preview/components/alert-style-with-icon";
|
||||
import AvatarStyleCircle from "@/preview/components/avatar-style-circle-sizes";
|
||||
import BadgeStyleVariants from "@/preview/components/badge-style-variants";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ChartArea,
|
||||
GithubIcon,
|
||||
MessageCircle,
|
||||
PaintbrushIcon,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import Footer from "@/components/footer";
|
||||
import WorksWithSection from "@/components/WorksWithSection";
|
||||
|
||||
async function getContributors(): Promise<
|
||||
{ avatar: string; username: string; url: string }[]
|
||||
> {
|
||||
const request = await fetch(
|
||||
`https://api.github.com/repos/logging-studio/RetroUI/contributors`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const contributorsList = await request.json();
|
||||
return contributorsList.map(
|
||||
(c: { avatar_url: string; login: string; html_url: string }) => ({
|
||||
avatar: c.avatar_url,
|
||||
username: c.login,
|
||||
url: c.html_url,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export default async function Home() {
|
||||
const contributors = await getContributors();
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="bg-image bg-cover bg-no-repeat bg-center flex flex-col items-center min-h-screen">
|
||||
<section className="container max-w-6xl mx-auto px-4 lg:px-0 text-gray-900 flex justify-center items-center lg:gap-28 my-28">
|
||||
<div className="text-center lg:text-left w-full lg:w-2/3">
|
||||
<Link href="https://neo.retroui.dev" target="_blank" className="mb-4 inline-block">
|
||||
<Badge>
|
||||
Introducing Neo/SS
|
||||
<PaintbrushIcon className="ml-2 h-4 w-4 inline-block" />
|
||||
</Badge>
|
||||
</Link>
|
||||
|
||||
<Text as="h1" className="text-5xl text-foreground lg:text-6xl mt-8">
|
||||
Make your projects
|
||||
<br />
|
||||
<span className="text-outlined">stand out!</span>
|
||||
</Text>
|
||||
|
||||
<p className="text-lg text-muted-foreground mb-8 mt-4">
|
||||
A NeoBrutalism styled React + TailwindCSS UI library for building bold, modern web apps.
|
||||
Perfect for any project using Shadcn/ui.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center gap-4">
|
||||
<Link href="/docs" passHref>
|
||||
<Button
|
||||
className="w-full"
|
||||
aria-label="Get Started with RetroUI"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="w-full max-w-72 shadow shadow-primary">
|
||||
<CommandDisplay command='npx shadcn add @retroui/button' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:block lg:w-1/3">
|
||||
<Image
|
||||
alt="retro tv radio"
|
||||
src="/images/tv_radio.png"
|
||||
layout="responsive"
|
||||
width={500}
|
||||
height={500}
|
||||
className="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className="container max-w-6xl mx-auto px-4 lg:px-0 lg:my-36">
|
||||
{/* <Text as="h2" className="mb-16 text-center">
|
||||
Old school with modern twist! ✨
|
||||
</Text> */}
|
||||
<div className="grid gap-6 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 mb-8">
|
||||
<Card className="w-full bg-background shadow-none">
|
||||
<Card.Header>
|
||||
<Card.Title>Button</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Button>Primary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card className="w-full bg-background shadow-none">
|
||||
<Card.Header>
|
||||
<Card.Title>Badge</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<BadgeStyleVariants />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card className="w-full bg-background shadow-none">
|
||||
<Card.Header>
|
||||
<Card.Title>Avatar</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AvatarStyleCircle />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card className="w-full bg-background shadow-none">
|
||||
<Card.Header>
|
||||
<Card.Title>Accordion</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AccordionStyleDefault />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card className="w-full bg-background shadow-none">
|
||||
<Card.Header>
|
||||
<Card.Title>Input & Textarea</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Input />
|
||||
<div className="h-4"></div>
|
||||
<Textarea className="border-foreground" />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card className="w-full bg-background shadow-none">
|
||||
<Card.Header>
|
||||
<Card.Title>Alert</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AlertStyleDefaultIcon />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="container max-w-6xl mx-auto px-4 lg:px-0 flex flex-col lg:flex-row gap-12 lg:gap-16 lg:grid-cols-2 my-36">
|
||||
<div className="w-full lg:w-3/5">
|
||||
<Text as="h2">
|
||||
Easily <span className="text-outlined">Customize</span> to Your Own
|
||||
Needs! 🛠️
|
||||
</Text>
|
||||
<div className="flex flex-col space-y-1 mt-6 mb-8 text-muted-foreground">
|
||||
<Text className="text-lg">
|
||||
Copy-Paste Ready: Components that you can just copy paste.
|
||||
</Text>
|
||||
<Text className="text-lg">
|
||||
Tailwind Based: Customizable with Tailwind CSS.
|
||||
</Text>
|
||||
<Text className="text-lg">
|
||||
Type Safe: Typescript support for all components.
|
||||
</Text>
|
||||
</div>
|
||||
<Link href="/docs/components/button" passHref>
|
||||
<Button>See Examples</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="w-full lg:w-2/5">
|
||||
<Image
|
||||
src="/images/code_show.svg"
|
||||
width={600}
|
||||
height={400}
|
||||
alt="retroui code showcase"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="container max-w-6xl max-lg:px-4 mx-auto bg-[url('/images/starts_bg.svg')] bg-cover bg-no-repeat py-12">
|
||||
<Text as="h2" className="text-center mb-28 max-w-2xl mx-auto">
|
||||
A <span className="text-outlined">Growing</span> Community of Developers and Designers.
|
||||
</Text>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 max-w-3xl mx-auto">
|
||||
<div className="flex flex-col items-start relative mx-auto">
|
||||
<Text as="h3" className="mb-2 font-sans">
|
||||
Github Stars
|
||||
</Text>
|
||||
<Text className="text-outlined text-7xl lg:text-8xl font-head">
|
||||
1,200+
|
||||
</Text>
|
||||
<Image
|
||||
src="/images/shooting_star.svg"
|
||||
width={120}
|
||||
height={120}
|
||||
alt="shotting stars"
|
||||
className="absolute -top-[80px] -left-[80px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-start relative mx-auto">
|
||||
<Text as="h3" className="mb-2 font-sans">
|
||||
Discord Members
|
||||
</Text>
|
||||
<Text className="text-outlined text-7xl lg:text-8xl font-head">
|
||||
100+
|
||||
</Text>
|
||||
<Image
|
||||
src="/images/shooting_star.svg"
|
||||
width={120}
|
||||
height={120}
|
||||
alt="shotting stars"
|
||||
className="absolute -right-[80px] -bottom-[80px] rotate-180"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<WorksWithSection />
|
||||
|
||||
<section className="container max-w-6xl mx-auto border-2 bg-primary border-black py-16 px-4 lg:p-16 my-36">
|
||||
<Text as="h2" className="text-center text-black mb-2">
|
||||
Community Contributors
|
||||
</Text>
|
||||
<Text className="text-xl text-center text-black mb-8">
|
||||
RetroUI core is free and open-source, and it is made possible by our
|
||||
awesome contributors.
|
||||
</Text>
|
||||
<div className="max-w-2xl mx-auto flex flex-wrap justify-center gap-2 lg:gap-4">
|
||||
{contributors.map((contributor) => (
|
||||
<Link
|
||||
key={contributor.username}
|
||||
href={contributor.url}
|
||||
target="_blank"
|
||||
passHref
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<Avatar className="h-10 w-10 border-black lg:h-16 lg:w-16">
|
||||
<Avatar.Image
|
||||
src={contributor.avatar}
|
||||
alt={contributor.username}
|
||||
/>
|
||||
</Avatar>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row items-center justify-center gap-4 mt-12">
|
||||
<Link
|
||||
href="https://github.com/logging-studio/retroui"
|
||||
target="_blank"
|
||||
passHref
|
||||
>
|
||||
<Button
|
||||
className="bg-background"
|
||||
variant="outline"
|
||||
>
|
||||
<GithubIcon size="16" className="mr-2" />
|
||||
Star on Github
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="https://discord.gg/Jum3NJxK6Q" target="_blank" passHref>
|
||||
<Button
|
||||
className="bg-background"
|
||||
variant="outline"
|
||||
>
|
||||
<MessageCircle size="16" className="mr-2" />
|
||||
Join Community
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto px-4">
|
||||
<div className="container max-w-6xl h-full mx-auto py-16 flex flex-col justify-between gap-20">
|
||||
<div className="flex flex-col md:flex-row items-center gap-10">
|
||||
<div className="w-full md:w-3/5">
|
||||
<Text as="h1" className="text-4xl lg:text-5xl">
|
||||
Ship <span className="text-outlined">Faster</span> With Blocks
|
||||
<br />
|
||||
and Templates 🚀
|
||||
</Text>
|
||||
<Text
|
||||
as="p"
|
||||
className="text-muted-foreground font-medium text-xl mt-2 mb-8"
|
||||
>
|
||||
Get access to 100+ premium blocks, templates and figma kit,
|
||||
ready to make your project stand out.
|
||||
</Text>
|
||||
|
||||
<Link href="https://pro.retroui.dev/blocks" target="_blank">
|
||||
<Button>Explore RetroUI Pro</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-2/5">
|
||||
<Image
|
||||
src="/images/pro_showcase.svg"
|
||||
width={1920}
|
||||
height={1080}
|
||||
alt="retroui pro ui blocks showcase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import showcaseData from "@/showcase.json";
|
||||
import { Card, Text, Button } from "@/components/retroui";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import Footer from "@/components/footer";
|
||||
|
||||
export default function ShowcasePage() {
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-6xl mx-auto py-12 px-4 lg:px-0">
|
||||
<div className="mb-12 flex flex-col gap-2 items-start">
|
||||
<Text as="h1">
|
||||
Showcase
|
||||
</Text>
|
||||
<Text className="text-lg text-muted-foreground mb-4">
|
||||
Build something cool using RetroUI? Share it with the community.
|
||||
</Text>
|
||||
<Button asChild>
|
||||
<Link href="https://github.com/Logging-Studio/RetroUI/discussions/102" target="_blank">
|
||||
Add Your Project
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{Object.entries(showcaseData).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
<Card className="hover:shadow">
|
||||
<Card.Header>
|
||||
<Image
|
||||
src={value.cover}
|
||||
alt={value.name}
|
||||
width={600}
|
||||
height={400}
|
||||
className="mb-6 border-2 h-[200px] w-full object-fit border-muted-foreground"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Card.Title>{value.name}</Card.Title>
|
||||
|
||||
<Button asChild size="sm" variant="secondary">
|
||||
<Link href={value.url} target="_blank" rel="noopener noreferrer">
|
||||
Visit <ArrowUpRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Card,
|
||||
Select,
|
||||
Text,
|
||||
} from "@/components/retroui";
|
||||
import ChartStyleMultiple from "@/preview/charts/area-chart-style-multiple";
|
||||
import CourseCard from "@/preview/blocks/course-card";
|
||||
import SignInBlock from "@/preview/blocks/sign-in";
|
||||
import BarChartStyleMultiple from "@/preview/charts/bar-chart-style-multiple";
|
||||
import TeamMembersBlock from "@/preview/blocks/team-members";
|
||||
import CopyTheme from "@/components/CopyTheme";
|
||||
import { Theme } from "@/config/theme";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
const themes: {value: Theme, label: string}[] = [
|
||||
{
|
||||
value: Theme.Default,
|
||||
label: "Default",
|
||||
},
|
||||
{
|
||||
value: Theme.Purple,
|
||||
label: "Purple",
|
||||
},
|
||||
{
|
||||
value: Theme.Lime,
|
||||
label: "Lime",
|
||||
},
|
||||
{
|
||||
value: Theme.Red,
|
||||
label: "Red",
|
||||
},
|
||||
{
|
||||
value: Theme.Lavender,
|
||||
label: "Lavender",
|
||||
},
|
||||
{
|
||||
value: Theme.Orange,
|
||||
label: "Orange",
|
||||
},
|
||||
{
|
||||
value: Theme.Green,
|
||||
label: "Green",
|
||||
},
|
||||
];
|
||||
|
||||
const variants = [
|
||||
{
|
||||
value: "box",
|
||||
label: "Box",
|
||||
},
|
||||
{
|
||||
value: "with-radius",
|
||||
label: "Rounded",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Themes() {
|
||||
const { colorTheme, variant, setColorTheme, setVariant } = useTheme();
|
||||
|
||||
return (
|
||||
<section className={`theme-${colorTheme} ${variant} container max-w-8xl mx-auto py-12`}>
|
||||
<Text as="h1" className="font-normal mb-12">Don't like <span className="text-primary">{colorTheme === Theme.Default ? "Yellow" : colorTheme.slice(0, 1).toUpperCase() + colorTheme.slice(1)}?</span><br />Well, we have more! 🫡</Text>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-6 items-center justify-between mb-6">
|
||||
<div className="flex gap-6">
|
||||
<Select value={colorTheme} onValueChange={(value: Theme) => setColorTheme(value)}>
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Select Color" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
{
|
||||
themes.map((theme) => (
|
||||
<Select.Item key={theme.value} value={theme.value}>
|
||||
{theme.label}
|
||||
</Select.Item>
|
||||
))
|
||||
}
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
|
||||
<Select value={variant} onValueChange={(value) => setVariant(value)}>
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Select Variant" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
{
|
||||
variants.map((variant) => (
|
||||
<Select.Item key={variant.value} value={variant.value}>
|
||||
{variant.label}
|
||||
</Select.Item>
|
||||
))
|
||||
}
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<CopyTheme theme={colorTheme} hasRadius={variant === "with-radius"} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 w-full gap-6">
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card className="w-full border-muted bg-background shadow-none">
|
||||
<Card.Content>
|
||||
<CourseCard />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card className="w-full border-muted bg-background shadow-none">
|
||||
<Card.Content>
|
||||
<TeamMembersBlock />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card className="w-full border-muted bg-background shadow-none">
|
||||
<Card.Content>
|
||||
<ChartStyleMultiple />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<Card className="w-full border-muted bg-background shadow-none">
|
||||
<Card.Content>
|
||||
<BarChartStyleMultiple />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
<Card className="w-full border-muted bg-background shadow-none">
|
||||
<Card.Content>
|
||||
<SignInBlock />
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { TableOfContents } from "@/components/retroui/TableOfContents";
|
||||
|
||||
export default function page() {
|
||||
return (
|
||||
<div className="container max-w-6xl mx-auto space-y-6 flex">
|
||||
|
||||
<div className="flex-1">
|
||||
<h1>Table of Contents</h1>
|
||||
<h1>Table of Contents 1</h1>
|
||||
<div className="h-80"></div>
|
||||
<h2>Table of Contents 2</h2>
|
||||
<div className="h-80"></div>
|
||||
|
||||
<h3>Table of Contents 3</h3>
|
||||
<h3>Table of Contents 3</h3>
|
||||
<h3>Table of Contents 3</h3>
|
||||
<div className="h-80"></div>
|
||||
<h4>Table of Contents 4</h4>
|
||||
<h4>Table of Contents 4</h4>
|
||||
<h4>Table of Contents 4</h4>
|
||||
<div className="h-80"></div>
|
||||
|
||||
<h5>Table of Contents 5</h5>
|
||||
<h6>Table of Contents 6</h6>
|
||||
</div>
|
||||
<TableOfContents />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use server";
|
||||
|
||||
import { Resend } from "resend";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
export async function subscribeToNewsletter(email: string) {
|
||||
try {
|
||||
const { data, error } = await resend.contacts.create({
|
||||
email,
|
||||
audienceId: process.env.RESEND_AUDIENCE_ID!,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Failed to subscribe",
|
||||
};
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,494 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@theme inline {
|
||||
--font-head: var(--font-head);
|
||||
--font-sans: var(--font-sans);
|
||||
--radius: var(--radius);
|
||||
--shadow-xs: 1px 1px 0 0 var(--border);
|
||||
--shadow-sm: 2px 2px 0 0 var(--border);
|
||||
--shadow: 3px 3px 0 0 var(--border);
|
||||
--shadow-md: 4px 4px 0 0 var(--border);
|
||||
--shadow-lg: 6px 6px 0 0 var(--border);
|
||||
--shadow-xl: 10px 10px 0 1px var(--border);
|
||||
--shadow-2xl: 16px 16px 0 1px var(--border);
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-primary-hover: var(--primary-hover);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--background-image: var(--background-image);
|
||||
--chart-1: var(--chart-1);
|
||||
--chart-2: var(--chart-2);
|
||||
--chart-3: var(--chart-3);
|
||||
--chart-4: var(--chart-4);
|
||||
--chart-5: var(--chart-5);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0;
|
||||
--background: #fff;
|
||||
--foreground: #000;
|
||||
--card: #fff;
|
||||
--card-foreground: #000;
|
||||
--primary: #ffdb33;
|
||||
--primary-hover: #ffcc00;
|
||||
--primary-foreground: #000;
|
||||
--secondary: #000;
|
||||
--secondary-foreground: #fff;
|
||||
--muted: #d5d5d5;
|
||||
--muted-foreground: #5a5a5a;
|
||||
--accent: #fae583;
|
||||
--accent-foreground: #000;
|
||||
--destructive: #e63946;
|
||||
--destructive-foreground: #fff;
|
||||
--border: #000;
|
||||
--background-image: url("/images/banner_void_2.svg");
|
||||
--chart-1: #c4a1ff;
|
||||
--chart-2: #01ffcc;
|
||||
--chart-3: #e7f192;
|
||||
--chart-4: #000;
|
||||
--chart-5: #ff30cd;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #1a1a1a;
|
||||
--foreground: #f5f5f5;
|
||||
--card: #242424;
|
||||
--card-foreground: #f5f5f5;
|
||||
--primary: #ffdb33;
|
||||
--primary-hover: #ffcc00;
|
||||
--primary-foreground: #000;
|
||||
--secondary: #3a3a3a;
|
||||
--secondary-foreground: #f5f5f5;
|
||||
--muted: #3f3f46;
|
||||
--muted-foreground: #a0a0a0;
|
||||
--accent: #fae583;
|
||||
--accent-foreground: #000;
|
||||
--destructive: #e63946;
|
||||
--destructive-foreground: #fff;
|
||||
--border: #5c5c5c;
|
||||
--background-image: url("/images/bg_void_3.svg");
|
||||
--chart-1: #ffdb33;
|
||||
--chart-2: #000;
|
||||
--chart-3: #aeaeae;
|
||||
--chart-4: #fae583;
|
||||
--chart-5: #e63946;
|
||||
}
|
||||
|
||||
.with-radius {
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-purple {
|
||||
--background: #f5f5f5;
|
||||
--foreground: #1a1a1a;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #f5f5f5;
|
||||
--primary: #5F4FE6;
|
||||
--primary-hover: #4938C2;
|
||||
--primary-foreground: #fff;
|
||||
--secondary: #3a3a3a;
|
||||
--secondary-foreground: #f5f5f5;
|
||||
--muted: #CFCCEA;
|
||||
--muted-foreground: #5B5686;
|
||||
--accent: #FED13B;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #EF4444;
|
||||
--destructive-foreground: #fff;
|
||||
--border: #3a3a3a;
|
||||
--background-image: url("/images/bg_void_3.svg");
|
||||
--chart-1: #ffdb33;
|
||||
--chart-2: #000;
|
||||
--chart-3: #aeaeae;
|
||||
--chart-4: #fae583;
|
||||
--chart-5: #e63946;
|
||||
}
|
||||
|
||||
.dark .theme-purple {
|
||||
--background: #0f0f12;
|
||||
--foreground: #f5f5f5;
|
||||
--card: #1a1a1d;
|
||||
--card-foreground: #eaeaea;
|
||||
--primary: #7b6df5;
|
||||
--primary-hover: #5F4FE6;
|
||||
--primary-foreground: #fff;
|
||||
--secondary: #2a2a2e;
|
||||
--secondary-foreground: #eaeaea;
|
||||
--muted: #3d395a;
|
||||
--muted-foreground: #a49fce;
|
||||
--accent: #FED13B;
|
||||
--accent-foreground: #000;
|
||||
--destructive: #EF4444;
|
||||
--destructive-foreground: #fff;
|
||||
--border: #2e2e32;
|
||||
}
|
||||
|
||||
.theme-lime {
|
||||
--background: #FFFFFF;
|
||||
--foreground: #000000;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--input: #FFFFFF;
|
||||
--border: #000000;
|
||||
--ring: #AAFC3D;
|
||||
--primary: #AAFC3D;
|
||||
--primary-hover: #97f819;
|
||||
--primary-foreground: #000000;
|
||||
--secondary: #000000;
|
||||
--secondary-foreground: #FFFFFF;
|
||||
--muted: #F3F3F3;
|
||||
--muted-foreground: #444444;
|
||||
--accent: #FF4FD8;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FF0033;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
}
|
||||
|
||||
|
||||
.dark .theme-lime {
|
||||
--background: #0f0f0f;
|
||||
--foreground: #f5f5f5;
|
||||
--card: #1a1a1a;
|
||||
--card-foreground: #FFFFFF;
|
||||
--input: #2a2a2a;
|
||||
--border: #333333;
|
||||
--ring: #AAFC3D;
|
||||
--primary: #AAFC3D;
|
||||
--primary-hover: #97f819;
|
||||
--primary-foreground: #000000;
|
||||
--secondary: #FFFFFF;
|
||||
--secondary-foreground: #000000;
|
||||
--muted: #2a2a2a;
|
||||
--muted-foreground: #aaaaaa;
|
||||
--accent: #FF4FD8;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FF0033;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
}
|
||||
|
||||
.theme-red {
|
||||
--background: #FCFFE7;
|
||||
--foreground: #000000;
|
||||
--muted: #EFD0D5;
|
||||
--muted-foreground: #A42439;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #000000;
|
||||
--border: #000000;
|
||||
--input: #FFFFFF;
|
||||
--primary: #EA435F;
|
||||
--primary-hover: #D00000;
|
||||
--primary-foreground: #FFFFFF;
|
||||
--secondary: #FFDA5C;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #CEEBFC;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #D00000;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
--ring: #000000;
|
||||
}
|
||||
|
||||
.dark .theme-red {
|
||||
--background: #0f0f0f;
|
||||
--foreground: #f5f5f5;
|
||||
--muted: #3a1f24;
|
||||
--muted-foreground: #f2a7b2;
|
||||
--card: #1a1a1a;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #1a1a1a;
|
||||
--popover-foreground: #ffffff;
|
||||
--border: #2a2a2a;
|
||||
--input: #2a2a2a;
|
||||
--primary: #EA435F;
|
||||
--primary-hover: #D00000;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #FFDA5C;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #2a3b45;
|
||||
--accent-foreground: #CEEBFC;
|
||||
--destructive: #D00000;
|
||||
--destructive-foreground: #ffffff;
|
||||
--ring: #EA435F;
|
||||
}
|
||||
|
||||
.theme-lavender {
|
||||
--background: #F9F5F2;
|
||||
--foreground: #000000;
|
||||
--muted: #EDE3FF;
|
||||
--muted-foreground: #5B3B98;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #000000;
|
||||
--border: #000000;
|
||||
--input: #FFFFFF;
|
||||
--primary: #C4A1FF;
|
||||
--primary-hover: #9678FF;
|
||||
--primary-foreground: #000000;
|
||||
--secondary: #E7F193;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #FE91E9;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FE3B3B;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
--ring: #000000;
|
||||
}
|
||||
|
||||
.dark .theme-lavender {
|
||||
--background: #121014;
|
||||
--foreground: #f5f5f5;
|
||||
--muted: #2d2440;
|
||||
--muted-foreground: #c7b6f3;
|
||||
--card: #1a1820;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #1a1820;
|
||||
--popover-foreground: #ffffff;
|
||||
--border: #2a2a2e;
|
||||
--input: #2a2a2e;
|
||||
--primary: #C4A1FF;
|
||||
--primary-hover: #9678FF;
|
||||
--primary-foreground: #000000;
|
||||
--secondary: #E7F193;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #FE91E9;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FE3B3B;
|
||||
--destructive-foreground: #ffffff;
|
||||
--ring: #C4A1FF;
|
||||
}
|
||||
|
||||
.theme-orange {
|
||||
--background: #FFFFFF;
|
||||
--foreground: #000000;
|
||||
--muted: #FFE1C7;
|
||||
--muted-foreground: #8C3D00;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #000000;
|
||||
--border: #000000;
|
||||
--input: #FFFFFF;
|
||||
--primary: #F07200;
|
||||
--primary-hover: #ff8011;
|
||||
--primary-foreground: #FDEAD9;
|
||||
--secondary: #EF8C27;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #FF6B6B;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FE3B3B;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
--ring: #000000;
|
||||
}
|
||||
|
||||
.dark .theme-orange {
|
||||
--background: #12100e;
|
||||
--foreground: #f5f5f5;
|
||||
--muted: #3a2617;
|
||||
--muted-foreground: #ffb679;
|
||||
--card: #1a1a1a;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #1a1a1a;
|
||||
--popover-foreground: #ffffff;
|
||||
--border: #2a2a2a;
|
||||
--input: #2a2a2a;
|
||||
--primary: #F07200;
|
||||
--primary-hover: #ff8011;
|
||||
--primary-foreground: #FDEAD9;
|
||||
--secondary: #EF8C27;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #FF6B6B;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FE3B3B;
|
||||
--destructive-foreground: #ffffff;
|
||||
--ring: #F07200;
|
||||
}
|
||||
|
||||
.theme-green {
|
||||
--background: #FFFFFF;
|
||||
--foreground: #000000;
|
||||
--muted: #CBE9D8;
|
||||
--muted-foreground: #1F7751;
|
||||
--card: #F9F5F2;
|
||||
--card-foreground: #000000;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #000000;
|
||||
--border: #000000;
|
||||
--input: #FFFFFF;
|
||||
--primary: #599D77;
|
||||
--primary-hover: #39654c;
|
||||
--primary-foreground: #FFFFFF;
|
||||
--secondary: #A3D9B1;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #FFE75A;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #F05D5D;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
--ring: #000000;
|
||||
}
|
||||
|
||||
.dark .theme-green {
|
||||
--background: #0f1210;
|
||||
--foreground: #f5f5f5;
|
||||
--muted: #1f3a2d;
|
||||
--muted-foreground: #9cd9b8;
|
||||
--card: #181c19;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #181c19;
|
||||
--popover-foreground: #ffffff;
|
||||
--border: #2a2e2b;
|
||||
--input: #2a2e2b;
|
||||
--primary: #599D77;
|
||||
--primary-hover: #39654c;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #3a6f52;
|
||||
--secondary-foreground: #ffffff;
|
||||
--accent: #FFE75A;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #F05D5D;
|
||||
--destructive-foreground: #ffffff;
|
||||
--ring: #599D77;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
|
||||
/* Firefox scrollbar */
|
||||
html {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: var(--muted) var(--foreground);
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
background-image: var(--background-image);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.text-outlined {
|
||||
letter-spacing: 2px;
|
||||
text-shadow:
|
||||
0px 0px 0 var(--primary),
|
||||
0px 0px 0 var(--primary),
|
||||
4px 4px 0 var(--primary),
|
||||
4px 4px 0 var(--primary);
|
||||
}
|
||||
|
||||
/* Syntax Highlighting stuff */
|
||||
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
background: #222;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.hljs-subst {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.hljs-section {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote,
|
||||
.hljs-meta {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-regexp {
|
||||
color: #ffcc33;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-addition {
|
||||
color: #00cc66;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-link {
|
||||
color: #32aaee;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #6644aa;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-variable,
|
||||
.hljs-deletion,
|
||||
.hljs-template-tag {
|
||||
color: #bb1166;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-doctag,
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for sidebar */
|
||||
.sidebar-scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
transition: scrollbar-color 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-scroll:hover {
|
||||
scrollbar-color: var(--color-primary) transparent;
|
||||
}
|
||||
|
||||
/* Webkit browsers (Chrome, Safari, Edge) */
|
||||
.sidebar-scroll::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.sidebar-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-scroll::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import TopNav from "@/components/TopNav";
|
||||
import "./global.css";
|
||||
import { Archivo_Black, Space_Grotesk, Space_Mono } from "next/font/google";
|
||||
import { Metadata } from "next";
|
||||
import { Toaster } from "@/components/retroui";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import ThemeWrapper from "@/components/ThemeWrapper";
|
||||
|
||||
const sans = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
weight: "400",
|
||||
variable: "--font-sans",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const head = Archivo_Black({
|
||||
subsets: ["latin"],
|
||||
weight: "400",
|
||||
variable: "--font-head",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const mono = Space_Mono({
|
||||
subsets: ["latin"],
|
||||
weight: "400",
|
||||
variable: "--font-mono",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "NeoBrutalism Styled React Components | RetroUI",
|
||||
description:
|
||||
"RetroUI - NeoBrutalism styled component library built with React and TailwindCSS for modern web apps.",
|
||||
openGraph: {
|
||||
images: "https://retroui.dev/banner.png",
|
||||
title: "NeoBrutalism Styled React Components | RetroUI",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
const darkMode = localStorage.getItem('darkMode');
|
||||
if (darkMode === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error applying theme:', e);
|
||||
}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{/* <script
|
||||
defer
|
||||
src="https://cloud.umami.is/script.js"
|
||||
data-website-id="97dd6182-c656-4265-97e0-ee9613b88078"
|
||||
/> */}
|
||||
<script
|
||||
defer
|
||||
data-website-id="dfid_zlQSgC5h2RNuCLHQTf7Fd"
|
||||
data-domain="retroui.dev"
|
||||
src="https://datafa.st/js/script.js">
|
||||
</script>
|
||||
</head>
|
||||
<body
|
||||
className={`${head.variable} ${sans.variable} ${mono.variable}`}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<div className="bg-background text-foreground">
|
||||
<TopNav />
|
||||
{children}
|
||||
<Toaster />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
<SpeedInsights />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
sitemap: "https://retroui.dev/sitemap.xml",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { generateRegistryRssFeed } from "@wandry/analytics-sdk";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const baseUrl = new URL(request.url).origin;
|
||||
|
||||
const rssXml = await generateRegistryRssFeed({
|
||||
baseUrl,
|
||||
componentsUrl: "/docs/components",
|
||||
excludeItemTypes: ["registry:block", "registry:style", "registry:lib"],
|
||||
rss: {
|
||||
title: "@retroui",
|
||||
description: "Subscribe to @retroui updates",
|
||||
link: "https://retroui.dev",
|
||||
pubDateStrategy: "githubLastEdit",
|
||||
},
|
||||
github: {
|
||||
owner: "Logging-Studio",
|
||||
repo: "RetroUI",
|
||||
token: process.env.GITHUB_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
if (!rssXml) {
|
||||
return new Response("RSS feed not available", {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(rssXml, {
|
||||
headers: {
|
||||
"Content-Type": "application/rss+xml; charset=utf-8",
|
||||
"Cache-Control":
|
||||
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { allDocs } from "@/.contentlayer/generated";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: "https://retroui.dev",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 1,
|
||||
},
|
||||
...allDocs.map((doc) => ({
|
||||
url: `https://retroui.dev${doc.url}`,
|
||||
lastModified: doc.lastUpdated,
|
||||
})),
|
||||
];
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/retroui";
|
||||
|
||||
interface ICodeBlock extends React.HTMLAttributes<HTMLPreElement> {}
|
||||
|
||||
export async function copyToClipboardWithMeta(value: string, event?: Event) {
|
||||
navigator.clipboard.writeText(value);
|
||||
if (event) {
|
||||
// trackEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
export function CodeBlock({ className, children, ...props }: ICodeBlock) {
|
||||
const [hasCopied, setHasCopied] = React.useState(false);
|
||||
const preRef = React.useRef<HTMLPreElement>(null);
|
||||
|
||||
const handleClickCopy = async () => {
|
||||
const code = preRef.current?.textContent;
|
||||
if (code) {
|
||||
setHasCopied(true);
|
||||
await navigator.clipboard.writeText(code);
|
||||
|
||||
setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<pre
|
||||
className={cn(
|
||||
"overflow-x-auto rounded-sm bg-[#282A36] mt-3 mb-6 p-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Button
|
||||
id="cody-copy-button"
|
||||
data-umami-event="copy-code-button"
|
||||
disabled={hasCopied}
|
||||
className="absolute top-4 right-4 z-10"
|
||||
size="sm"
|
||||
onClick={handleClickCopy}
|
||||
>
|
||||
{hasCopied ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
<span ref={preRef}>{children}</span>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import React from "react";
|
||||
import { HTMLAttributes } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "./retroui";
|
||||
|
||||
interface IComponentShowcase extends HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
const CopyableCommand = ({ command }: { command: string }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 group">
|
||||
<code className="flex-1">{command}</code>
|
||||
<Button size="sm" onClick={copyToClipboard} className="hidden md:block" title="Copy to clipboard">
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
<Button className="md:hidden" size="icon" onClick={copyToClipboard} title="Copy to clipboard mobile">
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function CliCommand({
|
||||
npmCommand,
|
||||
yarnCommand,
|
||||
pnpmCommand,
|
||||
bunCommand,
|
||||
}: {
|
||||
npmCommand: string;
|
||||
yarnCommand?: string;
|
||||
pnpmCommand?: string;
|
||||
bunCommand?: string;
|
||||
}) {
|
||||
const isNpx = npmCommand.includes("npx");
|
||||
if (isNpx) {
|
||||
pnpmCommand = pnpmCommand ?? npmCommand.replace("npx", "pnpm dlx");
|
||||
yarnCommand = yarnCommand ?? npmCommand.replace("npx", "yarn dlx");
|
||||
bunCommand = bunCommand ?? npmCommand.replace("npx", "bunx");
|
||||
} else {
|
||||
pnpmCommand = pnpmCommand ?? npmCommand.replace("npm", "pnpm");
|
||||
yarnCommand = yarnCommand ?? npmCommand.replace("npm install", "yarn add");
|
||||
bunCommand = bunCommand ?? npmCommand.replace("npm", "bun");
|
||||
}
|
||||
|
||||
return (
|
||||
<TabGroup className="p-4 my-2 bg-gray-800 rounded-md text-background/90">
|
||||
<TabList className="flex space-x-4 mb-6 text-sm">
|
||||
<Tab className="cursor-pointer text-gray-400 relative px-2 py-1 bg-transparent data-selected:border-b-2 border-accent data-selected:text-white focus:outline-hidden">
|
||||
pnpm
|
||||
</Tab>
|
||||
<Tab className="cursor-pointer text-gray-400 relative px-2 py-1 bg-transparent data-selected:border-b-2 border-accent data-selected:text-white focus:outline-hidden">
|
||||
npm
|
||||
</Tab>
|
||||
<Tab className="cursor-pointer text-gray-400 relative px-2 py-1 bg-transparent data-selected:border-b-2 border-accent data-selected:text-white focus:outline-hidden">
|
||||
yarn
|
||||
</Tab>
|
||||
<Tab className="cursor-pointer text-gray-400 relative px-2 py-1 bg-transparent data-selected:border-b-2 border-accent data-selected:text-white focus:outline-hidden">
|
||||
bun
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels className="text-sm text-accent">
|
||||
<TabPanel>
|
||||
<CopyableCommand command={pnpmCommand} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CopyableCommand command={npmCommand} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CopyableCommand command={yarnCommand} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CopyableCommand command={bunCommand} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function ComponentInstallCli({
|
||||
npmCommand,
|
||||
yarnCommand,
|
||||
pnpmCommand,
|
||||
bunCommand,
|
||||
}: {
|
||||
npmCommand: string;
|
||||
yarnCommand?: string;
|
||||
pnpmCommand?: string;
|
||||
bunCommand?: string;
|
||||
}) {
|
||||
return (
|
||||
<TabPanel>
|
||||
<CliCommand
|
||||
npmCommand={npmCommand}
|
||||
yarnCommand={yarnCommand}
|
||||
pnpmCommand={pnpmCommand}
|
||||
bunCommand={bunCommand}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function ComponentInstallManual({ children }: { children: React.ReactNode }) {
|
||||
return <TabPanel>{children}</TabPanel>;
|
||||
}
|
||||
|
||||
function ComponentInstall({ children }: IComponentShowcase) {
|
||||
return (
|
||||
<TabGroup>
|
||||
<TabList className="border-b bg-transparent flex space-x-4 mb-6 text-sm">
|
||||
<Tab className="min-w-12 cursor-pointer relative px-2 py-1 bg-transparent data-selected:border data-selected:border-b-0 data-selected:bg-black data-selected:text-white focus:outline-hidden">
|
||||
CLI
|
||||
</Tab>
|
||||
<Tab className="min-w-12 cursor-pointer relative px-2 py-1 bg-transparent data-selected:border data-selected:border-b-0 data-selected:bg-black data-selected:text-white focus:outline-hidden">
|
||||
Manual
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>{children}</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(ComponentInstall, {
|
||||
Cli: ComponentInstallCli,
|
||||
Manual: ComponentInstallManual,
|
||||
});
|
||||
|
||||
export { ComponentInstall };
|
||||
@@ -0,0 +1,64 @@
|
||||
import { componentConfig } from "@/config";
|
||||
import { TabGroup, TabList, TabPanels, TabPanel, Tab } from "@headlessui/react";
|
||||
import React, { HTMLAttributes } from "react";
|
||||
import { Button } from "./retroui";
|
||||
|
||||
interface IComponentShowcase extends HTMLAttributes<HTMLDivElement> {
|
||||
name: keyof typeof componentConfig.examples;
|
||||
}
|
||||
|
||||
export function ComponentShowcase({ name, children }: IComponentShowcase) {
|
||||
const { preview: Preview } = componentConfig.examples[name];
|
||||
const Code = React.Children.toArray(children)[0];
|
||||
|
||||
return (
|
||||
<TabGroup>
|
||||
<TabList className="space-x-4 border-b mb-6 text-sm">
|
||||
<Tab className="min-w-12 cursor-pointer relative px-2 py-1 bg-transparent data-selected:border data-selected:border-b-0 data-selected:bg-black data-selected:text-white focus:outline-hidden">
|
||||
Preview
|
||||
</Tab>
|
||||
<Tab className="min-w-12 cursor-pointer relative px-2 py-1 bg-transparent data-selected:border data-selected:border-b-0 data-selected:bg-black data-selected:text-white focus:outline-hidden">
|
||||
Code
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<div className="border rounded-sm px-6 py-24 flex justify-center items-center min-h-80 mt-4 relative">
|
||||
<a
|
||||
href={`https://v0.dev/chat/api/open?url=https://retroui.dev/r/${name}.json`}
|
||||
target="_blank"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="font-sans absolute top-4 right-4"
|
||||
>
|
||||
Open in
|
||||
<svg
|
||||
viewBox="0 0 40 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5 ml-2 text-current"
|
||||
>
|
||||
<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>
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<Preview />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className="relative rounded-sm overflow-auto">{Code}</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ComponentSourceProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export function ComponentSource({ children, className }: ComponentSourceProps) {
|
||||
return (
|
||||
<div className={cn("overflow-hidden rounded-md", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Button } from "@/components/retroui/Button";
|
||||
import { Dialog } from "@/components/retroui/Dialog";
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
import { Theme, themeConfig } from "@/config/theme";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
|
||||
export default function CopyTheme({ theme, hasRadius = false }: { theme: Theme, hasRadius?: boolean }) {
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Trigger asChild>
|
||||
<Button>Copy Theme</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content className={`theme-${theme} ${hasRadius ? "with-radius" : ""}`}>
|
||||
<Dialog.Header>
|
||||
<Text as="h5">Copy Theme</Text>
|
||||
</Dialog.Header>
|
||||
<section className="w-full p-4">
|
||||
<Text className="text-lg mb-4 text-muted-foreground">Copy and paste the following theme code into your CSS file.</Text>
|
||||
|
||||
<CodeBlock className="w-full max-h-[480px] text-gray-300 text-sm overflow-y-auto">
|
||||
{themeConfig({ hasRadius })[theme].color}
|
||||
</CodeBlock>
|
||||
</section>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import SideNav from "./SideNav";
|
||||
import { AlignJustify, X } from "lucide-react";
|
||||
import { Button } from "@/components/retroui";
|
||||
|
||||
export default function HamburgerMenu() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="p-2"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
>
|
||||
{isOpen ? (
|
||||
<X className="h-4 w-4" />
|
||||
) : (
|
||||
<AlignJustify className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 w-full h-screen bg-black opacity-50"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-0 bottom-0 h-screen left-0 z-10">
|
||||
<SideNav setIsOpen={setIsOpen} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Text, Input } from "@/components/retroui";
|
||||
import { useState } from "react";
|
||||
|
||||
const INIT = "INIT";
|
||||
const SUBMITTING = "SUBMITTING";
|
||||
const ERROR = "ERROR";
|
||||
const SUCCESS = "SUCCESS";
|
||||
const formStates = [INIT, SUBMITTING, ERROR, SUCCESS] as const;
|
||||
const formStyles = {
|
||||
id: "cltl71xnw000ris85z92rosvh",
|
||||
name: "Default",
|
||||
formStyle: "inline",
|
||||
placeholderText: "you@example.com",
|
||||
formFont: "font-sans",
|
||||
formFontColor: "#000000",
|
||||
formFontSizePx: 14,
|
||||
buttonText: "Subscribe",
|
||||
buttonFont: "font-sans",
|
||||
buttonFontColor: "#ffffff",
|
||||
buttonColor: "#000000",
|
||||
buttonFontSizePx: 14,
|
||||
successMessage: "Thanks! We'll be in touch!",
|
||||
successFont: "Inter",
|
||||
successFontColor: "#000000",
|
||||
successFontSizePx: 14,
|
||||
userGroup: "",
|
||||
};
|
||||
const domain = "app.loops.so";
|
||||
|
||||
export function JoinNewsletter() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [formState, setFormState] = useState<(typeof formStates)[number]>(INIT);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const resetForm = () => {
|
||||
setEmail("");
|
||||
setFormState(INIT);
|
||||
setErrorMessage("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Rate limit the number of submissions allowed
|
||||
* @returns {boolean} true if the form has been successfully submitted in the past minute
|
||||
*/
|
||||
const hasRecentSubmission = () => {
|
||||
const time = new Date();
|
||||
const timestamp = time.valueOf();
|
||||
const previousTimestamp = localStorage.getItem("loops-form-timestamp");
|
||||
|
||||
// Indicate if the last sign up was less than a minute ago
|
||||
if (
|
||||
previousTimestamp &&
|
||||
Number(previousTimestamp) + 60 * 1000 > timestamp
|
||||
) {
|
||||
setFormState(ERROR);
|
||||
setErrorMessage("Too many signups, please try again in a little while");
|
||||
return true;
|
||||
}
|
||||
|
||||
localStorage.setItem("loops-form-timestamp", timestamp.toString());
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
// Prevent the default form submission
|
||||
event.preventDefault();
|
||||
|
||||
// boundary conditions for submission
|
||||
if (formState !== INIT) return;
|
||||
if (!isValidEmail(email)) {
|
||||
setFormState(ERROR);
|
||||
setErrorMessage("Please enter a valid email");
|
||||
return;
|
||||
}
|
||||
if (hasRecentSubmission()) return;
|
||||
setFormState(SUBMITTING);
|
||||
|
||||
// build body
|
||||
const formBody = `userGroup=${encodeURIComponent(
|
||||
formStyles.userGroup,
|
||||
)}&email=${encodeURIComponent(email)}&mailingLists=`;
|
||||
|
||||
// API request to add user to newsletter
|
||||
fetch(`https://${domain}/api/newsletter-form/${formStyles.id}`, {
|
||||
method: "POST",
|
||||
body: formBody,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((res: any) => [res.ok, res.json(), res])
|
||||
.then(([ok, dataPromise, res]) => {
|
||||
if (ok) {
|
||||
resetForm();
|
||||
setFormState(SUCCESS);
|
||||
} else {
|
||||
dataPromise.then((data: any) => {
|
||||
setFormState(ERROR);
|
||||
setErrorMessage(data.message || res.statusText);
|
||||
localStorage.setItem("loops-form-timestamp", "");
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setFormState(ERROR);
|
||||
// check for cloudflare error
|
||||
if (error.message === "Failed to fetch") {
|
||||
setErrorMessage(
|
||||
"Too many signups, please try again in a little while",
|
||||
);
|
||||
} else if (error.message) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
localStorage.setItem("loops-form-timestamp", "");
|
||||
});
|
||||
};
|
||||
|
||||
switch (formState) {
|
||||
case SUCCESS:
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: `'${formStyles.successFont}', sans-serif`,
|
||||
color: formStyles.successFontColor,
|
||||
fontSize: `${formStyles.successFontSizePx}px`,
|
||||
}}
|
||||
>
|
||||
{formStyles.successMessage}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
case ERROR:
|
||||
return (
|
||||
<>
|
||||
<SignUpFormError />
|
||||
<BackButton />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto py-16 px-4 border-2 border-black">
|
||||
<div className="flex flex-col justify-center items-center text-center">
|
||||
<Text as="h2">Join Our Newsletter</Text>
|
||||
<p className="text-lg text-muted-foreground mb-8">
|
||||
Get notified about latest updates and new launches.
|
||||
</p>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex space-x-2 w-full lg:w-1/2"
|
||||
>
|
||||
<Input
|
||||
placeholder={formStyles.placeholderText}
|
||||
value={email}
|
||||
type="text"
|
||||
name="email"
|
||||
required={true}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button>Subscribe</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SignUpFormError() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "Inter, sans-serif",
|
||||
color: "rgb(185, 28, 28)",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
{errorMessage || "Oops! Something went wrong, please try again"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BackButton() {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<button
|
||||
style={{
|
||||
color: "#6b7280",
|
||||
font: "14px, Inter, sans-serif",
|
||||
margin: "10px auto",
|
||||
textAlign: "center",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
textDecoration: isHovered ? "underline" : "none",
|
||||
}}
|
||||
onMouseOut={() => setIsHovered(false)}
|
||||
onMouseOver={() => setIsHovered(true)}
|
||||
onClick={resetForm}
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isValidEmail(email: any) {
|
||||
return /.+@.+/.test(email);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, Badge, Card, Text } from "@/components/retroui";
|
||||
import { useMDXComponent } from "next-contentlayer/hooks";
|
||||
import React, { AnchorHTMLAttributes, HTMLAttributes } from "react";
|
||||
import { ComponentShowcase } from "./ComponentShowcase";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ComponentSource } from "./ComponentSource";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
import Link from "next/link";
|
||||
import { ComponentInstall, CliCommand } from "./ComponentInstall";
|
||||
import Image from "next/image";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import { Table } from "./retroui/Table";
|
||||
|
||||
const components = (type: "doc" | "blog") => ({
|
||||
h1: (props: HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Text as="h1" {...props} />
|
||||
),
|
||||
h2: (props: HTMLAttributes<HTMLHeadingElement>) =>
|
||||
type === "blog" ? (
|
||||
<Text as="h2" className="mb-4 mt-8" {...props} />
|
||||
) : (
|
||||
<Text as="h2" className="border-b pb-1 mb-6" {...props} />
|
||||
),
|
||||
h3: (props: HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Text as="h3" className="mb-4" {...props} />
|
||||
),
|
||||
h4: (props: HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Text as="h4" className="mb-2" {...props} />
|
||||
),
|
||||
h5: (props: HTMLAttributes<HTMLHeadElement>) => <Text as="h5" {...props} />,
|
||||
h6: (props: HTMLAttributes<HTMLHeadElement>) => <Text as="h6" {...props} />,
|
||||
p: (props: HTMLAttributes<HTMLHeadElement>) =>
|
||||
type === "blog" ? (
|
||||
<Text {...props} className="text-lg text-foreground" />
|
||||
) : (
|
||||
<Text {...props} />
|
||||
),
|
||||
li: (props: HTMLAttributes<HTMLHeadElement>) =>
|
||||
type === "blog" ? (
|
||||
<Text
|
||||
as="li"
|
||||
{...props}
|
||||
className="text-lg text-foreground ml-4 lg:ml-8 mb-2"
|
||||
/>
|
||||
) : (
|
||||
<Text as="li" className="mb-2" {...props} />
|
||||
),
|
||||
img: (props: HTMLAttributes<HTMLImageElement>) => (
|
||||
<img className="mx-auto w-full max-w-[600px] my-8" {...props} />
|
||||
),
|
||||
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const { href, target, rel, ...rest } = props;
|
||||
|
||||
if (!href) {
|
||||
return <a {...rest} />;
|
||||
}
|
||||
|
||||
const isExternal = href.startsWith("http");
|
||||
|
||||
return isExternal ? (
|
||||
<a
|
||||
href={href}
|
||||
target={target || "_blank"}
|
||||
rel={rel || "noopener noreferrer"}
|
||||
className="underline underline-offset-4 hover:decoration-primary"
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<Link
|
||||
href={href}
|
||||
className="underline underline-offset-4 hover:decoration-primary"
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
pre: CodeBlock,
|
||||
code: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) => (
|
||||
<code
|
||||
className={cn(
|
||||
"relative rounded-sm bg-[#282A36] p-1 text-primary text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
TabGroup,
|
||||
TabList,
|
||||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
Table,
|
||||
Link,
|
||||
Badge,
|
||||
Image,
|
||||
Card,
|
||||
Alert,
|
||||
ComponentShowcase,
|
||||
ComponentSource,
|
||||
ComponentInstall,
|
||||
CliCommand,
|
||||
});
|
||||
|
||||
export default function MDX({
|
||||
code,
|
||||
type = "doc",
|
||||
}: {
|
||||
code: string;
|
||||
type?: "doc" | "blog";
|
||||
}) {
|
||||
const Content = useMDXComponent(code);
|
||||
|
||||
return <Content components={components(type)} />;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
const CopyIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const CheckIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const RichTextCodeBlock: React.FC<{
|
||||
code: string
|
||||
language?: string
|
||||
title?: string
|
||||
}> = ({ code, language, title }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-6 rounded-lg overflow-hidden border-2 border-border bg-[#282A36]">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-[#1e1f29] border-b-2 border-border">
|
||||
<span className="text-sm text-gray-400 font-mono">
|
||||
{title || language || 'code'}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1.5 text-xs text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckIcon />
|
||||
<span>Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CopyIcon />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-4 overflow-x-auto">
|
||||
<code className="text-sm text-accent font-mono whitespace-pre">
|
||||
{code}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import Link from 'next/link'
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedUploadNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
type JSXConvertersFunction,
|
||||
} from '@payloadcms/richtext-lexical/react'
|
||||
import { Text } from '@/components/retroui'
|
||||
import { RichTextCodeBlock } from '@/components/RichTextCodeBlock'
|
||||
|
||||
const CustomUploadComponent: React.FC<{
|
||||
node: SerializedUploadNode
|
||||
}> = ({ node }) => {
|
||||
if (node.relationTo === 'media') {
|
||||
const uploadDoc = node.value
|
||||
if (typeof uploadDoc !== 'object') {
|
||||
return null
|
||||
}
|
||||
const { alt, height, url, width } = uploadDoc
|
||||
return (
|
||||
<img
|
||||
alt={alt}
|
||||
height={height}
|
||||
src={`https://cms.retroui.dev${url}`}
|
||||
width={width}
|
||||
className="mx-auto w-full max-w-[600px] my-8"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
type NodeTypes = DefaultNodeTypes
|
||||
export const JSXConverters: JSXConvertersFunction<NodeTypes> = ({
|
||||
defaultConverters,
|
||||
}) => {
|
||||
return {
|
||||
...defaultConverters,
|
||||
heading: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
const Tag = node.tag as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||
const classNames: Record<string, string> = {
|
||||
h1: '',
|
||||
h2: 'mb-4 mt-8 lg:text-3xl',
|
||||
h3: 'mb-4',
|
||||
h4: 'mb-2',
|
||||
h5: '',
|
||||
h6: '',
|
||||
}
|
||||
return (
|
||||
<Text as={Tag} className={classNames[Tag]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
paragraph: ({ node, nodesToJSX }) => {
|
||||
// Handle empty paragraphs as spacing
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return <br />
|
||||
}
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
return <Text className="text-lg text-foreground">{children}</Text>
|
||||
},
|
||||
text: ({ node }) => {
|
||||
const format = node.format ?? 0
|
||||
let text: React.ReactNode = node.text
|
||||
|
||||
// Format is a bitmask: 1=bold, 2=italic, 4=strikethrough, 8=underline, 16=code
|
||||
if (format & 16) {
|
||||
// Inline code
|
||||
text = (
|
||||
<code className="rounded-sm bg-[#282A36] px-1.5 py-0.5 text-primary text-sm">
|
||||
{text}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
if (format & 1) {
|
||||
text = <strong>{text}</strong>
|
||||
}
|
||||
if (format & 2) {
|
||||
text = <em>{text}</em>
|
||||
}
|
||||
if (format & 4) {
|
||||
text = <s>{text}</s>
|
||||
}
|
||||
if (format & 8) {
|
||||
text = <u>{text}</u>
|
||||
}
|
||||
|
||||
return <>{text}</>
|
||||
},
|
||||
list: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
if (node.listType === 'bullet') {
|
||||
return <ul className="list-disc pl-4 lg:pl-8 my-4">{children}</ul>
|
||||
}
|
||||
return <ol className="list-decimal pl-4 lg:pl-8 my-4">{children}</ol>
|
||||
},
|
||||
listitem: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
return (
|
||||
<Text as="li" className="text-lg text-foreground ml-4 lg:ml-8 mb-2">
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
link: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
const href = node.fields?.url || ''
|
||||
const isExternal = href.startsWith('http')
|
||||
const linkClass = 'underline underline-offset-4 hover:decoration-primary'
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={linkClass}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href} className={linkClass}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
quote: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: node.children })
|
||||
return (
|
||||
<blockquote className="border-l-4 border-primary pl-4 my-4 italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
)
|
||||
},
|
||||
linebreak: () => {
|
||||
return <br />
|
||||
},
|
||||
upload: ({ node }) => {
|
||||
return <CustomUploadComponent node={node} />
|
||||
},
|
||||
// Handle autolink nodes
|
||||
autolink: ({ node, nodesToJSX }) => {
|
||||
const children = nodesToJSX({ nodes: (node as any).children })
|
||||
const href = (node as any).fields?.url || ''
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-4 hover:decoration-primary break-all"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
// Blocks converter - nested object with slug as key
|
||||
blocks: {
|
||||
Code: ({ node }: any) => {
|
||||
const fields = (node as any).fields
|
||||
return (
|
||||
<RichTextCodeBlock
|
||||
code={fields?.code || ''}
|
||||
language={fields?.language}
|
||||
title={fields?.blockName}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { navConfig } from "@/config/navigation";
|
||||
import { Badge, Text } from "@/components/retroui";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface SideNavProps {
|
||||
setIsOpen?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export default function SideNav({ setIsOpen }: SideNavProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="sidebar-scroll border-r-2 z-10 overflow-y-scroll h-full max-h-[calc(100vh-6rem)] transition-transform transform md:translate-x-0 w-full bg-background flex flex-col justify-start md:justify-start py-8">
|
||||
<nav
|
||||
className="flex flex-col items-start max-lg:px-6 space-y-4 z-99"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
{navConfig.sideNavItems.map((item) => (
|
||||
<div key={item.title} className="w-full">
|
||||
<Text as="h5">{item.title}</Text>
|
||||
<div className="flex flex-col w-full">
|
||||
{item.children.map((child) => (
|
||||
<Link
|
||||
key={child.title}
|
||||
href={child.href}
|
||||
onClick={() => setIsOpen && setIsOpen(false)}
|
||||
target={child.href.startsWith("http") ? "_blank" : "_self"}
|
||||
className={`px-2 py-1 w-full border border-transparent text-muted-foreground flex items-center justify-between hover:text-foreground hover:bg-muted/50 transition-colors ${
|
||||
pathname === child.href &&
|
||||
"bg-primary text-primary-foreground"
|
||||
}`}
|
||||
>
|
||||
{child.title}
|
||||
{child.tag && (
|
||||
<Badge
|
||||
size="sm"
|
||||
className="py-0.5 px-1.5 border text-xs border-secondary text-muted-foreground bg-secondary/10"
|
||||
>
|
||||
{child.tag}
|
||||
</Badge>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { TableOfContents as TOCType } from '@/lib/toc';
|
||||
|
||||
interface TableOfContentsProps {
|
||||
toc: TOCType;
|
||||
}
|
||||
|
||||
function renderTOCItems(items: any[], level = 0) {
|
||||
if (!items || items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<ul className={`space-y-1 ${level > 0 ? 'ml-4 mt-1' : ''}`}>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<a
|
||||
href={item.url}
|
||||
className="text-sm hover:text-foreground transition-colors block py-1 border-l-2 border-transparent hover:border-accent pl-2"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
{item.items && renderTOCItems(item.items, level + 1)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TableOfContents({ toc }: TableOfContentsProps) {
|
||||
if (!toc.items || toc.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border p-4 rounded-sm max-h-60 overflow-y-auto sidebar-scroll">
|
||||
<h3 className="mb-3 border-b border-black pb-2">
|
||||
On this Page
|
||||
</h3>
|
||||
{renderTOCItems(toc.items)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Text, Card, Avatar } from "@/components/retroui";
|
||||
|
||||
interface TestimonialCardProps {
|
||||
quote: string;
|
||||
avatarSrc: string;
|
||||
name: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function TestimonialCard({
|
||||
quote,
|
||||
avatarSrc,
|
||||
name,
|
||||
title,
|
||||
}: TestimonialCardProps) {
|
||||
return (
|
||||
<Card className="w-full max-w-[400px] shadow-none hover:shadow-md">
|
||||
<Card.Content>
|
||||
<Text className="text-lg">{quote}</Text>
|
||||
|
||||
<div className="flex items-center space-x-2 mt-6">
|
||||
<Avatar className="h-10 w-10">
|
||||
<Avatar.Image alt={`${name} avatar`} src={avatarSrc} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{name}</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function ThemeWrapper({ children }: { children: React.ReactNode }) {
|
||||
const { colorTheme, variant } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const wrapper = document.querySelector(".theme-wrapper");
|
||||
if (wrapper) {
|
||||
wrapper.className = `theme-wrapper theme-${colorTheme} ${variant}`;
|
||||
}
|
||||
}, [colorTheme, variant]);
|
||||
|
||||
return <div className={`theme-wrapper theme-${colorTheme} ${variant}`}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { ArrowUpRight, GithubIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import HamburgerMenu from "./HamburgerMenu";
|
||||
import { Button, Text } from "@/components/retroui";
|
||||
import { navConfig } from "@/config/navigation";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
export default function TopNav() {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
|
||||
return (
|
||||
<nav className="sticky z-1 top-0 right-0 w-full border-b-2 bg-background">
|
||||
<div className="w-full bg-black text-white">
|
||||
<div className="container max-w-6xl mx-auto px-4 py-2 flex justify-center space-x-4 items-center">
|
||||
<Text className="text-sm lg:text-center">
|
||||
Ship faster with premium blocks and templates!
|
||||
</Text>
|
||||
<a href="https://dub.sh/retroui-pro" target="_blank">
|
||||
<Button
|
||||
size="sm"
|
||||
className="shadow-none font-sans font-bold px-2 py-1 w-30"
|
||||
>
|
||||
RetroUI Pro
|
||||
<ArrowUpRight className="ml-auto h-4 w-4 inline-block" />
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container max-w-6xl px-4 lg:px-0 mx-auto">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo Section */}
|
||||
<div className="shrink-0">
|
||||
<a
|
||||
href="/"
|
||||
className="text-black font-head text-2xl flex items-end"
|
||||
>
|
||||
<Image
|
||||
src="/images/logo.png"
|
||||
alt="retro ui logo"
|
||||
className="mr-2"
|
||||
height={30}
|
||||
width={30}
|
||||
/>
|
||||
<div className="text-foreground">RetroUI</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="hidden md:flex space-x-4">
|
||||
{navConfig.topNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.title}
|
||||
href={item.href}
|
||||
className="hover:underline decoration-primary underline-offset-2 transition-all"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 lg:hidden">
|
||||
<Link
|
||||
href="https://github.com/logging-studio/retroui"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<GithubIcon />
|
||||
</Link>
|
||||
<HamburgerMenu />
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex items-center space-x-3">
|
||||
<Link
|
||||
href="https://github.com/logging-studio/retroui"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="secondary" size="icon">
|
||||
<GithubIcon size="14"/>
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant="secondary" size="icon" onClick={toggleDarkMode}>
|
||||
{isDarkMode ? <SunIcon size="14" /> : <MoonIcon size="14" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Text } from "@/components/retroui";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
export default function WorksWithSection() {
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
name: "vite",
|
||||
lightSrc: "/images/logos/vite.png",
|
||||
darkSrc: "/images/logos/vite_white.png",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
name: "nextjs",
|
||||
lightSrc: "/images/logos/nextjs.png",
|
||||
darkSrc: "/images/logos/next_white.png",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
name: "remix",
|
||||
lightSrc: "/images/logos/remix.png",
|
||||
darkSrc: "/images/logos/remix_white.png",
|
||||
size: 55,
|
||||
},
|
||||
{
|
||||
name: "astro",
|
||||
lightSrc: "/images/logos/astro.png",
|
||||
darkSrc: "/images/logos/astro_white.png",
|
||||
size: 55,
|
||||
},
|
||||
{
|
||||
name: "gatsby",
|
||||
lightSrc: "/images/logos/gatsby.png",
|
||||
darkSrc: "/images/logos/gatsby_white.png",
|
||||
size: 60,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="container max-w-6xl mx-auto px-4 lg:px-0 my-48" id="works-with">
|
||||
<Text as="h2" className="text-center max-w-2xl mx-auto mb-28">
|
||||
Works With Both <span className="text-outlined">SSR</span> and{" "}
|
||||
<span className="text-outlined">SPA</span> Applications.
|
||||
</Text>
|
||||
|
||||
<div className="grid grid-cols-5 gap-8 lg:gap-6 max-w-2xl mx-auto items-center">
|
||||
{frameworks.map((framework) => (
|
||||
<div key={framework.name}>
|
||||
<Image
|
||||
src={isDarkMode ? framework.darkSrc : framework.lightSrc}
|
||||
alt={`${framework.name} ${isDarkMode ? 'dark' : 'light'}`}
|
||||
width={framework.size}
|
||||
height={framework.size}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/retroui/Button";
|
||||
import { Input } from "@/components/retroui/Input";
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
import { Mail, TwitterIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { subscribeToNewsletter } from "@/app/actions/newsletter";
|
||||
|
||||
const quickLinks = [
|
||||
{ name: "Docs", href: "/docs" },
|
||||
{ name: "Blogs", href: "/blogs" },
|
||||
{ name: "Community", href: "https://discord.com/invite/Jum3NJxK6Q" },
|
||||
{ name: "Pro Blocks", href: "https://pro.retroui.dev" },
|
||||
];
|
||||
|
||||
const projectLinks = [
|
||||
{ name: "CommerCN", href: "https://commercn.com" },
|
||||
{ name: "Evendeals", href: "https://evendeals.com" },
|
||||
{ name: "Tanstack Starter Kit", href: "https://tanstackstarterkit.com" },
|
||||
];
|
||||
|
||||
function Footer() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
|
||||
const handleSubscribe = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus("loading");
|
||||
|
||||
const result = await subscribeToNewsletter(email);
|
||||
|
||||
if (result.success) {
|
||||
setStatus("success");
|
||||
setEmail("");
|
||||
} else {
|
||||
setStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="border-t-2 mt-24">
|
||||
<div className="max-w-6xl mx-auto px-4 py-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-12">
|
||||
<div className="lg:col-span-2 max-w-md">
|
||||
<Text as="h2" className="mb-6">
|
||||
STAY CONNECTED
|
||||
</Text>
|
||||
<Text className="text-muted-foreground leading-relaxed mb-2 text-sm">
|
||||
Join our community to stay updated with new component releases, updates, and special offers.
|
||||
</Text>
|
||||
<form onSubmit={handleSubscribe} className="mb-8">
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Button className="w-54 justify-center" type="submit" disabled={status === "loading"}>
|
||||
{status === "loading" ? "..." : "SUBSCRIBE"}
|
||||
</Button>
|
||||
</div>
|
||||
{status === "success" && (
|
||||
<Text className="text-green-600 text-sm mt-2">Subscribed successfully!</Text>
|
||||
)}
|
||||
{status === "error" && (
|
||||
<Text className="text-red-600 text-sm mt-2">Failed to subscribe. Try again.</Text>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="flex space-x-6">
|
||||
<a href="mailto:arif@retroui.dev" target="_blank" className="flex items-center gap-2">
|
||||
<div className="bg-black text-white p-1 shadow shadow-primary">
|
||||
<Mail className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="hover:underline underline-offset-4">
|
||||
arif@retroui.dev
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://x.com/ariflogs" target="_blank" className="flex items-center gap-2">
|
||||
<div className="bg-black text-white p-1 shadow shadow-primary">
|
||||
<TwitterIcon className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="hover:underline underline-offset-4">@ariflogs</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1">
|
||||
<Text as="h4" className="mb-6">
|
||||
Quick Links
|
||||
</Text>
|
||||
<ul className="space-y-2">
|
||||
{quickLinks.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a
|
||||
href={link.href}
|
||||
className="hover:underline font-medium underline-offset-4 decoration-primary decoration-2 transition-all"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1">
|
||||
<Text as="h4" className="mb-6">
|
||||
By Makers of RetroUI
|
||||
</Text>
|
||||
<ul className="space-y-2">
|
||||
{projectLinks.map((project) => (
|
||||
<li key={project.name}>
|
||||
<a
|
||||
href={project.href}
|
||||
target="_blank"
|
||||
className="hover:underline font-medium underline-offset-4 decoration-primary decoration-2 transition-all"
|
||||
>
|
||||
{project.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-foreground">
|
||||
<div className="max-w-6xl mx-auto px-4 py-6 text-center">
|
||||
<Text className="text-sm text-background">
|
||||
© 2026 Logging Studio. Crafted with ❤️ and ☕.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./ComponentShowcase";
|
||||
export * from "./JoinNewsletter";
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-2 bg-background rounded text-foreground shadow-md hover:shadow-sm data-[state=open]:shadow-sm transition-all overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = AccordionPrimitive.Item.displayName;
|
||||
|
||||
const AccordionHeader = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-start justify-between px-4 py-2 font-head cursor-pointer focus:outline-hidden [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionHeader.displayName = AccordionPrimitive.Header.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden font-body bg-white text-gray-700 data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("px-4 pt-2 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
const AccordionComponent = Object.assign(Accordion, {
|
||||
Item: AccordionItem,
|
||||
Header: AccordionHeader,
|
||||
Content: AccordionContent,
|
||||
});
|
||||
|
||||
export { AccordionComponent as Accordion };
|
||||
@@ -0,0 +1,57 @@
|
||||
import { HtmlHTMLAttributes } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
|
||||
const alertVariants = cva("relative w-full rounded border-2 p-4", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground [&_svg]:shrink-0",
|
||||
solid: "bg-black text-white",
|
||||
},
|
||||
status: {
|
||||
error: "bg-red-300 text-red-800 border-red-800",
|
||||
success: "bg-green-300 text-green-800 border-green-800",
|
||||
warning: "bg-yellow-300 text-yellow-800 border-yellow-800",
|
||||
info: "bg-blue-300 text-blue-800 border-blue-800",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
interface IAlertProps
|
||||
extends HtmlHTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof alertVariants> {}
|
||||
|
||||
const Alert = ({ className, variant, status, ...props }: IAlertProps) => (
|
||||
<div
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant, status }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
interface IAlertTitleProps extends HtmlHTMLAttributes<HTMLHeadingElement> {}
|
||||
const AlertTitle = ({ className, ...props }: IAlertTitleProps) => (
|
||||
<Text as="h5" className={cn(className)} {...props} />
|
||||
);
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
interface IAlertDescriptionProps
|
||||
extends HtmlHTMLAttributes<HTMLParagraphElement> {}
|
||||
const AlertDescription = ({ className, ...props }: IAlertDescriptionProps) => (
|
||||
<div className={cn("text-muted-foreground", className)} {...props} />
|
||||
);
|
||||
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
const AlertComponent = Object.assign(Alert, {
|
||||
Title: AlertTitle,
|
||||
Description: AlertDescription,
|
||||
});
|
||||
|
||||
export { AlertComponent as Alert };
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-14 w-14 border-2 rounded-full overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = "Avatar";
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = "Avatar.Image";
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-primary text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = "Avatar.Fallback";
|
||||
|
||||
const AvatarComponent = Object.assign(Avatar, {
|
||||
Image: AvatarImage,
|
||||
Fallback: AvatarFallback,
|
||||
});
|
||||
|
||||
export { AvatarComponent as Avatar };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import React, { HTMLAttributes } from "react";
|
||||
|
||||
const badgeVariants = cva("font-semibold rounded inline-flex items-center", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted text-muted-foreground",
|
||||
outline: "outline-2 outline-foreground text-foreground",
|
||||
solid: "bg-foreground text-background",
|
||||
surface: "outline-2 bg-primary text-primary-foreground",
|
||||
},
|
||||
size: {
|
||||
sm: "px-2 py-1 text-xs",
|
||||
md: "px-2.5 py-1.5 text-sm",
|
||||
lg: "px-3 py-2 text-base",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
interface ButtonProps
|
||||
extends HTMLAttributes<HTMLSpanElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
export function Badge({
|
||||
children,
|
||||
size = "md",
|
||||
variant = "default",
|
||||
className = "",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(badgeVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export function BasicCard() {
|
||||
return (
|
||||
<div className="inline-block border-2 p-4 shadow-md cursor-pointer transition-all hover:shadow-xs">
|
||||
<h4 className="font-head text-2xl font-medium mb-1">
|
||||
This is card Title
|
||||
</h4>
|
||||
<p>This is card description.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const BreadcrumbRoot = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<nav
|
||||
ref={ref}
|
||||
aria-label="breadcrumb"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbRoot.displayName = "Breadcrumb"
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 sm:gap-2.5 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbList.displayName = "BreadcrumbList"
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("inline-flex items-center", className)} {...props} />
|
||||
))
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"font-medium transition-colors hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring rounded-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
role="presentation"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis"
|
||||
|
||||
const Breadcrumb = Object.assign(BreadcrumbRoot, {
|
||||
List: BreadcrumbList,
|
||||
Item: BreadcrumbItem,
|
||||
Link: BreadcrumbLink,
|
||||
Page: BreadcrumbPage,
|
||||
Separator: BreadcrumbSeparator,
|
||||
Ellipsis: BreadcrumbEllipsis,
|
||||
})
|
||||
|
||||
export { Breadcrumb }
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"font-head transition-all rounded outline-hidden cursor-pointer duration-200 font-medium flex justify-center items-center disabled:opacity-60 disabled:cursor-not-allowed",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"shadow-md hover:shadow active:shadow-none bg-primary text-primary-foreground border-2 border-black transition hover:translate-y-1 active:translate-y-2 active:translate-x-1 hover:bg-primary-hover",
|
||||
secondary:
|
||||
"shadow-md hover:shadow active:shadow-none bg-secondary shadow-primary text-secondary-foreground border-2 border-black transition hover:translate-y-1 active:translate-y-2 active:translate-x-1 hover:bg-secondary-hover",
|
||||
outline:
|
||||
"shadow-md hover:shadow active:shadow-none bg-transparent border-2 transition hover:translate-y-1 active:translate-y-2 active:translate-x-1",
|
||||
link: "bg-transparent hover:underline",
|
||||
ghost: "bg-transparent hover:bg-accent"
|
||||
},
|
||||
size: {
|
||||
sm: "px-3 py-1 text-sm shadow hover:shadow-none",
|
||||
md: "px-4 py-1.5 text-base",
|
||||
lg: "px-6 lg:px-8 py-2 lg:py-3 text-md lg:text-lg",
|
||||
icon: "p-2",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface IButtonProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, IButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
size = "md",
|
||||
className = "",
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: IButtonProps,
|
||||
forwardedRef,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
ref={forwardedRef}
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
@@ -0,0 +1,210 @@
|
||||
"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/retroui/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 w-full outline-2 shadow-md 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 font-head", 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-8 p-2 border-2 rounded select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-8 p-2 border-2 rounded 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:outline-ring outline outline-input has-focus:ring-ring/50 has-focus:ring-[3px] rounded",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-base"
|
||||
: "rounded-none 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 outline-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"flex-1 font-normal text-sm 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 [&:last-child[data-selected=true]_button]:rounded-r group/day aspect-square select-none",
|
||||
props.showWeekNumber
|
||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l"
|
||||
: "[&:first-child[data-selected=true]_button]:rounded-l",
|
||||
defaultClassNames.day
|
||||
),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground opacity-80",
|
||||
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.toLocaleDateString()}
|
||||
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(
|
||||
"font-sans flex justify-center items-center data-[selected-single=true]:shadow-md data-[selected-single=true]:outline-2 outline-border data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-secondary data-[range-middle=true]:hover:text-secondary-foreground data-[range-middle=true]:text-secondary-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-1 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-[2px] data-[range-end=true]:rounded-none data-[range-end=true]:rounded-none data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-none [&>span]:text-xs",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
@@ -0,0 +1,49 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HTMLAttributes } from "react";
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
|
||||
interface ICardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Card = ({ className, ...props }: ICardProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-block border-2 rounded shadow-md transition-all hover:shadow-none bg-card",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CardHeader = ({ className, ...props }: ICardProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col justify-start p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CardTitle = ({ className, ...props }: ICardProps) => {
|
||||
return <Text as="h3" className={cn("mb-2", className)} {...props} />;
|
||||
};
|
||||
|
||||
const CardDescription = ({ className, ...props }: ICardProps) => (
|
||||
<p className={cn("text-muted-foreground", className)} {...props} />
|
||||
);
|
||||
|
||||
const CardContent = ({ className, ...props }: ICardProps) => {
|
||||
return <div className={cn("p-4", className)} {...props} />;
|
||||
};
|
||||
|
||||
const CardComponent = Object.assign(Card, {
|
||||
Header: CardHeader,
|
||||
Title: CardTitle,
|
||||
Description: CardDescription,
|
||||
Content: CardContent,
|
||||
});
|
||||
|
||||
export { CardComponent as Card };
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react"
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/retroui/Button"
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: "horizontal" | "vertical"
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return
|
||||
onSelect(api)
|
||||
api.on("reInit", onSelect)
|
||||
api.on("select", onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const CarouselObject = Object.assign(Carousel, {
|
||||
Content: CarouselContent,
|
||||
Item: CarouselItem,
|
||||
Previous: CarouselPrevious,
|
||||
Next: CarouselNext,
|
||||
})
|
||||
|
||||
export { CarouselObject as Carousel }
|
||||
@@ -0,0 +1,50 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
const checkboxVariants = cva("border-2 rounded", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground ",
|
||||
outline: "",
|
||||
solid:
|
||||
"data-[state=checked]:bg-foreground data-[state=checked]:text-background",
|
||||
},
|
||||
size: {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-5 w-5",
|
||||
lg: "h-6 w-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
interface CheckboxProps
|
||||
extends React.ComponentProps<typeof CheckboxPrimitive.Root>,
|
||||
VariantProps<typeof checkboxVariants> {}
|
||||
|
||||
export const Checkbox = ({
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
...props
|
||||
}: CheckboxProps) => (
|
||||
<CheckboxPrimitive.Root
|
||||
className={cn(
|
||||
checkboxVariants({
|
||||
size,
|
||||
variant,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className="h-full w-full">
|
||||
<Check className="h-full w-full" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dialog } from "@/components/retroui/Dialog";
|
||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Check, LucideIcon, Search } from "lucide-react";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded bg-background text-foreground border-border shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type CommandDialogProps = DialogProps & { className?: string };
|
||||
|
||||
const CommandDialog = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<Dialog.Content
|
||||
className={cn(
|
||||
"overflow-hidden p-0 shadow-lg rounded w-full max-w-md",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center border-border border-b px-3"
|
||||
cmdk-input-wrapper=""
|
||||
data-slot="command-input"
|
||||
>
|
||||
<Search className="me-2 h-4 w-4 shrink-0 opacity-50 text-foreground" />
|
||||
<CommandPrimitive.Input
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded bg-transparent py-3 text-sm outline-hidden text-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 font-body",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[400px] overflow-auto overscroll-contain transition-[height] h-[calc(min(300px,var(--cmdk-list-height)))] bg-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm text-muted-foreground font-body"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"overflow-hidden p-1.5 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground font-body",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"relative flex text-foreground cursor-pointer gap-2 select-none items-center rounded px-2 py-1.5 text-sm outline-hidden data-[disabled=true]:pointer-events-none data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground transition-all font-body",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"ms-auto text-xs tracking-widest text-muted-foreground font-body",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface CommandCheckProps {
|
||||
icon?: LucideIcon;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CommandCheck({
|
||||
icon: Icon = Check,
|
||||
className,
|
||||
...props
|
||||
}: CommandCheckProps) {
|
||||
return (
|
||||
<Icon
|
||||
data-slot="command-check"
|
||||
data-check="true"
|
||||
className={cn("size-4 ms-auto text-primary", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const CommandComponent = Object.assign(Command, {
|
||||
Check: CommandCheck,
|
||||
Dialog: CommandDialog,
|
||||
Empty: CommandEmpty,
|
||||
Group: CommandGroup,
|
||||
Input: CommandInput,
|
||||
Item: CommandItem,
|
||||
List: CommandList,
|
||||
Separator: CommandSeparator,
|
||||
Shortcut: CommandShortcut,
|
||||
});
|
||||
|
||||
export { CommandComponent as Command };
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { ClipboardIcon, CheckIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface CommandDisplayProps {
|
||||
command: string;
|
||||
}
|
||||
|
||||
export function CommandDisplay({ command }: CommandDisplayProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultColors = [
|
||||
"text-blue-300",
|
||||
"text-yellow-300",
|
||||
"text-green-300",
|
||||
"text-purple-300",
|
||||
];
|
||||
|
||||
// Split the command into parts for syntax highlighting
|
||||
const parts = command.split(" ").map((part, index) => {
|
||||
const color = defaultColors[index % defaultColors.length];
|
||||
return (
|
||||
<span key={index} className={color}>
|
||||
{part}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative group flex items-center bg-black/90 pl-4 py-2 font-mono text-xs">
|
||||
<div className="flex-1 whitespace-nowrap overflow-hidden">
|
||||
<div className="overflow-hidden text-ellipsis">
|
||||
{parts.map((part, index) => (
|
||||
<span key={`part-${index}`}>
|
||||
{part}
|
||||
{index < parts.length - 1 && " "}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="mr-2 p-1 shrink-0 text-gray-400 hover:text-white transition-colors"
|
||||
aria-label="Copy command"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="h-4 w-4 text-green-400" />
|
||||
) : (
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-primary focus:text-primary-foreground data-[state=open]:bg-primary data-[state=open]:text-primary-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-background text-foreground border-2 shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[--radix-context-menu-content-transform-origin] overflow-hidden rounded-sm p-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"bg-background text-foreground border-2 shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-context-menu-content-available-height)] min-w-[8rem] origin-[--radix-context-menu-content-transform-origin] overflow-x-hidden overflow-y-auto rounded-sm p-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-primary focus:text-primary-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-primary focus:text-primary-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-primary focus:text-primary-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const ContextMenuComponent = Object.assign(ContextMenu, {
|
||||
Trigger: ContextMenuTrigger,
|
||||
Content: ContextMenuContent,
|
||||
Item: ContextMenuItem,
|
||||
CheckboxItem: ContextMenuCheckboxItem,
|
||||
RadioItem: ContextMenuRadioItem,
|
||||
Label: ContextMenuLabel,
|
||||
Separator: ContextMenuSeparator,
|
||||
Shortcut: ContextMenuShortcut,
|
||||
Group: ContextMenuGroup,
|
||||
Portal: ContextMenuPortal,
|
||||
Sub: ContextMenuSub,
|
||||
SubContent: ContextMenuSubContent,
|
||||
SubTrigger: ContextMenuSubTrigger,
|
||||
RadioGroup: ContextMenuRadioGroup,
|
||||
});
|
||||
|
||||
export { ContextMenuComponent as ContextMenu };
|
||||
@@ -0,0 +1,232 @@
|
||||
"use client";
|
||||
|
||||
import * as ReactDialog from "@radix-ui/react-dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import React, { HTMLAttributes, ReactNode } from "react";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
const Dialog = ReactDialog.Root;
|
||||
const DialogTrigger = ReactDialog.Trigger;
|
||||
|
||||
const overlayVariants = cva(
|
||||
` fixed bg-black/80 font-head
|
||||
data-[state=open]:fade-in-0
|
||||
data-[state=open]:animate-in
|
||||
data-[state=closed]:animate-out
|
||||
data-[state=closed]:fade-out-0
|
||||
`,
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "inset-0 z-50 bg-black/85",
|
||||
none: "fixed bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface IDialogBackgroupProps
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof overlayVariants> {}
|
||||
|
||||
const DialogBackdrop = React.forwardRef<HTMLDivElement, IDialogBackgroupProps>(
|
||||
function DialogBackdrop(inputProps: IDialogBackgroupProps, forwardedRef) {
|
||||
const { variant = "default", className, ...props } = inputProps;
|
||||
|
||||
return (
|
||||
<ReactDialog.Overlay
|
||||
className={cn(overlayVariants({ variant }), className)}
|
||||
ref={forwardedRef}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
DialogBackdrop.displayName = "DialogBackdrop";
|
||||
|
||||
const dialogVariants = cva(
|
||||
`fixed left-[50%] top-[50%] z-50 grid rounded overflow-hidden w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border-2 bg-background shadow-lg duration-200
|
||||
data-[state=open]:animate-in
|
||||
data-[state=open]:fade-in-0
|
||||
data-[state=open]:zoom-in-95
|
||||
data-[state=closed]:animate-out
|
||||
data-[state=closed]:fade-out-0
|
||||
data-[state=closed]:zoom-out-95`,
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
auto: "max-w-fit",
|
||||
sm: "lg:max-w-[30%]",
|
||||
md: "lg:max-w-[40%]",
|
||||
lg: "lg:max-w-[50%]",
|
||||
xl: "lg:max-w-[60%]",
|
||||
"2xl": "lg:max-w-[70%]",
|
||||
"3xl": "lg:max-w-[80%]",
|
||||
"4xl": "lg:max-w-[90%]",
|
||||
screen: "max-w-[100%]",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "auto",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface IDialogContentProps
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof dialogVariants> {
|
||||
overlay?: IDialogBackgroupProps;
|
||||
}
|
||||
|
||||
const DialogContent = React.forwardRef<HTMLDivElement, IDialogContentProps>(
|
||||
function DialogContent(inputProps: IDialogContentProps, forwardedRef) {
|
||||
const {
|
||||
children,
|
||||
size = "auto",
|
||||
className,
|
||||
overlay,
|
||||
...props
|
||||
} = inputProps;
|
||||
|
||||
return (
|
||||
<ReactDialog.Portal>
|
||||
<DialogBackdrop {...overlay} />
|
||||
<ReactDialog.Content
|
||||
className={cn(dialogVariants({ size }), className)}
|
||||
ref={forwardedRef}
|
||||
{...props}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<ReactDialog.Title />
|
||||
</VisuallyHidden>
|
||||
<div className="flex flex-col relative">{children}</div>
|
||||
</ReactDialog.Content>
|
||||
</ReactDialog.Portal>
|
||||
);
|
||||
},
|
||||
);
|
||||
DialogContent.displayName = "DialogContent";
|
||||
|
||||
interface IDialogDescriptionProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
const DialogDescription = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: IDialogDescriptionProps) => {
|
||||
return (
|
||||
<ReactDialog.Description className={cn(className)} {...props}>
|
||||
{children}
|
||||
</ReactDialog.Description>
|
||||
);
|
||||
};
|
||||
|
||||
const dialogFooterVariants = cva(
|
||||
"flex items-center justify-end border-t-2 min-h-12 gap-4 px-4 py-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
},
|
||||
position: {
|
||||
fixed: "sticky bottom-0",
|
||||
static: "static",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
position: "fixed",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface IDialogFooterProps
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof dialogFooterVariants> {}
|
||||
|
||||
const DialogFooter = ({
|
||||
children,
|
||||
className,
|
||||
position,
|
||||
variant,
|
||||
...props
|
||||
}: IDialogFooterProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(dialogFooterVariants({ position, variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const dialogHeaderVariants = cva(
|
||||
"flex items-center justify-between border-b-2 px-4 min-h-12",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground",
|
||||
},
|
||||
position: {
|
||||
fixed: "sticky top-0",
|
||||
static: "static",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
position: "static",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const DialogHeaderDefaultLayout = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<DialogTrigger title="Close pop-up" className="cursor-pointer" asChild>
|
||||
<X />
|
||||
</DialogTrigger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IDialogHeaderProps
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof dialogHeaderVariants>,
|
||||
ReactDialog.DialogTitleProps {}
|
||||
|
||||
const DialogHeader = ({
|
||||
children,
|
||||
className,
|
||||
position,
|
||||
variant,
|
||||
asChild,
|
||||
...props
|
||||
}: IDialogHeaderProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(dialogHeaderVariants({ position, variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{asChild ? (
|
||||
children
|
||||
) : (
|
||||
<DialogHeaderDefaultLayout>{children}</DialogHeaderDefaultLayout>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DialogComponent = Object.assign(Dialog, {
|
||||
Trigger: DialogTrigger,
|
||||
Header: DialogHeader,
|
||||
Content: DialogContent,
|
||||
Description: DialogDescription,
|
||||
Footer: DialogFooter,
|
||||
});
|
||||
|
||||
export { DialogComponent as Dialog };
|
||||
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-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 DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b data-[vaul-drawer-direction=top]:border-b-2",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t data-[vaul-drawer-direction=bottom]:border-t-2",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l-2 data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r-2 data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-foreground mx-auto mt-4 hidden h-1 w-[60px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground text-xl font-head font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DrawerComponent = Object.assign(Drawer, {
|
||||
Trigger: DrawerTrigger,
|
||||
Portal: DrawerPortal,
|
||||
Overlay: DrawerOverlay,
|
||||
Close: DrawerClose,
|
||||
Content: DrawerContent,
|
||||
Header: DrawerHeader,
|
||||
Footer: DrawerFooter,
|
||||
Title: DrawerTitle,
|
||||
Description: DrawerDescription,
|
||||
});
|
||||
|
||||
export { DrawerComponent as Drawer };
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Text } from "@/components/retroui/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Ghost } from "lucide-react";
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
interface IEmptyProps extends HTMLAttributes<HTMLDivElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Empty = ({ className, ...props }: IEmptyProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center p-4 md:p-8 border-2 rounded shadow-md transition-all hover:shadow-none bg-card text-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Empty.displayName = "Empty";
|
||||
|
||||
const EmptyContent = ({ className, ...props }: IEmptyProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col items-center gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
EmptyContent.displayName = "Empty.Content";
|
||||
|
||||
const EmptyIcon = ({ children, className, ...props }: IEmptyProps) => {
|
||||
return (
|
||||
<div className={cn(className)} {...props}>
|
||||
{children || <Ghost className="w-full h-full" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
EmptyIcon.displayName = "Empty.Icon";
|
||||
|
||||
const EmptyTitle = ({ className, ...props }: IEmptyProps) => {
|
||||
return (
|
||||
<Text
|
||||
as="h3"
|
||||
className={cn("text-lg md:text-2xl font-bold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
EmptyTitle.displayName = "Empty.Title";
|
||||
|
||||
const EmptySeparator = ({ className, ...props }: IEmptyProps) => {
|
||||
return <div role="separator" className={cn("w-full h-1 bg-primary", className)} {...props} />;
|
||||
};
|
||||
EmptySeparator.displayName = "Empty.Separator";
|
||||
|
||||
const EmptyDescription = ({
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p
|
||||
className={cn("text-muted-foreground max-w-[320px]", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
EmptyDescription.displayName = "Empty.Description";
|
||||
|
||||
const EmptyComponent = Object.assign(Empty, {
|
||||
Content: EmptyContent,
|
||||
Icon: EmptyIcon,
|
||||
Title: EmptyTitle,
|
||||
Separator: EmptySeparator,
|
||||
Description: EmptyDescription,
|
||||
});
|
||||
|
||||
export { EmptyComponent as Empty };
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
variant?: "primary" | "outline" | "link";
|
||||
}
|
||||
|
||||
export function IconButton({
|
||||
children,
|
||||
size = "md",
|
||||
className = "",
|
||||
variant = "primary",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const sizeClasses = {
|
||||
sm: "p-2",
|
||||
md: "p-3",
|
||||
lg: "p-4",
|
||||
};
|
||||
|
||||
const variantClasses = {
|
||||
primary:
|
||||
"bg-primary text-black border-2 border-black hover:bg-primary-hover",
|
||||
outline: "bg-transparent text-black border-2 border-black",
|
||||
link: "bg-transparent text-primary hover:underline",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`font-head border-2 border-black shadow-md hover:shadow-xs transition-all ${sizeClasses[size]} ${variantClasses[variant]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { InputHTMLAttributes } from "react";
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Input: React.FC<InputProps> = ({
|
||||
type = "text",
|
||||
placeholder = "Enter text",
|
||||
className = "",
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
className={`px-4 py-2 w-full rounded border-2 shadow-md transition focus:outline-hidden focus:shadow-xs ${
|
||||
props["aria-invalid"]
|
||||
? "border-destructive text-destructive shadow-xs shadow-destructive"
|
||||
: ""
|
||||
} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
export const Label = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) => (
|
||||
<LabelPrimitive.Root className={cn(labelVariants(), className)} {...props} />
|
||||
);
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const loaderVariants = cva("flex gap-1", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "[&>div]:bg-primary [&>div]:border-border",
|
||||
secondary:
|
||||
"[&>div]:bg-secondary [&>div]:border-border",
|
||||
outline: "[&>div]:bg-transparent [&>div]:border-border",
|
||||
},
|
||||
size: {
|
||||
sm: "[&>div]:w-2 [&>div]:h-2",
|
||||
md: "[&>div]:w-3 [&>div]:h-3",
|
||||
lg: "[&>div]:w-4 [&>div]:h-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
interface LoaderProps
|
||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
|
||||
VariantProps<typeof loaderVariants> {
|
||||
asChild?: boolean;
|
||||
count?: number; // number of bouncing dots
|
||||
duration?: number; // animation duration in seconds
|
||||
delayStep?: number; // delay in ms
|
||||
}
|
||||
|
||||
const Loader = React.forwardRef<HTMLDivElement, LoaderProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
count = 3,
|
||||
duration = 0.5,
|
||||
delayStep = 100,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(loaderVariants({ variant, size }), className)}
|
||||
ref={ref}
|
||||
role="status"
|
||||
aria-label="Loading..."
|
||||
{...props}
|
||||
>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border-2 animate-bounce"
|
||||
style={{
|
||||
animationDuration: `${duration}s`,
|
||||
animationIterationCount: "infinite",
|
||||
animationDelay: `${i * delayStep}ms`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Loader.displayName = "Loader";
|
||||
export { Loader };
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { Text } from "./Text";
|
||||
import { Input } from "./Input";
|
||||
import { Button } from "./Button";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<div className="max-w-80 p-4 border-2 border-black shadow-md space-y-3">
|
||||
<div className="text-center">
|
||||
<Text as="h2">Login</Text>
|
||||
</div>
|
||||
<form className="flex flex-col gap-5 ">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<label htmlFor="email" className="w-11/12">
|
||||
Email
|
||||
</label>
|
||||
<Input id="email" type="email" placeholder="email" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<label htmlFor="password" className="w-11/12">
|
||||
Password
|
||||
</label>
|
||||
<Input id="password" type="password" placeholder="password" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center py-4 gap-1">
|
||||
<Button className="rounded-full active:shadow-none active:scale-95">
|
||||
Submit
|
||||
</Button>
|
||||
<div className="px-4 py-2 space-x-1">
|
||||
<span>Don't have an account?</span>
|
||||
<Link href="/" className="text-primary-700 hover:underline">
|
||||
sign-in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import React, { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
const Menu = DropdownMenu.Root;
|
||||
const Trigger = DropdownMenu.Trigger;
|
||||
|
||||
interface IMenuContent
|
||||
extends ComponentPropsWithoutRef<typeof DropdownMenu.Content> {}
|
||||
|
||||
const Content = ({ className, ...props }: IMenuContent) => (
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
side="bottom"
|
||||
align="start"
|
||||
className={cn(
|
||||
"bg-white border-2 shadow-md absolute top-2 min-w-20",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenu.Portal>
|
||||
);
|
||||
|
||||
const MenuItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentPropsWithoutRef<typeof DropdownMenu.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenu.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative text-black flex cursor-default select-none items-center rounded-xs px-2 py-1.5 text-sm outline-hidden transition-colors hover:bg-primary focus:bg-primary data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenuItem.displayName = "MenuItem";
|
||||
|
||||
const MenuComponent = Object.assign(Menu, {
|
||||
Trigger,
|
||||
Content,
|
||||
Item: MenuItem,
|
||||
});
|
||||
|
||||
export { MenuComponent as Menu };
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
|
||||
const popoverContentVariants = cva(
|
||||
"z-50 w-72 border-2 bg-background p-4 text-popover-foreground outline-none 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
|
||||
);
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> &
|
||||
VariantProps<typeof popoverContentVariants>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
popoverContentVariants({
|
||||
className,
|
||||
}),
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
const PopoverObject = Object.assign(Popover, {
|
||||
Trigger: PopoverTrigger,
|
||||
Content: PopoverContent,
|
||||
Anchor: PopoverAnchor,
|
||||
});
|
||||
|
||||
export { PopoverObject as Popover };
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export function ProductCard() {
|
||||
return (
|
||||
<div className="max-w-80 p-4 border-2 border-black">
|
||||
<img
|
||||
alt="product image"
|
||||
src="https://images.unsplash.com/photo-1596998791568-386ef5297c2e?q=80&w=2960&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
/>
|
||||
<div className="mt-4 space-y-4">
|
||||
<h3 className="font-head text-3xl font-medium mb-1">Classic Gameboy</h3>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-semibold">$29.99</span>
|
||||
<Button>Add To Cart</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden bg-background border-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
@@ -0,0 +1,82 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as RadioPrimitive from "@radix-ui/react-radio-group";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
|
||||
const radioVariants = cva("border-border border-2", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "",
|
||||
outline: "",
|
||||
solid: "",
|
||||
},
|
||||
size: {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-5 w-5",
|
||||
lg: "h-6 w-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
const radioIndicatorVariants = cva("flex ", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary border-2 border-border",
|
||||
outline: "border-2 border-border",
|
||||
solid: "bg-border",
|
||||
},
|
||||
size: {
|
||||
sm: "h-2 w-2",
|
||||
md: "h-2.5 w-2.5",
|
||||
lg: "h-3.5 w-3.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
interface RadioGroupProps
|
||||
extends React.ComponentProps<typeof RadioPrimitive.Root> {}
|
||||
|
||||
export const RadioGroupRoot = ({ className, ...props }: RadioGroupProps) => (
|
||||
<RadioPrimitive.Root className={cn("grid gap-2", className)} {...props} />
|
||||
);
|
||||
|
||||
interface RadioProps
|
||||
extends React.ComponentProps<typeof RadioPrimitive.Item>,
|
||||
VariantProps<typeof radioVariants> {}
|
||||
|
||||
export const RadioItem = ({
|
||||
children,
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
...props
|
||||
}: RadioProps) => (
|
||||
<RadioPrimitive.Item
|
||||
{...props}
|
||||
className={cn(
|
||||
radioVariants({
|
||||
size,
|
||||
variant,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<RadioPrimitive.Indicator className="flex justify-center items-center">
|
||||
<span className={radioIndicatorVariants({ size, variant })}></span>
|
||||
</RadioPrimitive.Indicator>
|
||||
{children}
|
||||
</RadioPrimitive.Item>
|
||||
);
|
||||
|
||||
const RadioComponent = Object.assign(RadioGroupRoot, {
|
||||
Item: RadioItem,
|
||||
});
|
||||
|
||||
export { RadioComponent as RadioGroup };
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectTrigger = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.SelectTriggerProps) => {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
className={cn(
|
||||
"flex h-10 rounded min-w-40 items-center shadow-md focus:shadow-xs justify-between border-2 border-input border-border bg-transparent px-4 py-2 placeholder:text-muted-foreground outline-none focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="ml-2 h-4 w-4" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectIcon = SelectPrimitive.Icon;
|
||||
|
||||
const SelectContent = ({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: SelectPrimitive.SelectContentProps) => {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden border border-border bg-background text-foreground shadow-md 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton className="flex cursor-default items-center justify-center py-1 text-muted-foreground">
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectPrimitive.ScrollDownButton className="flex cursor-default items-center justify-center py-1 text-muted-foreground">
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectItem = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.SelectItemProps) => (
|
||||
<SelectPrimitive.Item
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center py-1.5 px-2 outline-none data-[highlighted]:bg-primary data-[highlighted]:text-primary-foreground focus:bg-primary focus:text-primary-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4 text-foreground" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
const SelectLabel = SelectPrimitive.Label;
|
||||
const SelectSeparator = SelectPrimitive.Separator;
|
||||
|
||||
const SelectObj = Object.assign(Select, {
|
||||
Trigger: SelectTrigger,
|
||||
Value: SelectValue,
|
||||
Icon: SelectIcon,
|
||||
Content: SelectContent,
|
||||
Group: SelectGroup,
|
||||
Item: SelectItem,
|
||||
Label: SelectLabel,
|
||||
Separator: SelectSeparator,
|
||||
});
|
||||
|
||||
export { SelectObj as Select };
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-3 w-full grow overflow-hidden bg-background border-2">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4.5 w-4.5 border-2 bg-background shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<Sonner
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"h-auto w-full p-4 bg-background border group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border flex items-center relative",
|
||||
description:
|
||||
"group-[.toast]:text-muted-foreground ml-2 text-sm font-sans",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground py-1 px-2 bg-background border-border shadow hover:shadow-xs hover:translate-[2px] duration-200 transition-all focus:shadow-none border-2 ml-auto h-fit min-w-fit",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-foreground py-1 px-2 text-sm bg-background border-border shadow hover:shadow-xs hover:translate-[2px] duration-200 transition-all focus:shadow-none border-2 ml-auto h-fit min-w-fit",
|
||||
title: "ml-2 font-sans",
|
||||
closeButton:
|
||||
"absolute bg-background -top-1 -left-1 rounded-full p-0.5",
|
||||
},
|
||||
unstyled: true,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = ({ className, ...props }: SwitchPrimitives.SwitchProps) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer border-2 border-foreground items-center disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 bg-primary border-2 mx-0.5 border-foreground ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 data-[state=checked]:bg-background",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
);
|
||||
|
||||
export { Switch };
|
||||
@@ -0,0 +1,62 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabListProps,
|
||||
TabPanel,
|
||||
TabPanelProps,
|
||||
TabPanels,
|
||||
TabProps,
|
||||
} from "@headlessui/react";
|
||||
|
||||
const Tabs = TabGroup;
|
||||
const TabsPanels = TabPanels;
|
||||
|
||||
interface ITabsTriggerList extends TabListProps {
|
||||
className?: string;
|
||||
}
|
||||
const TabsTriggerList = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ITabsTriggerList) => {
|
||||
return (
|
||||
<TabList className={cn("flex flex-row space-x-2 w-full", className)} {...props}>
|
||||
{children}
|
||||
</TabList>
|
||||
);
|
||||
};
|
||||
|
||||
interface ITabsTrigger extends TabProps {
|
||||
className?: string;
|
||||
}
|
||||
const TabsTrigger = ({ children, className, ...props }: ITabsTrigger) => {
|
||||
return (
|
||||
<Tab
|
||||
className={cn(
|
||||
"px-4 py-1 border-2 border-transparent data-selected:border-border data-selected:bg-primary data-selected:text-primary-foreground data-selected:font-semibold focus:outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Tab>
|
||||
);
|
||||
};
|
||||
|
||||
interface ITabsContent extends TabPanelProps {
|
||||
className?: string;
|
||||
}
|
||||
const TabsContent = ({ children, className, ...props }: ITabsContent) => {
|
||||
return (
|
||||
<TabPanel
|
||||
className={cn("border-2 border-border mt-2 p-4 w-full", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export { Tabs, TabsPanels, TabsTrigger, TabsContent, TabsTriggerList };
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm border-2 shadow-lg", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b bg-primary text-primary-foreground font-head", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-accent text-accent-foreground font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-primary/50 hover:text-primary-foreground data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 md:h-12 px-4 text-left align-middle font-medium text-primary-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-2 md:p-3 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("my-2 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
const TableObj = Object.assign(Table, {
|
||||
Header: TableHeader,
|
||||
Body: TableBody,
|
||||
Footer: TableFooter,
|
||||
Row: TableRow,
|
||||
Head: TableHead,
|
||||
Cell: TableCell,
|
||||
Caption: TableCaption,
|
||||
})
|
||||
|
||||
export {
|
||||
TableObj as Table,
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface TOCItem {
|
||||
title: string;
|
||||
url: string;
|
||||
level: number;
|
||||
items?: TOCItem[];
|
||||
}
|
||||
|
||||
interface ManualTOCItem {
|
||||
id: string;
|
||||
title: string;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
interface TableOfContentsProps {
|
||||
depth?: number;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
data?: ManualTOCItem[];
|
||||
}
|
||||
|
||||
function generateTOCFromDOM(depth: number = 6): TOCItem[] {
|
||||
const headings: NodeListOf<HTMLHeadingElement> = document.querySelectorAll(
|
||||
Array.from({ length: depth }, (_, i) => `h${i + 1}`).join(', ')
|
||||
);
|
||||
|
||||
const items: TOCItem[] = [];
|
||||
const stack: TOCItem[] = [];
|
||||
|
||||
headings.forEach((heading) => {
|
||||
const level = parseInt(heading.tagName.charAt(1));
|
||||
const id = heading.id || heading.textContent?.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '') || '';
|
||||
|
||||
if (!heading.id && id) {
|
||||
heading.id = id;
|
||||
}
|
||||
|
||||
const item: TOCItem = {
|
||||
title: heading.textContent || '',
|
||||
url: `#${id}`,
|
||||
level,
|
||||
};
|
||||
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length === 0) {
|
||||
items.push(item);
|
||||
} else {
|
||||
const parent = stack[stack.length - 1];
|
||||
if (!parent.items) parent.items = [];
|
||||
parent.items.push(item);
|
||||
}
|
||||
|
||||
stack.push(item);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function convertManualDataToTOC(data: ManualTOCItem[]): TOCItem[] {
|
||||
const items: TOCItem[] = [];
|
||||
const stack: TOCItem[] = [];
|
||||
|
||||
data.forEach((item) => {
|
||||
const tocItem: TOCItem = {
|
||||
title: item.title,
|
||||
url: `#${item.id}`,
|
||||
level: item.depth,
|
||||
};
|
||||
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= item.depth) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length === 0) {
|
||||
items.push(tocItem);
|
||||
} else {
|
||||
const parent = stack[stack.length - 1];
|
||||
if (!parent.items) parent.items = [];
|
||||
parent.items.push(tocItem);
|
||||
}
|
||||
|
||||
stack.push(tocItem);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function renderTOCItems(items: TOCItem[], level = 0, activeId: string | null) {
|
||||
if (!items || items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<ul className={`space-y-1 ${level > 0 ? 'ml-4 mt-1' : ''}`}>
|
||||
{items.map((item, index) => {
|
||||
const isActive = activeId === item.url.substring(1);
|
||||
return (
|
||||
<li key={index}>
|
||||
<a
|
||||
href={item.url}
|
||||
className={`text-sm max-w-full truncate transition-colors block py-1 border-l-2 pl-2 ${
|
||||
isActive
|
||||
? 'text-accent-foreground border-border bg-accent'
|
||||
: 'border-transparent hover:border-border hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
{item.items && renderTOCItems(item.items, level + 1, activeId)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableOfContents({
|
||||
depth = 2,
|
||||
className = "",
|
||||
children,
|
||||
data,
|
||||
}: TableOfContentsProps) {
|
||||
const [tocItems, setTocItems] = useState<TOCItem[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const items = convertManualDataToTOC(data);
|
||||
setTocItems(items);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = generateTOCFromDOM(depth);
|
||||
setTocItems(items);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const updatedItems = generateTOCFromDOM(depth);
|
||||
setTocItems(updatedItems);
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['id']
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [depth, data]);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveId(entry.target.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: '-20% 0% -35% 0%' }
|
||||
);
|
||||
|
||||
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
headings.forEach((heading) => observer.observe(heading));
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
if (tocItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("border-2 shadow-md p-4 rounded w-52 h-60 overflow-y-auto sidebar-scroll", className)}>
|
||||
{children}
|
||||
{renderTOCItems(tocItems, 0, activeId)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ElementType, HTMLAttributes } from "react";
|
||||
import { VariantProps, cva } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const textVariants = cva("font-head", {
|
||||
variants: {
|
||||
as: {
|
||||
p: "font-sans text-base",
|
||||
li: "font-sans text-base",
|
||||
a: "font-sans text-base hover:underline underline-offset-2 decoration-primary",
|
||||
h1: "text-4xl lg:text-5xl font-bold",
|
||||
h2: "text-3xl lg:text-4xl font-semibold",
|
||||
h3: "text-2xl font-medium",
|
||||
h4: "text-xl font-normal",
|
||||
h5: "text-lg font-normal",
|
||||
h6: "text-base font-normal",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
as: "p",
|
||||
},
|
||||
});
|
||||
|
||||
interface TextProps
|
||||
extends Omit<HTMLAttributes<HTMLElement>, "className">,
|
||||
VariantProps<typeof textVariants> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Text = (props: TextProps) => {
|
||||
const { className, as, ...otherProps } = props;
|
||||
const Tag: ElementType = as || "p";
|
||||
|
||||
return (
|
||||
<Tag className={cn(textVariants({ as }), className)} {...otherProps} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Textarea({
|
||||
type = "text",
|
||||
placeholder = "Enter text...",
|
||||
className = "",
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<textarea
|
||||
placeholder={placeholder}
|
||||
rows={4}
|
||||
className={cn(
|
||||
"px-4 py-2 w-full border-2 rounded border-input shadow-md transition focus:outline-hidden focus:shadow-xs placeholder:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-transparent hover:bg-muted/70 hover:text-muted-foreground data-[state=on]:bg-muted",
|
||||
outlined:
|
||||
"border-2 border-input bg-transparent hover:bg-accent hover:text-accent-foreground/80 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
solid: "border-2 border-input bg-transparent hover:bg-secondary hover:text-secondary-foreground hover:border-secondary data-[state=on]:bg-secondary data-[state=on]:text-secondary-foreground data-[state=on]:border-secondary",
|
||||
"outline-muted":
|
||||
"border-2 border-input bg-transparent hover:hover:bg-muted/70 hover:hover:text-muted-foreground data-[state=on]:bg-muted",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3 min-w-10",
|
||||
sm: "h-9 px-2.5 min-w-9",
|
||||
lg: "h-11 px-5 min-w-11",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName;
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toggleVariants } from "./Toggle";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
const ToggleGroup = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("flex items-center justify-center gap-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
));
|
||||
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
||||
|
||||
const ToggleGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, children, variant, size, ...props }, ref) => {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
|
||||
const tooltipContentVariants = cva(
|
||||
"z-50 overflow-hidden border-2 border-border bg-background px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
primary: "bg-primary text-primary-foreground",
|
||||
solid: "bg-black text-white",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> &
|
||||
VariantProps<typeof tooltipContentVariants>
|
||||
>(({ className, sideOffset = 4, variant, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
tooltipContentVariants({
|
||||
variant,
|
||||
className,
|
||||
}),
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
const TooltipObject = Object.assign(Tooltip, {
|
||||
Trigger: TooltipTrigger,
|
||||
Content: TooltipContent,
|
||||
Provider: TooltipProvider,
|
||||
});
|
||||
|
||||
export { TooltipObject as Tooltip };
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import {
|
||||
Area,
|
||||
AreaChart as RechartsAreaChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts"
|
||||
|
||||
interface AreaChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
data: Record<string, any>[]
|
||||
index: string
|
||||
categories: string[]
|
||||
strokeColors?: string[]
|
||||
fillColors?: string[]
|
||||
tooltipBgColor?: string
|
||||
tooltipBorderColor?: string
|
||||
gridColor?: string
|
||||
valueFormatter?: (value: number) => string
|
||||
showGrid?: boolean
|
||||
showTooltip?: boolean
|
||||
fill?: "gradient" | "solid"
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AreaChart = React.forwardRef<HTMLDivElement, AreaChartProps>(
|
||||
(
|
||||
{
|
||||
data = [],
|
||||
index,
|
||||
categories = [],
|
||||
strokeColors = ["var(--foreground)"],
|
||||
fillColors = ["var(--primary)"],
|
||||
tooltipBgColor = "var(--background)",
|
||||
tooltipBorderColor = "var(--border)",
|
||||
gridColor = "var(--muted)",
|
||||
valueFormatter = (value: number) => value.toString(),
|
||||
showGrid = true,
|
||||
showTooltip = true,
|
||||
fill = "gradient",
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const chartId = React.useId()
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("h-80 w-full", className)} {...props}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsAreaChart data={data} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
{categories.map((category, index) => {
|
||||
const fillColor = fillColors[index] || fillColors[0]
|
||||
const gradientId = `gradient-${chartId}-${category}`
|
||||
return (
|
||||
<linearGradient key={category} id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
||||
{fill === "gradient" ? (
|
||||
<>
|
||||
<stop offset="5%" stopColor={fillColor} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={fillColor} stopOpacity={0} />
|
||||
</>
|
||||
) : (
|
||||
<stop stopColor={fillColor} stopOpacity={0.6} />
|
||||
)}
|
||||
</linearGradient>
|
||||
)
|
||||
})}
|
||||
</defs>
|
||||
|
||||
{showGrid && (
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} />
|
||||
)}
|
||||
|
||||
<XAxis
|
||||
dataKey={index}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={valueFormatter}
|
||||
/>
|
||||
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
content={({ active, payload, label }) => {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border p-2 shadow"
|
||||
style={{
|
||||
backgroundColor: tooltipBgColor,
|
||||
borderColor: tooltipBorderColor
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{index}
|
||||
</span>
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{entry.dataKey}
|
||||
</span>
|
||||
<span className="font-bold" style={{ color: strokeColors[0] }}>
|
||||
{valueFormatter(entry.value as number)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{categories.map((category, index) => {
|
||||
const strokeColor = strokeColors[index] || strokeColors[0]
|
||||
const gradientId = `gradient-${chartId}-${category}`
|
||||
|
||||
return (
|
||||
<Area
|
||||
key={category}
|
||||
dataKey={category}
|
||||
stroke={strokeColor}
|
||||
fill={`url(#${gradientId})`}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</RechartsAreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
AreaChart.displayName = "AreaChart"
|
||||
|
||||
export { AreaChart, type AreaChartProps }
|
||||
@@ -0,0 +1,164 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import {
|
||||
Bar,
|
||||
BarChart as RechartsBarChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts"
|
||||
|
||||
interface BarChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
data: Record<string, any>[]
|
||||
index: string
|
||||
categories: string[]
|
||||
strokeColors?: string[]
|
||||
fillColors?: string[]
|
||||
tooltipBgColor?: string
|
||||
tooltipBorderColor?: string
|
||||
gridColor?: string
|
||||
valueFormatter?: (value: number) => string
|
||||
showGrid?: boolean
|
||||
showTooltip?: boolean
|
||||
stacked?: boolean
|
||||
alignment?: "vertical" | "horizontal"
|
||||
className?: string
|
||||
}
|
||||
|
||||
const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
|
||||
(
|
||||
{
|
||||
data = [],
|
||||
index,
|
||||
categories = [],
|
||||
strokeColors = ["var(--foreground)"],
|
||||
fillColors = ["var(--primary)", "var(--secondary)"],
|
||||
tooltipBgColor = "var(--background)",
|
||||
tooltipBorderColor = "var(--border)",
|
||||
gridColor = "var(--muted)",
|
||||
valueFormatter = (value: number) => value.toString(),
|
||||
showGrid = true,
|
||||
showTooltip = true,
|
||||
stacked = false,
|
||||
alignment = "vertical",
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div ref={ref} className={cn("h-80 w-full", className)} {...props}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsBarChart
|
||||
data={data}
|
||||
layout={alignment === "horizontal" ? "vertical" : undefined}
|
||||
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
{showGrid && (
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} />
|
||||
)}
|
||||
|
||||
{alignment === "horizontal" ? (
|
||||
<>
|
||||
<XAxis
|
||||
type="number"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={valueFormatter}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey={index}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
width={80}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XAxis
|
||||
dataKey={index}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={valueFormatter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
content={({ active, payload, label }) => {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border p-2 shadow"
|
||||
style={{
|
||||
backgroundColor: tooltipBgColor,
|
||||
borderColor: tooltipBorderColor
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{index}
|
||||
</span>
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{entry.dataKey}
|
||||
</span>
|
||||
<span className="font-bold" style={{ color: strokeColors[0] }}>
|
||||
{valueFormatter(entry.value as number)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{categories.map((category, index) => {
|
||||
const fillColor = fillColors[index] || fillColors[0]
|
||||
const strokeColor = strokeColors[index] || strokeColors[0]
|
||||
|
||||
return (
|
||||
<Bar
|
||||
key={category}
|
||||
dataKey={category}
|
||||
fill={fillColor}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1}
|
||||
stackId={stacked ? "strokeId" : undefined} />
|
||||
)
|
||||
})}
|
||||
</RechartsBarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BarChart.displayName = "BarChart"
|
||||
|
||||
export { BarChart, type BarChartProps }
|
||||
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart as RechartsLineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts"
|
||||
|
||||
interface LineChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
data: Record<string, any>[]
|
||||
index: string
|
||||
categories: string[]
|
||||
strokeColors?: string[]
|
||||
tooltipBgColor?: string
|
||||
tooltipBorderColor?: string
|
||||
gridColor?: string
|
||||
valueFormatter?: (value: number) => string
|
||||
showGrid?: boolean
|
||||
showTooltip?: boolean
|
||||
strokeWidth?: number
|
||||
dotSize?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const LineChart = React.forwardRef<HTMLDivElement, LineChartProps>(
|
||||
(
|
||||
{
|
||||
data = [],
|
||||
index,
|
||||
categories = [],
|
||||
strokeColors = ["var(--foreground)"],
|
||||
tooltipBgColor = "var(--background)",
|
||||
tooltipBorderColor = "var(--border)",
|
||||
gridColor = "var(--muted)",
|
||||
valueFormatter = (value: number) => value.toString(),
|
||||
showGrid = true,
|
||||
showTooltip = true,
|
||||
strokeWidth = 2,
|
||||
dotSize = 4,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div ref={ref} className={cn("h-80 w-full", className)} {...props}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsLineChart data={data} margin={{ top: 0, right: 30, left: 0, bottom: 0 }}>
|
||||
{showGrid && (
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={gridColor} />
|
||||
)}
|
||||
|
||||
<XAxis
|
||||
dataKey={index}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
className="text-xs fill-muted-foreground"
|
||||
tickFormatter={valueFormatter}
|
||||
/>
|
||||
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
content={({ active, payload, label }) => {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border p-2 shadow"
|
||||
style={{
|
||||
backgroundColor: tooltipBgColor,
|
||||
borderColor: tooltipBorderColor
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{index}
|
||||
</span>
|
||||
<span className="font-bold text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{entry.dataKey}
|
||||
</span>
|
||||
<span className="font-bold" style={{ color: entry.color }}>
|
||||
{valueFormatter(entry.value as number)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{categories.map((category, index) => {
|
||||
const strokeColor = strokeColors[index] || strokeColors[0]
|
||||
|
||||
return (
|
||||
<Line
|
||||
key={category}
|
||||
dataKey={category}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
dot={{ r: dotSize, fill: strokeColor }}
|
||||
activeDot={{ r: dotSize + 2, fill: strokeColor }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</RechartsLineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
LineChart.displayName = "LineChart"
|
||||
|
||||
export { LineChart, type LineChartProps }
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import {
|
||||
Cell,
|
||||
Pie,
|
||||
PieChart as RechartsPieChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
} from "recharts"
|
||||
|
||||
interface PieChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
data: Record<string, any>[]
|
||||
dataKey: string
|
||||
nameKey: string
|
||||
colors?: string[]
|
||||
tooltipBgColor?: string
|
||||
tooltipBorderColor?: string
|
||||
valueFormatter?: (value: number) => string
|
||||
showTooltip?: boolean
|
||||
innerRadius?: number
|
||||
outerRadius?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const PieChart = React.forwardRef<HTMLDivElement, PieChartProps>(
|
||||
(
|
||||
{
|
||||
data = [],
|
||||
dataKey,
|
||||
nameKey,
|
||||
colors = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"],
|
||||
tooltipBgColor = "var(--background)",
|
||||
tooltipBorderColor = "var(--border)",
|
||||
valueFormatter = (value: number) => value.toString(),
|
||||
showTooltip = true,
|
||||
innerRadius = 0,
|
||||
outerRadius = 100,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div ref={ref} className={cn("h-80 w-full", className)} {...props}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsPieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
dataKey={dataKey}
|
||||
nameKey={nameKey}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
isAnimationActive={false}
|
||||
className="w-full h-full"
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={colors[index % colors.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
|
||||
{showTooltip && (
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
if (!active || !payload?.length) return null
|
||||
|
||||
const data = payload[0]
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border p-2 shadow"
|
||||
style={{
|
||||
backgroundColor: tooltipBgColor,
|
||||
borderColor: tooltipBorderColor
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
{data.name}
|
||||
</span>
|
||||
<span className="font-bold text-foreground">
|
||||
{valueFormatter(data.value as number)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</RechartsPieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
PieChart.displayName = "PieChart"
|
||||
|
||||
export { PieChart, type PieChartProps }
|
||||
@@ -0,0 +1,29 @@
|
||||
export * from "./Button";
|
||||
export * from "./Input";
|
||||
export * from "./Textarea";
|
||||
export * from "./Checkbox";
|
||||
export * from "./Radio";
|
||||
export * from "./Select";
|
||||
export * from "./Switch";
|
||||
export * from "./Label";
|
||||
export * from "./Text";
|
||||
export * from "./Accordion";
|
||||
export * from "./Alert";
|
||||
export * from "./Card";
|
||||
export * from "./Avatar";
|
||||
export * from "./Badge";
|
||||
export * from "./Tab";
|
||||
export * from "./Dialog";
|
||||
export * from "./Menu";
|
||||
export * from "./Progress";
|
||||
export * from "./Popover";
|
||||
export * from "./Toggle";
|
||||
export * from "./ToggleGroup";
|
||||
export * from "./Sonner";
|
||||
export * from "./Tooltip";
|
||||
export * from "./Breadcrumb";
|
||||
export * from "./CommandDisplay";
|
||||
export * from "./Command";
|
||||
export * from "./Loader";
|
||||
export * from "./ContextMenu";
|
||||
export * from "./Empty";
|
||||
@@ -0,0 +1,743 @@
|
||||
import { lazy } from "react";
|
||||
|
||||
export const componentConfig: {
|
||||
core: {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
filePath: string;
|
||||
dependencies?: string[];
|
||||
preview?: React.LazyExoticComponent<() => JSX.Element>;
|
||||
};
|
||||
};
|
||||
examples: {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
filePath: string;
|
||||
preview: React.LazyExoticComponent<() => JSX.Element>;
|
||||
};
|
||||
};
|
||||
} = {
|
||||
core: {
|
||||
accordion: {
|
||||
name: "accordion",
|
||||
dependencies: ["@radix-ui/react-accordion", "lucide-react"],
|
||||
filePath: "components/retroui/Accordion.tsx",
|
||||
},
|
||||
alert: {
|
||||
name: "alert",
|
||||
filePath: "components/retroui/Alert.tsx",
|
||||
},
|
||||
areaChart: {
|
||||
name: "areaChart",
|
||||
filePath: "components/retroui/charts/AreaChart.tsx",
|
||||
},
|
||||
avatar: {
|
||||
name: "avatar",
|
||||
filePath: "components/retroui/Avatar.tsx",
|
||||
},
|
||||
badge: {
|
||||
name: "badge",
|
||||
filePath: "components/retroui/Badge.tsx",
|
||||
},
|
||||
barChart: {
|
||||
name: "barChart",
|
||||
filePath: "components/retroui/charts/BarChart.tsx",
|
||||
},
|
||||
breadcrumb: {
|
||||
name: "breadcrumb",
|
||||
filePath: "components/retroui/Breadcrumb.tsx",
|
||||
},
|
||||
button: {
|
||||
name: "button",
|
||||
filePath: "components/retroui/Button.tsx",
|
||||
},
|
||||
calendar: {
|
||||
name: "calendar",
|
||||
filePath: "components/retroui/Calendar.tsx",
|
||||
},
|
||||
card: {
|
||||
name: "card",
|
||||
filePath: "components/retroui/Card.tsx",
|
||||
},
|
||||
carousel: {
|
||||
name: "carousel",
|
||||
filePath: "components/retroui/Carousel.tsx",
|
||||
},
|
||||
checkbox: {
|
||||
name: "checkbox",
|
||||
filePath: "components/retroui/Checkbox.tsx",
|
||||
},
|
||||
command: {
|
||||
name: "command",
|
||||
filePath: "components/retroui/Command.tsx",
|
||||
},
|
||||
"context-menu": {
|
||||
name: "context-menu",
|
||||
filePath: "components/retroui/ContextMenu.tsx",
|
||||
},
|
||||
dialog: {
|
||||
name: "dialog",
|
||||
filePath: "components/retroui/Dialog.tsx",
|
||||
},
|
||||
drawer: {
|
||||
name: "drawer",
|
||||
filePath: "components/retroui/Drawer.tsx",
|
||||
},
|
||||
empty: {
|
||||
name: "empty",
|
||||
filePath: "components/retroui/Empty.tsx",
|
||||
},
|
||||
input: {
|
||||
name: "input",
|
||||
filePath: "components/retroui/Input.tsx",
|
||||
},
|
||||
label: {
|
||||
name: "label",
|
||||
filePath: "components/retroui/Label.tsx",
|
||||
},
|
||||
lineChart: {
|
||||
name: "lineChart",
|
||||
filePath: "components/retroui/charts/LineChart.tsx",
|
||||
},
|
||||
loader: {
|
||||
name: "loader",
|
||||
filePath: "components/retroui/Loader.tsx",
|
||||
},
|
||||
menu: {
|
||||
name: "menu",
|
||||
filePath: "components/retroui/Menu.tsx",
|
||||
},
|
||||
pieChart: {
|
||||
name: "pieChart",
|
||||
filePath: "components/retroui/charts/PieChart.tsx",
|
||||
},
|
||||
popover: {
|
||||
name: "popover",
|
||||
filePath: "components/retroui/Popover.tsx",
|
||||
},
|
||||
progress: {
|
||||
name: "progress",
|
||||
filePath: "components/retroui/Progress.tsx",
|
||||
},
|
||||
radio: {
|
||||
name: "radio",
|
||||
filePath: "components/retroui/Radio.tsx",
|
||||
},
|
||||
select: {
|
||||
name: "select",
|
||||
filePath: "components/retroui/Select.tsx",
|
||||
},
|
||||
slider: {
|
||||
name: "slider",
|
||||
dependencies: ["@radix-ui/react-slider"],
|
||||
filePath: "components/retroui/Slider.tsx",
|
||||
},
|
||||
sonner: {
|
||||
name: "sonner",
|
||||
filePath: "components/retroui/Sonner.tsx",
|
||||
},
|
||||
switch: {
|
||||
name: "switch",
|
||||
filePath: "components/retroui/Switch.tsx",
|
||||
},
|
||||
table: {
|
||||
name: "table",
|
||||
filePath: "components/retroui/Table.tsx",
|
||||
},
|
||||
text: {
|
||||
name: "text",
|
||||
filePath: "components/retroui/Text.tsx",
|
||||
},
|
||||
toc: {
|
||||
name: "toc",
|
||||
filePath: "components/retroui/TableOfContents.tsx",
|
||||
},
|
||||
toggle: {
|
||||
name: "toggle",
|
||||
filePath: "components/retroui/Toggle.tsx",
|
||||
},
|
||||
"toggle-group": {
|
||||
name: "toggle-group",
|
||||
filePath: "components/retroui/ToggleGroup.tsx",
|
||||
},
|
||||
tooltip: {
|
||||
name: "tooltip",
|
||||
filePath: "components/retroui/Tooltip.tsx",
|
||||
},
|
||||
},
|
||||
examples: {
|
||||
"accordion-style-default": {
|
||||
name: "accordion-style-default",
|
||||
filePath: "preview/components/accordion-style-default.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/accordion-style-default"),
|
||||
),
|
||||
},
|
||||
"alert-style-all-status": {
|
||||
name: "alert-style-all-status",
|
||||
filePath: "preview/components/alert-style-all-status.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/alert-style-all-status"),
|
||||
),
|
||||
},
|
||||
"alert-style-default": {
|
||||
name: "alert-style-default",
|
||||
filePath: "preview/components/alert-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/alert-style-default")),
|
||||
},
|
||||
"alert-style-solid": {
|
||||
name: "alert-style-solid",
|
||||
filePath: "preview/components/alert-style-solid.tsx",
|
||||
preview: lazy(() => import("@/preview/components/alert-style-solid")),
|
||||
},
|
||||
"alert-style-with-icon": {
|
||||
name: "alert-style-with-icon",
|
||||
filePath: "preview/components/alert-style-with-icon.tsx",
|
||||
preview: lazy(() => import("@/preview/components/alert-style-with-icon")),
|
||||
},
|
||||
"area-chart-style-default": {
|
||||
name: "chart-style-default",
|
||||
filePath: "preview/charts/area-chart-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/area-chart-style-default")),
|
||||
},
|
||||
"area-chart-style-multiple": {
|
||||
name: "area-chart-style-multiple",
|
||||
filePath: "preview/charts/area-chart-style-multiple.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/area-chart-style-multiple")),
|
||||
},
|
||||
"area-chart-style-stacked": {
|
||||
name: "area-chart-style-stacked",
|
||||
filePath: "preview/charts/area-chart-style-stacked.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/area-chart-style-stacked")),
|
||||
},
|
||||
"avatar-style-circle": {
|
||||
name: "avatar-style-circle",
|
||||
filePath: "preview/components/avatar-style-circle.tsx",
|
||||
preview: lazy(() => import("@/preview/components/avatar-style-circle")),
|
||||
},
|
||||
"avatar-style-circle-fallbacks": {
|
||||
name: "avatar-style-fallbacks",
|
||||
filePath: "preview/components/avatar-style-circle-fallbacks.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/avatar-style-circle-fallbacks"),
|
||||
),
|
||||
},
|
||||
"avatar-style-circle-sizes": {
|
||||
name: "avatar-style-circle-sizes",
|
||||
filePath: "preview/components/avatar-style-circle-sizes.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/avatar-style-circle-sizes"),
|
||||
),
|
||||
},
|
||||
"badge-style-default": {
|
||||
name: "badge-style-default",
|
||||
filePath: "preview/components/badge-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/badge-style-default")),
|
||||
},
|
||||
"badge-style-rounded": {
|
||||
name: "badge-style-default",
|
||||
filePath: "preview/components/badge-style-rounded.tsx",
|
||||
preview: lazy(() => import("@/preview/components/badge-style-rounded")),
|
||||
},
|
||||
"badge-style-sizes": {
|
||||
name: "badge-style-sizes",
|
||||
filePath: "preview/components/badge-style-sizes.tsx",
|
||||
preview: lazy(() => import("@/preview/components/badge-style-sizes")),
|
||||
},
|
||||
"badge-style-variants": {
|
||||
name: "badge-style-variants",
|
||||
filePath: "preview/components/badge-style-variants.tsx",
|
||||
preview: lazy(() => import("@/preview/components/badge-style-variants")),
|
||||
},
|
||||
"bar-chart-style-default": {
|
||||
name: "bar-chart-style-default",
|
||||
filePath: "preview/charts/bar-chart-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/bar-chart-style-default")),
|
||||
},
|
||||
"bar-chart-style-grouped": {
|
||||
name: "bar-chart-style-grouped",
|
||||
filePath: "preview/charts/bar-chart-style-grouped.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/bar-chart-style-grouped")),
|
||||
},
|
||||
"bar-chart-style-horizontal": {
|
||||
name: "bar-chart-style-horizontal",
|
||||
filePath: "preview/charts/bar-chart-style-horizontal.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/charts/bar-chart-style-horizontal"),
|
||||
),
|
||||
},
|
||||
"bar-chart-style-multiple": {
|
||||
name: "bar-chart-style-multiple",
|
||||
filePath: "preview/charts/bar-chart-style-multiple.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/bar-chart-style-multiple")),
|
||||
},
|
||||
"bar-chart-style-stacked": {
|
||||
name: "bar-chart-style-stacked",
|
||||
filePath: "preview/charts/bar-chart-style-stacked.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/bar-chart-style-stacked")),
|
||||
},
|
||||
"breadcrumb-custom-separator": {
|
||||
name: "breadcrumb-custom-separator",
|
||||
filePath: "preview/components/breadcrumb-custom-separator.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/breadcrumb-custom-separator"),
|
||||
),
|
||||
},
|
||||
"breadcrumb-link-component": {
|
||||
name: "breadcrumb-link-component",
|
||||
filePath: "preview/components/breadcrumb-link-component.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/breadcrumb-link-component"),
|
||||
),
|
||||
},
|
||||
"breadcrumb-style-collapsed": {
|
||||
name: "breadcrumb-style-collapsed",
|
||||
filePath: "preview/components/breadcrumb-style-collapsed.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/breadcrumb-style-collapsed"),
|
||||
),
|
||||
},
|
||||
"breadcrumb-style-default": {
|
||||
name: "breadcrumb-style-default",
|
||||
filePath: "preview/components/breadcrumb-style-default.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/breadcrumb-style-default"),
|
||||
),
|
||||
},
|
||||
"button-style-default": {
|
||||
name: "button-style-default",
|
||||
filePath: "preview/components/button-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/button-style-default")),
|
||||
},
|
||||
"button-style-icon": {
|
||||
name: "button-style-icon",
|
||||
filePath: "preview/components/button-style-icon.tsx",
|
||||
preview: lazy(() => import("@/preview/components/button-style-icon")),
|
||||
},
|
||||
"button-style-link": {
|
||||
name: "button-style-link",
|
||||
filePath: "preview/components/button-style-link.tsx",
|
||||
preview: lazy(() => import("@/preview/components/button-style-link")),
|
||||
},
|
||||
"button-style-outline": {
|
||||
name: "button-style-default",
|
||||
filePath: "preview/components/button-style-outline.tsx",
|
||||
preview: lazy(() => import("@/preview/components/button-style-outline")),
|
||||
},
|
||||
"button-style-secondary": {
|
||||
name: "button-style-secondary",
|
||||
filePath: "preview/components/button-style-secondary.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/button-style-secondary"),
|
||||
),
|
||||
},
|
||||
"button-style-with-icon": {
|
||||
name: "button-style-with-icon",
|
||||
filePath: "preview/components/button-style-with-icon.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/button-style-with-icon"),
|
||||
),
|
||||
},
|
||||
"calendar-style-default": {
|
||||
name: "calendar-style-default",
|
||||
filePath: "preview/components/calendar-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/calendar-style-default")),
|
||||
},
|
||||
"card-style-commerce": {
|
||||
name: "card-style-commerce",
|
||||
filePath: "preview/components/card-style-commerce.tsx",
|
||||
preview: lazy(() => import("@/preview/components/card-style-commerce")),
|
||||
},
|
||||
"card-style-default": {
|
||||
name: "card-style-default",
|
||||
filePath: "preview/components/card-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/card-style-default")),
|
||||
},
|
||||
"card-style-testimonial": {
|
||||
name: "card-style-testimonial",
|
||||
filePath: "preview/components/card-style-testimonial.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/card-style-testimonial"),
|
||||
),
|
||||
},
|
||||
"carousel-style-default": {
|
||||
name: "carousel-style-default",
|
||||
filePath: "preview/components/carousel-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/carousel-style-default")),
|
||||
},
|
||||
"carousel-style-sizes": {
|
||||
name: "carousel-style-sizes",
|
||||
filePath: "preview/components/carousel-style-sizes.tsx",
|
||||
preview: lazy(() => import("@/preview/components/carousel-style-sizes")),
|
||||
},
|
||||
"carousel-style-vertical": {
|
||||
name: "carousel-style-vertical",
|
||||
filePath: "preview/components/carousel-style-vertical.tsx",
|
||||
preview: lazy(() => import("@/preview/components/carousel-style-vertical")),
|
||||
},
|
||||
"checkbox-style-default": {
|
||||
name: "checkbox-style-default",
|
||||
filePath: "preview/components/checkbox-style-default.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/checkbox-style-default"),
|
||||
),
|
||||
},
|
||||
"checkbox-style-sizes": {
|
||||
name: "checkbox-style-default",
|
||||
filePath: "preview/components/checkbox-style-sizes.tsx",
|
||||
preview: lazy(() => import("@/preview/components/checkbox-style-sizes")),
|
||||
},
|
||||
"checkbox-style-variants": {
|
||||
name: "checkbox-style-toggle",
|
||||
filePath: "preview/components/checkbox-style-variants.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/checkbox-style-variants"),
|
||||
),
|
||||
},
|
||||
"command-style-default": {
|
||||
name: "command-style-default",
|
||||
filePath: "preview/components/command-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/command-style-default")),
|
||||
},
|
||||
"command-style-dialog": {
|
||||
name: "command-style-dialog",
|
||||
filePath: "preview/components/command-style-dialog.tsx",
|
||||
preview: lazy(() => import("@/preview/components/command-style-dialog")),
|
||||
},
|
||||
"context-menu-style-default": {
|
||||
name: "context-menu-style-default",
|
||||
filePath: "preview/components/context-menu-style-default.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/context-menu-style-default"),
|
||||
),
|
||||
},
|
||||
"dialog-style-default": {
|
||||
name: "dialog-style-default",
|
||||
filePath: "preview/components/dialog-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/dialog-style-default")),
|
||||
},
|
||||
"dialog-style-width-variant": {
|
||||
name: "dialog-style-width-variant",
|
||||
filePath: "preview/components/dialog-style-width-variant.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/dialog-style-width-variant"),
|
||||
),
|
||||
},
|
||||
"dialog-style-with-footer": {
|
||||
name: "dialog-style-with-footer",
|
||||
filePath: "preview/components/dialog-style-with-footer.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/dialog-style-with-footer"),
|
||||
),
|
||||
},
|
||||
"dialog-style-with-form": {
|
||||
name: "dialog-style-with-form",
|
||||
filePath: "preview/components/dialog-style-with-form.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/dialog-style-with-form"),
|
||||
),
|
||||
},
|
||||
"drawer-style-default": {
|
||||
name: "drawer-style-default",
|
||||
filePath: "preview/components/drawer-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/drawer-style-default")),
|
||||
},
|
||||
"drawer-style-right-direction": {
|
||||
name: "drawer-style-right-direction",
|
||||
filePath: "preview/components/drawer-style-right-direction.tsx",
|
||||
preview: lazy(() => import("@/preview/components/drawer-style-right-direction")),
|
||||
},
|
||||
// "dropdown-style-default": {
|
||||
// name: "dropdown-style-default",
|
||||
// filePath: "preview/components/dropdown-style-default.tsx",
|
||||
// preview: lazy(() => import("@/preview/components/dropdown-style-default")),
|
||||
// },
|
||||
"input-style-default": {
|
||||
name: "input-style-default",
|
||||
filePath: "preview/components/input-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/input-style-default")),
|
||||
},
|
||||
"input-style-error": {
|
||||
name: "input-style-error",
|
||||
filePath: "preview/components/input-style-error.tsx",
|
||||
preview: lazy(() => import("@/preview/components/input-style-error")),
|
||||
},
|
||||
"input-style-with-label": {
|
||||
name: "input-style-with-label",
|
||||
filePath: "preview/components/input-style-with-label.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/input-style-with-label"),
|
||||
),
|
||||
},
|
||||
"label-style-default": {
|
||||
name: "label-style-default",
|
||||
filePath: "preview/components/label-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/label-style-default")),
|
||||
},
|
||||
"line-chart-style-default": {
|
||||
name: "line-chart-style-default",
|
||||
filePath: "preview/charts/line-chart-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/line-chart-style-default")),
|
||||
},
|
||||
"line-chart-style-multiple": {
|
||||
name: "line-chart-style-multiple",
|
||||
filePath: "preview/charts/line-chart-style-multiple.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/line-chart-style-multiple")),
|
||||
},
|
||||
"loader-style-custom": {
|
||||
name: "loader-style-custom",
|
||||
filePath: "preview/components/loader-style-custom.tsx",
|
||||
preview: lazy(() => import("@/preview/components/loader-style-custom"))
|
||||
},
|
||||
"loader-style-default": {
|
||||
name: "loader-style-default",
|
||||
filePath: "preview/components/loader-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/loader-style-default")),
|
||||
},
|
||||
"loader-style-outline": {
|
||||
name: "loader-style-outline",
|
||||
filePath: "preview/components/loader-style-outline.tsx",
|
||||
preview: lazy(() => import("@/preview/components/loader-style-outline")),
|
||||
},
|
||||
"loader-style-sizes": {
|
||||
name: "loader-style-sizes",
|
||||
filePath: "preview/components/loader-style-sizes.tsx",
|
||||
preview: lazy(() => import("@/preview/components/loader-style-sizes")),
|
||||
},
|
||||
"loader-style-solid": {
|
||||
name: "loader-style-solid",
|
||||
filePath: "preview/components/loader-style-solid.tsx",
|
||||
preview: lazy(() => import("@/preview/components/loader-style-solid")),
|
||||
},
|
||||
"menu-style-default": {
|
||||
name: "menu-style-default",
|
||||
filePath: "preview/components/menu-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/menu-style-default")),
|
||||
},
|
||||
"pie-chart-style-default": {
|
||||
name: "pie-chart-style-default",
|
||||
filePath: "preview/charts/pie-chart-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/pie-chart-style-default")),
|
||||
},
|
||||
"pie-chart-style-donut": {
|
||||
name: "pie-chart-style-donut",
|
||||
filePath: "preview/charts/pie-chart-style-donut.tsx",
|
||||
preview: lazy(() => import("@/preview/charts/pie-chart-style-donut")),
|
||||
},
|
||||
"popover-style-default": {
|
||||
name: "popover-style-default",
|
||||
filePath: "preview/components/popover-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/popover-style-default")),
|
||||
},
|
||||
"popover-style-default-shadow": {
|
||||
name: "popover-style-default-shadow",
|
||||
filePath: "preview/components/popover-style-default-shadow.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/popover-style-default-shadow"),
|
||||
),
|
||||
},
|
||||
"progress-style-default": {
|
||||
name: "progress-style-default",
|
||||
filePath: "preview/components/progress-style-default.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/progress-style-default"),
|
||||
),
|
||||
},
|
||||
"radio-style-default": {
|
||||
name: "radio-style-default",
|
||||
filePath: "preview/components/radio-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/radio-style-default")),
|
||||
},
|
||||
"radio-style-sizes": {
|
||||
name: "radio-style-sizes",
|
||||
filePath: "preview/components/radio-style-sizes.tsx",
|
||||
preview: lazy(() => import("@/preview/components/radio-style-sizes")),
|
||||
},
|
||||
"radio-style-variants": {
|
||||
name: "radio-style-variants",
|
||||
filePath: "preview/components/radio-style-variants.tsx",
|
||||
preview: lazy(() => import("@/preview/components/radio-style-variants")),
|
||||
},
|
||||
"retro-player": {
|
||||
name: "retro-player",
|
||||
filePath: "preview/components/retro-player.tsx",
|
||||
preview: lazy(() => import("@/preview/components/retro-player"))
|
||||
},
|
||||
"select-style-default": {
|
||||
name: "select-style-default",
|
||||
filePath: "preview/components/select-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/select-style-default")),
|
||||
},
|
||||
"slider-style-default": {
|
||||
name: "slider-style-default",
|
||||
filePath: "preview/components/slider-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/slider-style-default")),
|
||||
},
|
||||
"sonner-style-colored-status": {
|
||||
name: "sonner-style-colored-status",
|
||||
filePath: "preview/components/sonner-style-colored-status.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/sonner-style-colored-status"),
|
||||
),
|
||||
},
|
||||
"sonner-style-default": {
|
||||
name: "sonner-style-default",
|
||||
filePath: "preview/components/sonner-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/sonner-style-default")),
|
||||
},
|
||||
"sonner-style-status": {
|
||||
name: "sonner-style-status",
|
||||
filePath: "preview/components/sonner-style-status.tsx",
|
||||
preview: lazy(() => import("@/preview/components/sonner-style-status")),
|
||||
},
|
||||
"switch-style-default": {
|
||||
name: "switch-style-default",
|
||||
filePath: "preview/components/switch-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/switch-style-default")),
|
||||
},
|
||||
"switch-style-disabled": {
|
||||
name: "switch-style-disabled",
|
||||
filePath: "preview/components/switch-style-disabled.tsx",
|
||||
preview: lazy(() => import("@/preview/components/switch-style-disabled")),
|
||||
},
|
||||
"tab-style-default": {
|
||||
name: "typography-p",
|
||||
filePath: "preview/components/tab-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/tab-style-default")),
|
||||
},
|
||||
"table-style-default": {
|
||||
name: "table-style-default",
|
||||
filePath: "preview/components/table-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/table-style-default")),
|
||||
},
|
||||
"table-with-checkbox": {
|
||||
name: "table-with-checkbox",
|
||||
filePath: "preview/components/table-with-checkbox.tsx",
|
||||
preview: lazy(() => import("@/preview/components/table-with-checkbox")),
|
||||
},
|
||||
"table-with-sticky-header": {
|
||||
name: "table-with-sticky-header",
|
||||
filePath: "preview/components/table-with-sticky-header.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/table-with-sticky-header"),
|
||||
),
|
||||
},
|
||||
"text-headings": {
|
||||
name: "text-headings",
|
||||
filePath: "preview/components/text-headings.tsx",
|
||||
preview: lazy(() => import("@/preview/components/text-headings")),
|
||||
},
|
||||
"textarea-style-default": {
|
||||
name: "textarea-style-default",
|
||||
filePath: "preview/components/textarea-style-default.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/textarea-style-default"),
|
||||
),
|
||||
},
|
||||
"toc-style-default": {
|
||||
name: "toc-style-default",
|
||||
filePath: "preview/components/toc-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/toc-style-default")),
|
||||
},
|
||||
"toc-style-depth": {
|
||||
name: "toc-style-depth",
|
||||
filePath: "preview/components/toc-style-depth.tsx",
|
||||
preview: lazy(() => import("@/preview/components/toc-style-depth")),
|
||||
},
|
||||
"toc-style-manual": {
|
||||
name: "toc-style-manual",
|
||||
filePath: "preview/components/toc-style-manual.tsx",
|
||||
preview: lazy(() => import("@/preview/components/toc-style-manual")),
|
||||
},
|
||||
"toggle-group-style-default": {
|
||||
name: "toggle-group-style-default",
|
||||
filePath: "preview/components/toggle-group-style-default.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/toggle-group-style-default"),
|
||||
),
|
||||
},
|
||||
"toggle-group-style-outline-muted": {
|
||||
name: "toggle-group-style-outline-muted",
|
||||
filePath: "preview/components/toggle-group-style-outline-muted.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/toggle-group-style-outline-muted"),
|
||||
),
|
||||
},
|
||||
"toggle-group-style-outlined": {
|
||||
name: "toggle-group-style-outlined",
|
||||
filePath: "preview/components/toggle-group-style-outlined.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/toggle-group-style-outlined"),
|
||||
),
|
||||
},
|
||||
"toggle-group-style-solid": {
|
||||
name: "toggle-group-style-solid",
|
||||
filePath: "preview/components/toggle-group-style-solid.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/toggle-group-style-solid"),
|
||||
),
|
||||
},
|
||||
"toggle-style-default": {
|
||||
name: "toggle-style-default",
|
||||
filePath: "preview/components/toggle-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/toggle-style-default")),
|
||||
},
|
||||
"toggle-style-outline-muted": {
|
||||
name: "toggle-style-outline-muted",
|
||||
filePath: "preview/components/toggle-style-outline-muted.tsx",
|
||||
preview: lazy(
|
||||
() => import("@/preview/components/toggle-style-outline-muted"),
|
||||
),
|
||||
},
|
||||
"toggle-style-outlined": {
|
||||
name: "toggle-style-outlined",
|
||||
filePath: "preview/components/toggle-style-outlined.tsx",
|
||||
preview: lazy(() => import("@/preview/components/toggle-style-outlined")),
|
||||
},
|
||||
"toggle-style-solid": {
|
||||
name: "toggle-style-solid",
|
||||
filePath: "preview/components/toggle-style-solid.tsx",
|
||||
preview: lazy(() => import("@/preview/components/toggle-style-solid")),
|
||||
},
|
||||
"tooltip-style-default": {
|
||||
name: "tooltip-style-default",
|
||||
filePath: "preview/components/tooltip-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/tooltip-style-default")),
|
||||
},
|
||||
"tooltip-style-primary": {
|
||||
name: "tooltip-style-primary",
|
||||
filePath: "preview/components/tooltip-style-primary.tsx",
|
||||
preview: lazy(() => import("@/preview/components/tooltip-style-primary")),
|
||||
},
|
||||
"tooltip-style-solid": {
|
||||
name: "tooltip-style-solid",
|
||||
filePath: "preview/components/tooltip-style-solid.tsx",
|
||||
preview: lazy(() => import("@/preview/components/tooltip-style-solid")),
|
||||
},
|
||||
"typography-p": {
|
||||
name: "typography-p",
|
||||
filePath: "preview/components/typography-p.tsx",
|
||||
preview: lazy(() => import("@/preview/components/typography-p")),
|
||||
},
|
||||
"empty-style-default": {
|
||||
name: "empty-style-default",
|
||||
filePath: "preview/components/empty-style-default.tsx",
|
||||
preview: lazy(() => import("@/preview/components/empty-style-default"))
|
||||
},
|
||||
"empty-style-custom-icon": {
|
||||
name: "empty-style-custom-icon",
|
||||
filePath: "preview/components/empty-style-custom-icon.tsx",
|
||||
preview: lazy(() => import("@/preview/components/empty-style-custom-icon"))
|
||||
},
|
||||
"empty-style-custom-everything": {
|
||||
name: "empty-style-custom-everything",
|
||||
filePath: "preview/components/empty-style-custom-everything.tsx",
|
||||
preview: lazy(() => import("@/preview/components/empty-style-custom-everything"))
|
||||
},
|
||||
"empty-style-table": {
|
||||
name: "empty-style-table",
|
||||
filePath: "preview/components/empty-style-table.tsx",
|
||||
preview: lazy(() => import("@/preview/components/empty-style-table"))
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./components";
|
||||
export * from "./navigation";
|
||||
@@ -0,0 +1,113 @@
|
||||
import { INavigationConfig } from "@/types";
|
||||
|
||||
const componentsRoute = "/docs/components";
|
||||
const chartsRoute = "/docs/charts";
|
||||
const utilsRoute = "/docs/utils";
|
||||
|
||||
export const navConfig: INavigationConfig = {
|
||||
topNavItems: [
|
||||
{ title: "Docs", href: "/docs" },
|
||||
{ title: "Components", href: `${componentsRoute}/button` },
|
||||
{ title: "Themes", href: "/themes" },
|
||||
{ title: "Blog", href: "/blogs" },
|
||||
{ title: "Showcase", href: "/showcase" },
|
||||
],
|
||||
sideNavItems: [
|
||||
{
|
||||
title: "Getting Started",
|
||||
children: [
|
||||
{ title: "Introduction", href: "/docs" },
|
||||
{
|
||||
title: "Installation",
|
||||
href: "/docs/install",
|
||||
},
|
||||
// {
|
||||
// title: "Changelog",
|
||||
// href: "https://pro.retroui.dev",
|
||||
// },
|
||||
{
|
||||
title: "Blocks",
|
||||
href: "https://pro.retroui.dev/blocks",
|
||||
tag: "Pro",
|
||||
},
|
||||
{
|
||||
title: "Templates",
|
||||
href: "https://pro.retroui.dev/templates",
|
||||
tag: "Pro",
|
||||
},
|
||||
{
|
||||
title: "Figma Kit",
|
||||
href: "https://pro.retroui.dev/figma",
|
||||
tag: "Pro",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Components",
|
||||
children: [
|
||||
{ title: "Accordion", href: `${componentsRoute}/accordion` },
|
||||
{ title: "Alert", href: `${componentsRoute}/alert` },
|
||||
{ title: "Avatar", href: `${componentsRoute}/avatar` },
|
||||
{ title: "Badge", href: `${componentsRoute}/badge` },
|
||||
{ title: "Breadcrumb", href: `${componentsRoute}/breadcrumb` },
|
||||
{ title: "Button", href: `${componentsRoute}/button` },
|
||||
{ title: "Card", href: `${componentsRoute}/card` },
|
||||
{ title: "Calendar", href: `${componentsRoute}/calendar` },
|
||||
{ title: "Carousel", href: `${componentsRoute}/carousel` },
|
||||
{ title: "Checkbox", href: `${componentsRoute}/checkbox` },
|
||||
{ title: "Command", href: `${componentsRoute}/command` },
|
||||
{ title: "Dialog", href: `${componentsRoute}/dialog` },
|
||||
{ title: "Drawer", href: `${componentsRoute}/drawer`, tag: "New" },
|
||||
{ title: "Empty", href: `${componentsRoute}/empty`, tag: "New" },
|
||||
{ title: "Input", href: `${componentsRoute}/input` },
|
||||
{ title: "Label", href: `${componentsRoute}/label` },
|
||||
{ title: "Loader", href: `${componentsRoute}/loader` },
|
||||
{ title: "Menu", href: `${componentsRoute}/menu` },
|
||||
{ title: "Context Menu", href: `${componentsRoute}/context-menu` },
|
||||
{
|
||||
title: "Popover",
|
||||
href: `${componentsRoute}/popover`,
|
||||
},
|
||||
{ title: "Progress", href: `${componentsRoute}/progress` },
|
||||
{ title: "Radio", href: `${componentsRoute}/radio` },
|
||||
{ title: "Select", href: `${componentsRoute}/select` },
|
||||
{ title: "Slider", href: `${componentsRoute}/slider` },
|
||||
{
|
||||
title: "Sonner",
|
||||
href: `${componentsRoute}/sonner`,
|
||||
},
|
||||
{ title: "Switch", href: `${componentsRoute}/switch` },
|
||||
{ title: "Tab", href: `${componentsRoute}/tab` },
|
||||
{ title: "Table", href: `${componentsRoute}/table` },
|
||||
{ title: "Table Of Contents", href: `${componentsRoute}/toc`, tag: "New" },
|
||||
{ title: "Textarea", href: `${componentsRoute}/textarea` },
|
||||
{ title: "Text", href: `${componentsRoute}/text` },
|
||||
{
|
||||
title: "Toggle",
|
||||
href: `${componentsRoute}/toggle`,
|
||||
},
|
||||
{
|
||||
title: "Toggle Group",
|
||||
href: `${componentsRoute}/toggle-group`,
|
||||
},
|
||||
{
|
||||
title: "Tooltip",
|
||||
href: `${componentsRoute}/tooltip`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Chart",
|
||||
children: [
|
||||
{ title: "Bar Chart", href: `${chartsRoute}/bar-chart`, tag: "Updated" },
|
||||
{ title: "Line Chart", href: `${chartsRoute}/line-chart` },
|
||||
{ title: "Area Chart", href: `${chartsRoute}/area-chart` },
|
||||
{ title: "Pie Chart", href: `${chartsRoute}/pie-chart` },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Utilities",
|
||||
children: [{ title: "cn", href: `${utilsRoute}/cn` }],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,354 @@
|
||||
export enum Theme {
|
||||
Default = "default",
|
||||
Purple = "purple",
|
||||
Lime = "lime",
|
||||
Red = "red",
|
||||
Lavender = "lavender",
|
||||
Orange = "orange",
|
||||
Green = "green",
|
||||
}
|
||||
|
||||
|
||||
export const themeConfig = ({hasRadius = false}: {hasRadius?: boolean}) => ({
|
||||
[Theme.Default]: {
|
||||
name: "default",
|
||||
color:
|
||||
`:root {
|
||||
--radius: ${hasRadius ? "0.5rem" : "0"};
|
||||
--background: #fff;
|
||||
--foreground: #000;
|
||||
--card: #fff;
|
||||
--card-foreground: #000;
|
||||
--primary: #ffdb33;
|
||||
--primary-hover: #ffcc00;
|
||||
--primary-foreground: #000;
|
||||
--secondary: #000;
|
||||
--secondary-foreground: #fff;
|
||||
--muted: #cccccc;
|
||||
--muted-foreground: #5a5a5a;
|
||||
--accent: #fae583;
|
||||
--accent-foreground: #000;
|
||||
--destructive: #e63946;
|
||||
--destructive-foreground: #fff;
|
||||
--border: #000;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #1a1a1a;
|
||||
--foreground: #f5f5f5;
|
||||
--card: #242424;
|
||||
--card-foreground: #f5f5f5;
|
||||
--primary: #ffdb33;
|
||||
--primary-hover: #ffcc00;
|
||||
--primary-foreground: #000;
|
||||
--secondary: #3a3a3a;
|
||||
--secondary-foreground: #f5f5f5;
|
||||
--muted: #3f3f46;
|
||||
--muted-foreground: #a0a0a0;
|
||||
--accent: #fae583;
|
||||
--accent-foreground: #000;
|
||||
--destructive: #e63946;
|
||||
--destructive-foreground: #fff;
|
||||
--border: #5c5c5c;
|
||||
}
|
||||
`,
|
||||
},
|
||||
[Theme.Purple]: {
|
||||
name: "purple",
|
||||
color:
|
||||
`:root {
|
||||
--radius: ${hasRadius ? "0.5rem" : "0"};
|
||||
--background: #f5f5f5;
|
||||
--foreground: #1a1a1a;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #f5f5f5;
|
||||
--primary: #5F4FE6;
|
||||
--primary-hover: #4938C2;
|
||||
--primary-foreground: #fff;
|
||||
--secondary: #3a3a3a;
|
||||
--secondary-foreground: #f5f5f5;
|
||||
--muted: #CFCCEA;
|
||||
--muted-foreground: #5B5686;
|
||||
--accent: #FED13B;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #EF4444;
|
||||
--destructive-foreground: #fff;
|
||||
--border: #3a3a3a;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #0f0f12;
|
||||
--foreground: #f5f5f5;
|
||||
--card: #1a1a1d;
|
||||
--card-foreground: #eaeaea;
|
||||
--primary: #7b6df5;
|
||||
--primary-hover: #5F4FE6;
|
||||
--primary-foreground: #fff;
|
||||
--secondary: #2a2a2e;
|
||||
--secondary-foreground: #eaeaea;
|
||||
--muted: #3d395a;
|
||||
--muted-foreground: #a49fce;
|
||||
--accent: #FED13B;
|
||||
--accent-foreground: #000;
|
||||
--destructive: #EF4444;
|
||||
--destructive-foreground: #fff;
|
||||
--border: #2e2e32;
|
||||
}
|
||||
`,
|
||||
},
|
||||
[Theme.Lime]: {
|
||||
name: "Lime",
|
||||
color:
|
||||
`:root {
|
||||
--radius: ${hasRadius ? "0.5rem" : "0"};
|
||||
--background: #FFFFFF;
|
||||
--foreground: #000000;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--input: #FFFFFF;
|
||||
--ring: #AAFC3D;
|
||||
--primary: #AAFC3D;
|
||||
--primary-hover: #97f819;
|
||||
--primary-foreground: #FFFFFF;
|
||||
--secondary: #5252FF;
|
||||
--secondary-foreground: #000000;
|
||||
--muted: #F3F4F6;
|
||||
--muted-foreground: #6B7280;
|
||||
--accent: #FF00FF;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #EF4444;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
--border: #3a3a3a;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #FFFFFF;
|
||||
--foreground: #000000;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--input: #FFFFFF;
|
||||
--border: #000000;
|
||||
--ring: #AAFC3D;
|
||||
--primary: #AAFC3D;
|
||||
--primary-hover: #97f819;
|
||||
--primary-foreground: #000000;
|
||||
--secondary: #000000;
|
||||
--secondary-foreground: #FFFFFF;
|
||||
--muted: #F3F3F3;
|
||||
--muted-foreground: #444444;
|
||||
--accent: #FF4FD8;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FF0033;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
}
|
||||
`,
|
||||
},
|
||||
[Theme.Red]: {
|
||||
name: "Red",
|
||||
color:
|
||||
`:root {
|
||||
--radius: ${hasRadius ? "0.5rem" : "0"};
|
||||
--background: #FCFFE7;
|
||||
--foreground: #000000;
|
||||
--muted: #EFD0D5;
|
||||
--muted-foreground: #A42439;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #000000;
|
||||
--border: #000000;
|
||||
--input: #FFFFFF;
|
||||
--primary: #EA435F;
|
||||
--primary-hover: #D00000;
|
||||
--primary-foreground: #FFFFFF;
|
||||
--secondary: #FFDA5C;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #CEEBFC;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #D00000;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
--ring: #000000;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #0f0f0f;
|
||||
--foreground: #f5f5f5;
|
||||
--muted: #3a1f24;
|
||||
--muted-foreground: #f2a7b2;
|
||||
--card: #1a1a1a;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #1a1a1a;
|
||||
--popover-foreground: #ffffff;
|
||||
--border: #2a2a2a;
|
||||
--input: #2a2a2a;
|
||||
--primary: #EA435F;
|
||||
--primary-hover: #D00000;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #FFDA5C;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #2a3b45;
|
||||
--accent-foreground: #CEEBFC;
|
||||
--destructive: #D00000;
|
||||
--destructive-foreground: #ffffff;
|
||||
--ring: #EA435F;
|
||||
}
|
||||
`,
|
||||
},
|
||||
[Theme.Lavender]: {
|
||||
name: "Lavender",
|
||||
color:
|
||||
`:root {
|
||||
--radius: ${hasRadius ? "0.5rem" : "0"};
|
||||
--background: #F9F5F2;
|
||||
--foreground: #000000;
|
||||
--muted: #EDE3FF;
|
||||
--muted-foreground: #5B3B98;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #000000;
|
||||
--border: #000000;
|
||||
--input: #FFFFFF;
|
||||
--primary: #C4A1FF;
|
||||
--primary-hover: #9678FF;
|
||||
--primary-foreground: #000000;
|
||||
--secondary: #E7F193;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #FE91E9;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FE3B3B;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
--ring: #000000;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #121014;
|
||||
--foreground: #f5f5f5;
|
||||
--muted: #2d2440;
|
||||
--muted-foreground: #c7b6f3;
|
||||
--card: #1a1820;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #1a1820;
|
||||
--popover-foreground: #ffffff;
|
||||
--border: #2a2a2e;
|
||||
--input: #2a2a2e;
|
||||
--primary: #C4A1FF;
|
||||
--primary-hover: #9678FF;
|
||||
--primary-foreground: #000000;
|
||||
--secondary: #E7F193;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #FE91E9;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FE3B3B;
|
||||
--destructive-foreground: #ffffff;
|
||||
--ring: #C4A1FF;
|
||||
}
|
||||
|
||||
`,
|
||||
},
|
||||
[Theme.Orange]: {
|
||||
name: "Orange",
|
||||
color:
|
||||
`:root {
|
||||
--radius: ${hasRadius ? "0.5rem" : "0"};
|
||||
--background: #F9F5F2;
|
||||
--foreground: #000000;
|
||||
--muted: #EFD0D5;
|
||||
--muted-foreground: #A42439;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #000000;
|
||||
--border: #000000;
|
||||
--input: #FFFFFF;
|
||||
--primary: #EA435F;
|
||||
--primary-hover: #D00000;
|
||||
--primary-foreground: #FFFFFF;
|
||||
--secondary: #FFDA5C;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #CEEBFC;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #D00000;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
--ring: #000000;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #12100e;
|
||||
--foreground: #f5f5f5;
|
||||
--muted: #3a2617;
|
||||
--muted-foreground: #ffb679;
|
||||
--card: #1a1a1a;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #1a1a1a;
|
||||
--popover-foreground: #ffffff;
|
||||
--border: #2a2a2a;
|
||||
--input: #2a2a2a;
|
||||
--primary: #F07200;
|
||||
--primary-hover: #ff8011;
|
||||
--primary-foreground: #FDEAD9;
|
||||
--secondary: #EF8C27;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #FF6B6B;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #FE3B3B;
|
||||
--destructive-foreground: #ffffff;
|
||||
--ring: #F07200;
|
||||
}
|
||||
`,
|
||||
},
|
||||
[Theme.Green]: {
|
||||
name: "Green",
|
||||
color:
|
||||
`:root {
|
||||
--radius: ${hasRadius ? "0.5rem" : "0"};
|
||||
--background: #F9F5F2;
|
||||
--foreground: #000000;
|
||||
--muted: #EFD0D5;
|
||||
--muted-foreground: #A42439;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #000000;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #000000;
|
||||
--border: #000000;
|
||||
--input: #FFFFFF;
|
||||
--primary: #EA435F;
|
||||
--primary-hover: #D00000;
|
||||
--primary-foreground: #FFFFFF;
|
||||
--secondary: #FFDA5C;
|
||||
--secondary-foreground: #000000;
|
||||
--accent: #CEEBFC;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #D00000;
|
||||
--destructive-foreground: #FFFFFF;
|
||||
--ring: #000000;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #0f1210;
|
||||
--foreground: #f5f5f5;
|
||||
--muted: #1f3a2d;
|
||||
--muted-foreground: #9cd9b8;
|
||||
--card: #181c19;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #181c19;
|
||||
--popover-foreground: #ffffff;
|
||||
--border: #2a2e2b;
|
||||
--input: #2a2e2b;
|
||||
--primary: #599D77;
|
||||
--primary-hover: #39654c;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #3a6f52;
|
||||
--secondary-foreground: #ffffff;
|
||||
--accent: #FFE75A;
|
||||
--accent-foreground: #000000;
|
||||
--destructive: #F05D5D;
|
||||
--destructive-foreground: #ffffff;
|
||||
--ring: #599D77;
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
export const themes = Object.keys(themeConfig({hasRadius: false}));
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
---
|
||||
title: Integrate RetroUI with Payload CMS
|
||||
coverImage: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/blogs/retroui-with-payload-cms/retroui-with-payload-cms-cover.png
|
||||
description: Using UI libraries can significantly speed up your development. In this article, we will explore the top 5 for your next React project.
|
||||
tags:
|
||||
- react
|
||||
- ui library
|
||||
- top 5
|
||||
author:
|
||||
name: Arif Hossain
|
||||
avatar: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/arif.jpg
|
||||
x: ariflogs
|
||||
status: draft
|
||||
publishedAt: 14 Nov, 2025
|
||||
---
|
||||
|
||||
|
||||
|
||||
A practical, end‑to‑end guide to wire up **Payload CMS** as your content backend and **RetroUI** as your front‑end component kit—then ship a clean, neo‑brutalist **Blog List** in Next.js.
|
||||
|
||||
> Works great if your stack is **Next.js + TailwindCSS**. Payload is Next‑native and RetroUI is Tailwind‑first, so they snap together nicely.
|
||||
|
||||
---
|
||||
|
||||
## What you’ll build
|
||||
|
||||
* A Payload CMS app with a **Posts** collection
|
||||
* A Next.js front‑end that fetches posts from Payload’s REST API
|
||||
* A `/blog` page that renders a responsive list using RetroUI components (Cards, Badges, Buttons)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* **Node.js 20.9+** (Payload requires Node 20.9 or newer)
|
||||
* **Next.js 15+** for the front‑end (Payload is Next‑native)
|
||||
* Any compatible DB: SQLite (dev), Postgres, or MongoDB
|
||||
|
||||
> Tip: For local development, SQLite is the quickest path. Switch to Postgres/Mongo when deploying.
|
||||
|
||||
---
|
||||
|
||||
## Part 1 — Set up Payload CMS
|
||||
|
||||
You can add Payload to an existing Next.js app or scaffold a new one. We’ll scaffold a dedicated Payload project so its API stays clean and portable.
|
||||
|
||||
### 1) Create a new Payload app
|
||||
|
||||
```bash
|
||||
npx create-payload-app
|
||||
# Follow the prompts: choose a template (blank or blog), set DB (SQLite for dev), etc.
|
||||
cd <your-payload-app>
|
||||
pnpm dev # or npm run dev / yarn dev
|
||||
```
|
||||
|
||||
This starts Payload and its Admin UI (usually at `http://localhost:3000/admin`).
|
||||
|
||||
### 2) Define a Posts collection
|
||||
|
||||
Create `src/collections/Posts.ts`:
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'publishedAt', 'status'],
|
||||
},
|
||||
access: {
|
||||
read: () => true, // public read access for blog
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
name: 'excerpt',
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
name: 'coverImage',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Draft', value: 'draft' },
|
||||
{ label: 'Published', value: 'published' },
|
||||
],
|
||||
defaultValue: 'draft',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'date',
|
||||
admin: { position: 'sidebar' },
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Posts;
|
||||
```
|
||||
|
||||
Add it to your main config `src/payload.config.ts`:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config';
|
||||
import Posts from './collections/Posts';
|
||||
|
||||
export default buildConfig({
|
||||
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
|
||||
admin: { user: 'users' },
|
||||
collections: [
|
||||
Posts,
|
||||
// Media and Users collections if you need them
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
> If you don’t have a **Media** or **Users** collection yet, run `create-payload-app` with a blog template or add simple collections for uploads and auth later.
|
||||
|
||||
### 3) Seed a few posts
|
||||
|
||||
Open the Admin UI → **Posts** → **Create New** and add a couple of published posts with past `publishedAt` dates.
|
||||
|
||||
### 4) Confirm the REST API works
|
||||
|
||||
Payload exposes a REST API by default at `/api/<collection>`.
|
||||
|
||||
Visit:
|
||||
|
||||
```
|
||||
http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published
|
||||
```
|
||||
|
||||
You should see JSON with your posts.
|
||||
|
||||
> When you deploy, set `PAYLOAD_PUBLIC_SERVER_URL` and your DB env vars. If you’re hosting Payload separately from Next.js, enable **CORS** for your front‑end origin.
|
||||
|
||||
---
|
||||
|
||||
## Part 2 — Set up Next.js + RetroUI front‑end
|
||||
|
||||
We’ll build a Next.js app that consumes the Payload REST API and renders a Blog List using **RetroUI** components.
|
||||
|
||||
### 1) Create the Next.js app
|
||||
|
||||
```bash
|
||||
npx create-next-app@latest retroui-payload-blog
|
||||
cd retroui-payload-blog
|
||||
```
|
||||
|
||||
### 2) Install TailwindCSS (if you didn’t choose the Tailwind template)
|
||||
|
||||
```bash
|
||||
pnpm i -D tailwindcss postcss autoprefixer
|
||||
npx tailwindcss init -p
|
||||
```
|
||||
|
||||
Add `./components/**/*.{ts,tsx}` and `./app/**/*.{ts,tsx}` to `tailwind.config.ts` content. Also include RetroUI’s paths if required by the installer.
|
||||
|
||||
### 3) Install RetroUI
|
||||
|
||||
Use the official installer (CLI) or manual install. Example (pnpm):
|
||||
|
||||
```bash
|
||||
pnpm add retroui
|
||||
```
|
||||
|
||||
Then import components where needed. (If RetroUI ships a CLI to copy components, run it here and follow prompts.)
|
||||
|
||||
### 4) Connect to Payload (env + fetch helper)
|
||||
|
||||
Create `.env.local`:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
Add a tiny fetch utility `lib/payload.ts`:
|
||||
|
||||
```ts
|
||||
export type Post = {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
excerpt?: string;
|
||||
coverImage?: { url: string; filename: string } | string | null;
|
||||
status: 'draft' | 'published';
|
||||
publishedAt?: string | null;
|
||||
};
|
||||
|
||||
export async function getPublishedPosts(limit = 12) {
|
||||
const base = process.env.NEXT_PUBLIC_PAYLOAD_BASE_URL;
|
||||
const url = new URL('/api/posts', base);
|
||||
url.searchParams.set('limit', String(limit));
|
||||
url.searchParams.set('sort', '-publishedAt');
|
||||
url.searchParams.set('where[status][equals]', 'published');
|
||||
|
||||
const res = await fetch(url.toString(), { next: { revalidate: 60 } });
|
||||
if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.docs as Post[];
|
||||
}
|
||||
```
|
||||
|
||||
### 5) Build the Blog List page with RetroUI
|
||||
|
||||
Create `app/blog/page.tsx` (App Router):
|
||||
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { getPublishedPosts, type Post } from '@/lib/payload';
|
||||
|
||||
// Example RetroUI components — adjust paths/names per your install
|
||||
import { Card } from 'retroui/card';
|
||||
import { Button } from 'retroui/button';
|
||||
import { Badge } from 'retroui/badge';
|
||||
|
||||
export const revalidate = 60; // ISR cadence
|
||||
|
||||
export default async function BlogListPage() {
|
||||
const posts = await getPublishedPosts(12);
|
||||
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-10">
|
||||
<h1 className="text-4xl font-extrabold mb-6">Blog</h1>
|
||||
<p className="text-muted-foreground mb-10">
|
||||
Latest posts from the Payload CMS backend, styled with RetroUI.
|
||||
</p>
|
||||
|
||||
<section className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post) => (
|
||||
<Card key={post.id} className="p-0 overflow-hidden">
|
||||
{typeof post.coverImage === 'object' && post.coverImage?.url ? (
|
||||
<div className="relative h-44 w-full">
|
||||
<Image
|
||||
src={post.coverImage.url}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Article</Badge>
|
||||
{post.publishedAt ? (
|
||||
<span className="text-xs opacity-70">
|
||||
{new Date(post.publishedAt).toLocaleDateString()}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold leading-tight">
|
||||
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
|
||||
</h2>
|
||||
|
||||
{post.excerpt ? (
|
||||
<p className="text-sm text-muted-foreground line-clamp-3">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="pt-2">
|
||||
<Button asChild>
|
||||
<Link href={`/blog/${post.slug}`}>Read more</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> If your RetroUI package exposes different import names/paths, adjust `import { Card } from 'retroui/card'` lines to match. You can also swap in any other RetroUI components (Tabs, Inputs, etc.).
|
||||
|
||||
### 6) Post detail page (optional)
|
||||
|
||||
Create `app/blog/[slug]/page.tsx`:
|
||||
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
async function getPost(slug: string) {
|
||||
const base = process.env.NEXT_PUBLIC_PAYLOAD_BASE_URL!;
|
||||
const url = new URL('/api/posts', base);
|
||||
url.searchParams.set('limit', '1');
|
||||
url.searchParams.set('where[slug][equals]', slug);
|
||||
const res = await fetch(url.toString(), { next: { revalidate: 60 } });
|
||||
if (!res.ok) throw new Error('Failed to load post');
|
||||
const data = await res.json();
|
||||
return data.docs[0];
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: { params: { slug: string } }) {
|
||||
const post = await getPost(params.slug);
|
||||
if (!post) return <div className="p-10">Not found</div>;
|
||||
|
||||
return (
|
||||
<article className="container mx-auto px-4 py-10 max-w-3xl">
|
||||
<Link href="/blog" className="underline">← Back to blog</Link>
|
||||
<h1 className="text-4xl font-extrabold mt-3">{post.title}</h1>
|
||||
{post.coverImage?.url ? (
|
||||
<div className="relative h-80 w-full my-6">
|
||||
<Image src={post.coverImage.url} alt={post.title} fill className="object-cover" />
|
||||
</div>
|
||||
) : null}
|
||||
{post.content?.root ? (
|
||||
// If using Payload Lexical richText renderer, render here
|
||||
<div className="prose prose-neutral dark:prose-invert">
|
||||
{/* Render your rich text */}
|
||||
</div>
|
||||
) : (
|
||||
post.excerpt ? <p className="mt-4 text-lg">{post.excerpt}</p> : null
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> For rich text, use your preferred Payload Renderer (e.g., @payloadcms/richtext-lexical) and render accordingly.
|
||||
|
||||
---
|
||||
|
||||
## Part 3 — Cross‑origin & deployment notes
|
||||
|
||||
If your Next.js app runs at a different origin than Payload:
|
||||
|
||||
* Enable **CORS** in Payload: set `cors: ["https://your-next-app.com", "http://localhost:3000"]` in `payload.config.ts`.
|
||||
* Set `serverURL` (and `PAYLOAD_PUBLIC_SERVER_URL`) correctly so image URLs resolve.
|
||||
* Protect drafts: we allowed public `read` above. For private content, replace with an `access.read` function that checks auth/roles.
|
||||
|
||||
Deployment options:
|
||||
|
||||
* **Self‑host** (Node app on a VPS) or deploy to **Vercel**/**Cloudflare** for modern DX.
|
||||
* Use **SQLite** for quick demos, **Postgres/Mongo** for prod.
|
||||
|
||||
---
|
||||
|
||||
## Part 4 — Enhancements
|
||||
|
||||
* **Search & filters**: Use Payload’s query parameters (e.g., `where[title][like]`) and wire a RetroUI **Input** + **Tabs** to filter.
|
||||
* **Pagination**: the REST API returns `totalDocs`, `limit`, `page`—build “Load more” with a RetroUI Button.
|
||||
* **Images**: move `coverImage` to a dedicated **Media** collection and use Payload’s upload adapter.
|
||||
* **Preview mode**: set up a draft preview route in Next.js that fetches with draft‑auth headers.
|
||||
* **GraphQL**: Payload also exposes GraphQL—use it if you prefer typed queries.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* **CORS errors**: Check `cors` config and ensure `serverURL` is set.
|
||||
* **Images not showing**: Confirm the `url` returned for uploads and that Next.js `next.config.js` allows the Payload domain under `images.domains`.
|
||||
* **Nothing renders**: Inspect the fetch URL in `lib/payload.ts`. Ensure records exist and `status` is `published`.
|
||||
|
||||
---
|
||||
|
||||
## Recap
|
||||
|
||||
You now have:
|
||||
|
||||
* A Payload CMS app exposing posts over REST
|
||||
* A Next.js site styled with RetroUI
|
||||
* A `/blog` that lists and links to individual posts
|
||||
|
||||
From here, add authors, categories, and tags; style with more RetroUI components; and ship it.
|
||||
@@ -0,0 +1,380 @@
|
||||
---
|
||||
title: Top 10 Next.js UI Libraries in 2025
|
||||
coverImage: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/blogs/top-10-nextjs-2025/cover.png
|
||||
description: Using UI libraries can significantly speed up your development. In this article, we will explore the top 10 for your Next.js project.
|
||||
tags:
|
||||
- nextjs
|
||||
- ui library
|
||||
- top 10
|
||||
author:
|
||||
name: Imran Hossain
|
||||
avatar: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/people/imran.jpg
|
||||
linkedin: faraazhossainimran
|
||||
|
||||
status: published
|
||||
publishedAt: 31 May, 2025
|
||||
---
|
||||
|
||||
Next.js has become the go-to React framework for building modern, full-stack web applications. But great functionality alone isn't enough. Your app needs a beautiful, fast, and accessible user interface. That’s where UI libraries come in.
|
||||
|
||||
<br />
|
||||
|
||||
Using UI libraries can dramatically streamline your development workflow. The good news is that there’s no shortage of high-quality UI libraries that work seamlessly with Next.js. Below are the top 10 UI libraries in 2025 that can help you build modern, user-friendly interfaces more efficiently in your next Next.js project.
|
||||
|
||||
<br />
|
||||
|
||||
## [1. RetroUI](https://retroui.dev)
|
||||
|
||||

|
||||
|
||||
If you are bored with generic looking UI libraries, you should check out [RetroUI](https://retroui.dev). It brings a combination of retro and modern design elements to create a unique and memorable user experience.
|
||||
The library is built with React and Tailwind CSS, making it easy to integrate into your existing projects.
|
||||
|
||||
<br />
|
||||
|
||||
RetroUI comes with 100+ utility components, including `buttons`, `cards`, `modals`, and more that you can just copy-paste into your projects.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why use RetroUI in 2025?
|
||||
|
||||
- **Unique Design:** RetroUI's unique design elements make it stand out from other UI libraries.
|
||||
- **Easy to Use:** RetroUI's components are easy to use and can be customized with Tailwind CSS.
|
||||
- **Bold Design System:** For websites that aims to look bold, RetroUI can be a perfect fit.
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Bold Design System:** RetroUI's biggest benefit can also be it's biggest drawback, If you are looking for a more subtle design, RetroUI might not be the best fit.
|
||||
- **Smaller Community Size:** As a new UI library, RetroUI doesn't have a large community yet compared to other mainstream libraries.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Perfect for someone looking to combine old school and modern design!
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [2. Shadcn/ui](https://ui.shadcn.com)
|
||||
|
||||

|
||||
|
||||
|
||||
Shadcn/ui has quickly become one of the most talked-about UI libraries in the React and Next.js ecosystem. It blends the power of **Tailwind CSS**, **Radix UI**, and modern design principles to provide a customizable and production-ready set of components.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why shadcn/ui in 2025?
|
||||
|
||||
- **Modern Aesthetic**: Components follow sleek, minimal, and accessible design standards, fitting perfectly with modern UI trends.
|
||||
- **Composable & Customizable**: Built on Tailwind CSS and Radix UI primitives, giving developers full control over styles and behavior.
|
||||
- **TypeScript Friendly**: All components are fully typed, providing a great developer experience with auto completion and safety.
|
||||
- **Great for Next.js**: Designed with app directory support in mind, making it a natural fit for modern Next.js projects.
|
||||
|
||||
```tsx
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
<Button variant="outline">Click me</Button>
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Manual Setup**: Unlike Chakra or Material UI, you don’t install it as a complete package. You copy and paste components into your project, which can be tedious for some developers.
|
||||
- **Learning Curve**: Requires some familiarity with Tailwind CSS and Radix UI to fully unlock its potential.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Ideal for developers who want full control over styling and component behavior while maintaining a modern design system. Especially great for teams building custom design systems or wanting highly optimized Next.js apps without the bloat.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
|
||||
## [3. Chakra UI](https://chakra-ui.com)
|
||||

|
||||
|
||||
**Chakra UI** is a popular React component library known for its simplicity, accessibility, and developer-friendly API. It provides a comprehensive set of pre-styled components that speed up the development of clean, responsive UIs.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why Chakra UI in 2025?
|
||||
|
||||
- **Large Selection of Components**: Comes with a wide range of ready-to-use components like modals, forms, alerts, icons, cards, and more.
|
||||
- **Prop-Based Styling**: Uses a utility prop system that makes styling components intuitive and efficient without writing separate CSS.
|
||||
- **Theming Support**: Fully customizable theme system with support for dark mode and a live theme playground.
|
||||
- **Accessibility Built-in**: All components follow WAI-ARIA standards and are keyboard-navigable by default.
|
||||
- **Helpful Utilities**: Includes extra helper components and hooks such as useDisclosure, useToast, and more for managing UI state easily.
|
||||
|
||||
```tsx
|
||||
import { FormatNumber } from "@chakra-ui/react";
|
||||
|
||||
<FormatNumber value={1234.45} style="currency" currency="USD" />;
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Bundle Size**: The total package size exceeds 2MB (unpacked), and when combined with dependencies, it can add notable weight to your application.
|
||||
- **Less Tailwind-Friendly**: Chakra’s styling system differs from utility-first frameworks like Tailwind, which may be limiting for some teams.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Perfect for developers who want to move fast without worrying about styling from scratch. Great for dashboards, admin panels, and apps where accessibility and design consistency are important out-of-the-box.
|
||||
|
||||
|
||||
## [4. MUI (Material UI)](https://mui.com)
|
||||
|
||||

|
||||
|
||||
**MUI (Material UI)**is one of the most established and feature-rich UI libraries for React. Based on Google's Material Design, it offers a large set of components, robust customization options, and strong community support, making it a go-to choice for many production apps.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why MUI in 2025?
|
||||
|
||||
- **Comprehensive Component Library**: Offers 100+ high-quality components including tables, date pickers, data grids, and more.
|
||||
- **Material Design Compliance**: Implements Google's Material Design out-of-the-box for a professional, polished look.
|
||||
- **Theming & Customization**: Advanced theming system allows deep customization to fit any design system.
|
||||
- **Powerful Layout Tools**: Includes responsive Grid and Box components that simplify layout creation.
|
||||
- **Strong Ecosystem**: Comes with official add-ons like MUI X for data-rich components and design kits for Figma.
|
||||
|
||||
```tsx
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
<Button variant="contained" color="primary">Click Me</Button>
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Large Bundle Size**: MUI’s component richness comes at the cost of performance — it can significantly increase bundle size.
|
||||
- **Style Customization Can Be Verbose**: Overriding styles using the sx prop or styled() can be less intuitive for Tailwind or Chakra users.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Best for enterprise-level applications or teams that want a robust, scalable, and production-ready UI solution with built-in accessibility and design consistency. Ideal for admin dashboards, CRMs, and SaaS platforms.
|
||||
|
||||
|
||||
## [5. Radix UI](https://www.radix-ui.com)
|
||||
|
||||

|
||||
**Radix UI** is a low-level UI component library that provides unstyled, accessible primitives for building custom and highly flexible interfaces. Rather than giving you styled components, it offers logic and behavior, allowing complete control over design.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why Radix UI in 2025?
|
||||
|
||||
- **Unstyled by Design**: Gives you complete styling freedom using any CSS approach — Tailwind, CSS Modules, Styled Components, etc.
|
||||
- **Accessibility First**: Every component adheres to WAI-ARIA standards and comes with built-in accessibility logic.
|
||||
- **Composability**: Components are built as primitives, allowing you to compose and extend them to suit your design needs.
|
||||
- **Framework-Agnostic**: While optimized for React, Radix UI is modular enough to be integrated into various setups, including modern Next.js apps.
|
||||
- **Consistent API**: Offers a unified API across all components, making it predictable and easy to use once you're familiar with one.
|
||||
|
||||
|
||||
```tsx
|
||||
import * as Switch from '@radix-ui/react-switch';
|
||||
|
||||
<Switch.Root className="...">
|
||||
<Switch.Thumb className="..." />
|
||||
</Switch.Root>
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **No Styling Included**: Beginners may find it harder to use, as it requires pairing with a styling solution (e.g., Tailwind CSS).
|
||||
- **Not a Full UI Kit**: Radix UI doesn’t provide all the components you'd find in traditional UI libraries (e.g., tables, charts, etc.).
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Perfect for developers and teams who want full control over their UI design and behavior. Ideal for custom design systems and projects where flexibility and accessibility are top priorities.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [6. Mantine](https://mantine.dev)
|
||||

|
||||
|
||||
|
||||
**Mantine** is a modern React component library that offers a rich set of customizable and accessible UI components, combined with powerful hooks and utilities. It focuses on developer experience and performance, making it a great choice for building scalable Next.js applications.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why Mantine in 2025?
|
||||
|
||||
- **Extensive Component Library**: Includes over 100 components such as modals, notifications, inputs, and advanced data display elements.
|
||||
- **Flexible Styling**: Supports multiple styling approaches including CSS-in-JS, Tailwind CSS integration, and theme overrides.
|
||||
- **Built-in Dark Mode**: Offers easy dark/light theme switching with seamless theming capabilities.
|
||||
- **Rich Utility Hooks**: Provides handy hooks for common tasks like form handling, modals, and media queries.
|
||||
- **TypeScript Support**: Fully typed components for excellent developer experience and safer code.
|
||||
|
||||
```tsx
|
||||
import { Button } from '@mantine/core';
|
||||
|
||||
<Button variant="outline">Click me</Button>
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Smaller Community**: Compared to Chakra UI or MUI, Mantine’s community is growing but still smaller, meaning fewer third-party resources.
|
||||
- **Less Established**: Being relatively newer, some components or integrations might be less battle-tested.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Ideal for developers looking for a comprehensive and flexible UI toolkit with great developer ergonomics. Perfect for building both small projects and complex apps that require a customizable design system.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [7. Hero UI](https://heroui.com)
|
||||
|
||||

|
||||
|
||||
|
||||
**Hero UI** is a lightweight, Tailwind CSS-based component library that focuses on delivering clean, accessible, and modern UI components out-of-the-box. It emphasizes simplicity and responsiveness, making it an excellent choice for developers who prefer a utility-first CSS approach like Tailwind.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why Hero UI in 2025?
|
||||
|
||||
- **Tailwind-First Design**: Built entirely with Tailwind CSS, ensuring consistency and ease of customization for Tailwind users.
|
||||
- **Accessible Components**: Prioritizes accessibility with keyboard navigation and screen-reader-friendly components.
|
||||
- **Simple Integration**: Easy to plug into any Tailwind + React or Next.js project without heavy configuration.
|
||||
- **Well-Designed Defaults**: Offers visually appealing default styles that align with modern UI trends without overwhelming customization.
|
||||
- **Fast Prototyping**: Perfect for building UIs quickly with pre-designed, responsive components.
|
||||
|
||||
```tsx
|
||||
import { Button } from '@hero-ui/react';
|
||||
|
||||
<Button variant="primary">Click me</Button>
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Limited Component Set**: Compared to larger libraries like MUI or Mantine, Hero UI has a smaller set of components.
|
||||
- **Tailwind Dependency**: Not suitable if you don’t want to use Tailwind CSS, as it relies heavily on Tailwind utility classes.
|
||||
- **Minimal Ecosystem**: Fewer plugins or extended tools compared to more mature libraries.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Best for developers working with Tailwind CSS who want clean, ready-to-use components with minimal setup. Ideal for small to mid-sized projects, landing pages, and startups looking to ship quickly with a modern design.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [8. Ant Design](https://ant.design)
|
||||
|
||||

|
||||
|
||||
**Ant Design** is a comprehensive and enterprise-grade UI library built for React. Developed by Alibaba, it is widely used in production-level applications, especially in enterprise and admin dashboards, due to its vast component set and professional design system.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why Ant Design in 2025?
|
||||
|
||||
- **Rich Component Library**: Offers a large number of well-crafted components, including complex UI elements like tables, trees, timelines, and charts.
|
||||
- **Design System**: Follows a well-defined design philosophy that promotes consistency, usability, and responsiveness.
|
||||
- **Internationalization (i18n)**: Built-in support for multiple languages and locale settings out of the box.
|
||||
- **Enterprise Features**: Comes with features like form validation, data tables, permission controls, and icon packs that are ideal for large-scale apps.
|
||||
- **Custom Theming**: Allows advanced theming using Less variables or CSS-in-JS with full control over visual style.
|
||||
|
||||
```tsx
|
||||
import { Button } from 'antd';
|
||||
|
||||
<Button type="primary">Click Me</Button>
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Heavy Bundle Size**: One of the heaviest UI libraries in terms of bundle size, which might affect performance in smaller apps.
|
||||
- **Overhead for Simple Projects**: The complexity and richness may be overkill for lightweight or minimal UIs.
|
||||
- **Less Tailwind-Friendly**: Not designed with utility-first frameworks like Tailwind CSS in mind.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Ideal for enterprise dashboards, internal tools, admin panels, and data-intensive applications. Best suited for teams who need robust components and built-in business-level features without building from scratch.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [9. DaisyUI](https://daisyui.com)
|
||||
|
||||

|
||||
**DaisyUI** is a plugin for Tailwind CSS that adds pre-styled, customizable UI components. It allows developers to build beautiful interfaces quickly while keeping the flexibility and utility-first approach of Tailwind intact. Lightweight and beginner-friendly, DaisyUI is a popular choice for rapid development.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why DaisyUI in 2025?
|
||||
|
||||
- **Tailwind Native**: Built directly on top of Tailwind CSS, making it seamless to integrate and customize using Tailwind classes.
|
||||
- **Theme Support**: Comes with multiple pre-built themes and easy dark mode toggling, customizable via Tailwind config.
|
||||
- **Component-Rich**: Provides a wide range of components like buttons, cards, modals, navbars, dropdowns, and more.
|
||||
- **Minimal Setup**: Easy to install and integrate into any Tailwind + Next.js project with zero JavaScript bloat.
|
||||
- **Open Source & Community Driven**: Actively maintained with a growing contributor base and real-world usage.
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Basic Functionality**: Most components are presentational and don’t come with built-in interactivity like form validation or dropdown logic.
|
||||
- **Not Framework-Specific**: Unlike MUI or Chakra UI, DaisyUI is just styled HTML classes — you'll still need to handle component behavior yourself.
|
||||
- **Limited Advanced Components**: Lacks complex or enterprise-level components (e.g., data grids, sortable tables, etc.).
|
||||
|
||||
#### Use Case
|
||||
|
||||
Perfect for beginners or developers who want to prototype quickly using Tailwind CSS with a pre-styled component set. Ideal for marketing pages, landing pages, blogs, and simple dashboards in Next.js apps.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [10. Ark UI](https://ark-ui.com)
|
||||
|
||||

|
||||
|
||||
**Ark UI** is a headless, accessible component library built by the creators of Chakra UI. It offers composable, unstyled components that give developers complete control over the UI while handling accessibility and interaction logic internally. It's perfect for custom design systems.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why Ark UI in 2025?
|
||||
|
||||
- **Headless Components**: Ark UI provides the functionality and accessibility of components without imposing styles, allowing full design freedom.
|
||||
- **Accessibility by Default**: All components are WAI-ARIA compliant, keyboard navigable, and screen-reader friendly out of the box.
|
||||
- **Framework-Agnostic**: Works not only with React/Next.js but also with Vue, Solid, and other frontend frameworks.
|
||||
- **Composable Architecture**: Encourages flexibility and reusability by breaking components into primitives that can be composed freely.
|
||||
- **Ideal for Design Systems**: Perfect for teams building custom UI libraries or fully branded interfaces.
|
||||
|
||||
### Drawbacks
|
||||
|
||||
- **No Styling Provided**: Unlike Chakra UI or Mantine, you must style everything yourself using Tailwind, CSS-in-JS, or your preferred method.
|
||||
- **Steeper Learning Curve**: Beginners may find it harder to use because it requires more setup and an understanding of headless component architecture.
|
||||
- **Fewer Out-of-the-Box Components**: Still expanding its library — may not yet have all the components found in more mature kits.
|
||||
|
||||
### Use Case
|
||||
|
||||
Ideal for teams or developers who want total styling control and need accessible, composable building blocks for custom UI systems in React or Next.js projects. Great for products with a unique brand identity or when building a design system from scratch.
|
||||
@@ -0,0 +1,204 @@
|
||||
---
|
||||
title: Top 5 React UI Libraries in 2025
|
||||
coverImage: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/blogs/top-5-react-ui-library/react-cover.png
|
||||
description: Using UI libraries can significantly speed up your development. In this article, we will explore the top 5 for your next React project.
|
||||
tags:
|
||||
- react
|
||||
- ui library
|
||||
- top 5
|
||||
author:
|
||||
name: Arif Hossain
|
||||
avatar: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/arif.jpg
|
||||
x: ariflogs
|
||||
status: published
|
||||
publishedAt: 12 Feb, 2024
|
||||
---
|
||||
|
||||
React remains one of the most popular JavaScript frameworks for building web applications, and UI libraries play a important role in speeding up development. Using UI libraries can significantly speed up your development process and help you create consistent, high-quality user interfaces.
|
||||
|
||||
<br />
|
||||
Fortunately, there are many React great UI libraries available that can help you
|
||||
achieve this. Here are the top 5 React UI libraries in 2025 that you should
|
||||
consider using in your next project.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [1. RetroUI](https://retroui.dev)
|
||||
|
||||

|
||||
|
||||
If you are bored with generic looking UI libraries, you should check out [RetroUI](https://retroui.dev). It brings a combination of retro and modern design elements to create a unique and memorable user experience.
|
||||
The library is built with React and Tailwind CSS, making it easy to integrate into your existing projects.
|
||||
|
||||
<br />
|
||||
|
||||
RetroUI comes with 100+ utility components, including `buttons`, `cards`, `modals`, and more that you can just copy-paste into your projects.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why use RetroUI in 2025?
|
||||
|
||||
- **Unique Design:** RetroUI's unique design elements make it stand out from other UI libraries.
|
||||
- **Easy to Use:** RetroUI's components are easy to use and can be customized with Tailwind CSS.
|
||||
- **Bold Design System:** For websites that aims to look bold, RetroUI can be a perfect fit.
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Bold Design System:** RetroUI's biggest benefit can also be it's biggest drawback, If you are looking for a more subtle design, RetroUI might not be the best fit.
|
||||
- **Smaller Community Size:** As a new UI library, RetroUI doesn't have a large community yet compared to other mainstream libraries.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Perfect for someone looking to combine old school and modern design!
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [2. Chakra UI](https://chakra-ui.com)
|
||||
|
||||

|
||||
Chakra UI has gained popularity for its simplicity and flexibility. It offers a modern, accessible, and easy-to-use components that can fit any React applications.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why Chakra UI in 2025?
|
||||
|
||||
- Large Selection of Components: Chakra UI offers a wide range of components, including `dialogs`, `forms`, `icons`, `stats`, and more.
|
||||
- Ease of Styling: Chakra UI Uses a prop-based styling approach that allows for easily customize the components.
|
||||
- Theming Support: Chakra UI allows deep customization along with a playground where you can try and test your themes.
|
||||
- Utilities: Chakra UI also provides a bunch of helper components that can handle some common functionalities.
|
||||
|
||||
```js
|
||||
import { FormatNumber } from "@chakra-ui/react";
|
||||
|
||||
<FormatNumber value={1234.45} style="currency" currency="USD" />;
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- Heavy: The unpacked size of Chakra UI is over 2MB, and combining that with it's core dependencies, can make Chakra UI a heavy library to use.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case
|
||||
|
||||
Developers who don't wanna spend a lot of time on building/styling UIs, as ChakraUI has almost all the common components you can think of.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [3. Shadcn/UI](https://ui.shadcn.com/)
|
||||
|
||||

|
||||
|
||||
Shadcn is not necessarily a UI library, but a collection of UI components that you can use to build your own UI library. It's a great starting point for building your own UI library, as it provides a lot of components that you can use and customize.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why Use ShadCN in 2025?
|
||||
|
||||
- Fully Customizable: Unlike traditional UI kits, it doesn't have a specific design language. You can fully customize it to match your brand's design.
|
||||
- Minimalistic Design: If you like clean and minimalistic design, you'll love ShadCN.
|
||||
- Backed by Vercel: Shadcn is now managed by Vercel and integrates well with other Vercel products like NextJS, v0 and AI SDK.
|
||||
- Developer-Friendly: No lock-in—just import and customize as needed.
|
||||
|
||||
<br />
|
||||
|
||||
#### Drawbacks of ShadCN
|
||||
|
||||
- **Learning Curve:** If you're new to component libraries, it might take some time to get used to.
|
||||
- **Looks Generic:** ShadCN components look generic, as it's meant to be. So you might have to put some effort into making them look unique.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [4. Hero UI](https://www.heroui.com/)
|
||||
|
||||

|
||||
|
||||
Hero UI is a relatively newer UI library that offers a wide range of base and animated components for Next.js and React applications.
|
||||
|
||||
<br />
|
||||
|
||||
#### Why use Hero UI in 2025?
|
||||
|
||||
- Modern Design: Hero UI offers a modern and sleek design that is perfect for building responsive and accessible applications.
|
||||
- Animation Support: Most of it's components supports animations out of the box.
|
||||
- Developer-Friendly: Hero UI is easy to use and integrate with existing React projects.
|
||||
- CLI Support: Hero UI comes with a CLI that makes it easy to initialize new projects and install components.
|
||||
|
||||
#### Drawbacks
|
||||
|
||||
- **Limited Customization:** Hero UI has a opinionated design, so you might not be able to customize it as much as you want.
|
||||
|
||||
<br />
|
||||
|
||||
#### Use Case:
|
||||
|
||||
If you want to make your website look and feel like Apple's, Hero UI is the go-to choice.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## [5. Mantine](https://mantine.dev/)
|
||||
|
||||

|
||||
|
||||
Mentine is a fully featured React UI library that offers a wide range of components, hooks, and utilities for building modern web applications.
|
||||
|
||||
**Why Mantine in 2025?**
|
||||
|
||||
- Rich Component Library: Mantine offers a wide range of components, from basic `buttons` to complex `data tables`.
|
||||
- Hooks-Based: Mantine's hooks-based architecture makes it easy to integrate with existing React projects.
|
||||
- Extensions: Mentine offers some free extensions like `Code Highlight`, `Rich text editor` that can come in handy.
|
||||
|
||||
```jsx
|
||||
import { CodeHighlight } from "@mantine/code-highlight";
|
||||
|
||||
const exampleCode = `
|
||||
function Button() {
|
||||
return <button>Click me</button>;
|
||||
}
|
||||
`;
|
||||
|
||||
function Demo() {
|
||||
return (
|
||||
<CodeHighlight
|
||||
code={`// Custom copy label${exampleCode}`}
|
||||
language="tsx"
|
||||
copyLabel="Copy button code"
|
||||
copiedLabel="Copied!"
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- Focus on DX: Mantine prioritizes developer experience with excellent documentation and TypeScript support.
|
||||
|
||||
#### Drawbacks:
|
||||
|
||||
- Large Size: Mantine's extensive feature set comes at the cost of a larger bundle size!
|
||||
- Learning Curve: Mentine as so many features like `@mentine/hooks`, `@mentine/charts`, `extensions` and much more which can be overwhelming for beginners.
|
||||
|
||||
#### Use Case:
|
||||
|
||||
Mentine is a great choice for data intensive applications, dashboards, and admin panels.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
### **Conclusion**
|
||||
|
||||
In 2025, the React ecosystem is more vibrant than ever, with UI libraries catering to a wide range of needs. Whether you're looking for a minimal design system like [Shadcn](https://ui.shadcn.com), or an bold design system like [RetroUI](https://retroui.dev), there's a React UI library for almost every project.
|
||||
|
||||
When choosing a library, consider factors like customization, performance, accessibility, and community support. Each of these top 5 libraries has its strengths, so pick the one that aligns best with your project requirements and development style.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: Why Neo-Brutalism is Perfect for Modern Websites
|
||||
coverImage: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/blogs/why-neo-brutalism-best-for-modern-websites/cover.png
|
||||
description: Bored of how every website looks alike? Neo-brutalism breaks the pattern. It’s a design that stands out and might be the perfect fit for modern websites.
|
||||
tags:
|
||||
- ui/ux
|
||||
- design
|
||||
author:
|
||||
name: Imran Hossain
|
||||
avatar: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/people/imran.jpg
|
||||
linkedin: faraazhossainimran
|
||||
|
||||
status: published
|
||||
publishedAt: 18 August, 2025
|
||||
---
|
||||
|
||||
|
||||
Bored of how every website looks alike? Neo-brutalism breaks the pattern. It’s a design that stands out and might be the perfect fit for modern websites.
|
||||
|
||||
<br />
|
||||
|
||||
## **What exactly is Neo-Brutalism?**
|
||||
|
||||
Neo-brutalism is a design system, inspired by brutalist architecture, using concrete blocks, exposed structures, and a no-frills style. In web design, that means:
|
||||
|
||||
<br />
|
||||
|
||||
- → Sharp edges and grid layouts
|
||||
- → High-contrast colors
|
||||
- → Simple typography
|
||||
- → A “hand-coded” or minimalist feel
|
||||
|
||||

|
||||
|
||||
<br />
|
||||
|
||||
## **Why It Works So Well?**
|
||||
|
||||
Nowadays, every other website looks the same, making it hard to grab visitor attention!
|
||||
In cases like this Neo Brutalism can help you stand out from the crowd. While exploring at different styles, you come across **neo-brutalism,** a bold and raw design style. It uses plain colors, big text, and basic shapes. It's loud, different, and hard to ignore.
|
||||
|
||||

|
||||
<br />
|
||||
|
||||
You think, *“This could grab people’s attention.”*
|
||||
|
||||
But then you ask yourself, *“Does this fit the brand I’m working on?”*
|
||||
|
||||
If your brand wants to make a strong impression, neo-brutalism might be a great choice. But if the brand is more elegant, quiet, or professional, this design could feel too aggressive.
|
||||
|
||||
However, you might decide to try it on a small site, maybe a creative portfolio or a campaign. But for something long-term, or for a product that needs to work everywhere, you may pick a design that’s more flexible.
|
||||
|
||||
<br />
|
||||
|
||||
## What are Neo-brutalist Characteristics?
|
||||
|
||||
<br />
|
||||
|
||||
### 1. It Gets Noticed Fast
|
||||
|
||||

|
||||
|
||||
Most websites look similar, but neo-brutalist designs are different. Their simple style catches the eye right away. For brands like startups, designers, or creative people who want to be noticed, this is a big plus.
|
||||
|
||||
<br />
|
||||
|
||||
### 2. It’s Fast and Lightweight
|
||||
|
||||
Neo-brutalist websites usually avoid heavy animations and large frameworks. This makes them load quickly, run smoothly, and work better on mobile devices. It’s good for users, and Google likes it too.
|
||||
|
||||
<br />
|
||||
|
||||
### 3. It Feels Genuine
|
||||
|
||||

|
||||
|
||||
People today like things that feel genuine. Neo-brutalism skips the extra polish and shows things as they are. It’s not trying to hide anything or be fancy, and that honesty helps build trust.
|
||||
|
||||
<br />
|
||||
|
||||
### 4. It’s Developer-Friendly
|
||||
|
||||
Neo-brutalism uses simple HTML and basic styles. That makes it easier to build and maintain, especially for developers who want to focus on function over decoration.
|
||||
|
||||
<br />
|
||||
|
||||
### 5. Unleash Creativity
|
||||
|
||||
Neo-Brutalism is a playground for designers. It breaks away from traditional design rules and encourages creative freedom. For brands, this builds a unique brand identity that feels modern.
|
||||
|
||||
<br />
|
||||
|
||||
## Some Real World Examples
|
||||
Many websites use neobrutalist design to stand out. Here are a few examples:
|
||||
|
||||
<br />
|
||||
|
||||
### [Gumroad](https://gumroad.com)
|
||||
|
||||

|
||||
|
||||
**Gumroad**, a platform for independent creators, also follows a neobrutalist style. Its website looks raw and simple, helping users focus on what matters: selling their work easily and without distractions. This matches Gumroad’s mission of empowering creators with a straightforward, no-frills platform.
|
||||
|
||||
<br />
|
||||
|
||||
### [Feastables](https://feastables.com)
|
||||
|
||||

|
||||
|
||||
**Feastables**, the snack brand created by MrBeast, also uses neobrutalist design to stand out. The website is bold, colorful, and easy to understand. Instead of using shiny effects or fancy styles, Feastables keeps things raw, fun, and direct.
|
||||
|
||||
<br />
|
||||
|
||||
The bright colors, big text, and playful layout reflect the brand’s energy and MrBeast’s personality. Everything is made to be clear, fast, and exciting, just like his videos. This matches neobrutalism’s goal of being bold, functional, and different from typical polished designs.
|
||||
|
||||
Feastables uses design not just to look good, but to connect quickly with fans and make the brand feel fun and real.
|
||||
|
||||
<br />
|
||||
|
||||
## Final Thoughts
|
||||
|
||||
Neo-brutalism isn’t just about looking different. It’s about rethinking how websites communicate. In a world where everything is overdesigned, a raw, bold approach feels surprisingly fresh.
|
||||
|
||||
So if you’re building a modern website and want to stand out, maybe it’s time to try neo-brutalism.
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: Area Chart
|
||||
description: A customizable area chart component to visualize your data with gradient fills and smooth curves. 📈
|
||||
lastUpdated: 12 Aug, 2025
|
||||
links:
|
||||
source: https://github.com/logging-studio/RetroUI/blob/main/components/retroui/charts/AreaChart.tsx
|
||||
---
|
||||
|
||||
<ComponentShowcase name="area-chart-style-default" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## Installation
|
||||
|
||||
<ComponentInstall>
|
||||
<ComponentInstall.Cli npmCommand="npx shadcn@latest add 'https://retroui.dev/r/area-chart.json'" />
|
||||
<ComponentInstall.Manual>
|
||||
|
||||
#### 1. Install dependencies:
|
||||
|
||||
```sh
|
||||
npm install recharts
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### 2. Copy the code 👇 into your project:
|
||||
|
||||
<ComponentSource name="areaChart" />
|
||||
|
||||
</ComponentInstall.Manual>
|
||||
</ComponentInstall>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## Examples
|
||||
|
||||
### Default
|
||||
|
||||
<ComponentShowcase name="area-chart-style-default" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
### Multiple Categories
|
||||
|
||||
<ComponentShowcase name="area-chart-style-multiple" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
### Stacked Areas
|
||||
|
||||
<ComponentShowcase name="area-chart-style-stacked" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## API Reference
|
||||
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Prop</Table.Head>
|
||||
<Table.Head>Type</Table.Head>
|
||||
<Table.Head>Default</Table.Head>
|
||||
<Table.Head>Description</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">data</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`Record<string, any>[]`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`[]`</Table.Cell>
|
||||
<Table.Cell>Array of data objects to display</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">index</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">-</Table.Cell>
|
||||
<Table.Cell>Key for the x-axis (category) data</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">categories</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string[]`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`[]`</Table.Cell>
|
||||
<Table.Cell>Array of keys for the data values to display as areas</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">fill</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"gradient" | "solid"`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"gradient"`</Table.Cell>
|
||||
<Table.Cell>Fill style for the areas</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">strokeColors</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string[]`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`["var(--foreground)"]`</Table.Cell>
|
||||
<Table.Cell>Array of stroke colors for the areas</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">fillColors</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string[]`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`["var(--primary)"]`</Table.Cell>
|
||||
<Table.Cell>Array of fill colors for the areas</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">tooltipBgColor</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"var(--background)"`</Table.Cell>
|
||||
<Table.Cell>Background color for tooltips</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">tooltipBorderColor</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"var(--border)"`</Table.Cell>
|
||||
<Table.Cell>Border color for tooltips</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">gridColor</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"var(--muted)"`</Table.Cell>
|
||||
<Table.Cell>Color for the grid lines</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">valueFormatter</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`(value: number) => string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`(value) => value.toString()`</Table.Cell>
|
||||
<Table.Cell>Function to format values</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">showGrid</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`boolean`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`true`</Table.Cell>
|
||||
<Table.Cell>Whether to show grid lines</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">showTooltip</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`boolean`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`true`</Table.Cell>
|
||||
<Table.Cell>Whether to show tooltips on hover</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">className</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">-</Table.Cell>
|
||||
<Table.Cell>Additional CSS classes</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: Bar Chart
|
||||
description: A customizable bar chart component to visualize your data with support for multiple categories, custom colors, and horizontal alignment. 📊
|
||||
lastUpdated: 02 Jan, 2026
|
||||
links:
|
||||
source: https://github.com/logging-studio/RetroUI/blob/main/components/retroui/charts/BarChart.tsx
|
||||
---
|
||||
|
||||
<ComponentShowcase name="bar-chart-style-default" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## Installation
|
||||
|
||||
<ComponentInstall>
|
||||
<ComponentInstall.Cli npmCommand="npx shadcn@latest add 'https://retroui.dev/r/bar-chart.json'" />
|
||||
<ComponentInstall.Manual>
|
||||
|
||||
#### 1. Install dependencies:
|
||||
|
||||
```sh
|
||||
npm install recharts
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
#### 2. Copy the code 👇 into your project:
|
||||
|
||||
<ComponentSource name="barChart" />
|
||||
|
||||
</ComponentInstall.Manual>
|
||||
</ComponentInstall>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## Examples
|
||||
|
||||
### Default
|
||||
|
||||
<ComponentShowcase name="bar-chart-style-default" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
### Grouped Data
|
||||
|
||||
<ComponentShowcase name="bar-chart-style-grouped" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
### Stacked Data
|
||||
|
||||
<ComponentShowcase name="bar-chart-style-stacked" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
### Horizontal Alignment
|
||||
|
||||
<ComponentShowcase name="bar-chart-style-horizontal" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## API Reference
|
||||
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Prop</Table.Head>
|
||||
<Table.Head>Type</Table.Head>
|
||||
<Table.Head>Default</Table.Head>
|
||||
<Table.Head>Description</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">data</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`Record<string, any>[]`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`[]`</Table.Cell>
|
||||
<Table.Cell>Array of data objects to display</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">index</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">-</Table.Cell>
|
||||
<Table.Cell>Key for the x-axis (category) data</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">categories</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string[]`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`[]`</Table.Cell>
|
||||
<Table.Cell>Array of keys for the data values to display as bars</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">alignment</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"vertical" | "horizontal"`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"vertical"`</Table.Cell>
|
||||
<Table.Cell>Orientation of the bars</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">strokeColors</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string[]`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`["var(--foreground)"]`</Table.Cell>
|
||||
<Table.Cell>Array of stroke colors for the bars</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">fillColors</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string[]`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`["var(--primary)"]`</Table.Cell>
|
||||
<Table.Cell>Array of fill colors for the bars</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">tooltipBgColor</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"var(--background)"`</Table.Cell>
|
||||
<Table.Cell>Background color for tooltips</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">tooltipBorderColor</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"var(--border)"`</Table.Cell>
|
||||
<Table.Cell>Border color for tooltips</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">gridColor</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`"var(--muted)"`</Table.Cell>
|
||||
<Table.Cell>Color for the grid lines</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">valueFormatter</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`(value: number) => string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`(value) => value.toString()`</Table.Cell>
|
||||
<Table.Cell>Function to format values</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">showGrid</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`boolean`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`true`</Table.Cell>
|
||||
<Table.Cell>Whether to show grid lines</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">showTooltip</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`boolean`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`true`</Table.Cell>
|
||||
<Table.Cell>Whether to show tooltips on hover</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">stacked</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`boolean`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`false`</Table.Cell>
|
||||
<Table.Cell>Whether to stack the bars</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="font-medium">className</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">`string`</Table.Cell>
|
||||
<Table.Cell className="*:text-xs">-</Table.Cell>
|
||||
<Table.Cell>Additional CSS classes</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user