Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d15b16ecab | |||
| fe03c42fa1 | |||
| 25a918d146 | |||
| 87918ac5b4 | |||
| a30cec4810 | |||
| 1b0653c97a | |||
| 710395f3a2 | |||
| e7e58e854a | |||
| d13a14facc | |||
| cef7235c4b | |||
| fcff1a1d36 | |||
| c7ba9e1508 | |||
| 1d8dc4b9d8 | |||
| ea5e0fb096 | |||
| 26f64d043d | |||
| a078c3dcd2 | |||
| 5a50390e3f | |||
| 894d85d059 | |||
| 0ef41d27e7 | |||
| 4200164712 | |||
| 6d4c6980d5 | |||
| f3f36a7146 | |||
| 8de209bff7 | |||
| e9d01be74d | |||
| 66ac2e67ae | |||
| 7ed88f39c1 | |||
| 32d18bfd41 | |||
| a1b32c1ceb | |||
| 9e683b32ba | |||
| b7c947805d | |||
| a76fdfa774 | |||
| ac605eb71f | |||
| 5b343b6181 | |||
| 05562dcf90 | |||
| 0a60388258 | |||
| d794225cb7 | |||
| ec2ed200f5 | |||
| 2f7703ec8e | |||
| 25cbe1146f | |||
| 3db9b6248e | |||
| b1b5eda15b | |||
| e3e13a095f | |||
| 86f921d098 | |||
| eefda8d6e2 | |||
| 2e4ce7bcee | |||
| 5a4c4f498d | |||
| ec30a9adbd | |||
| a6954c4156 | |||
| b887a2f503 | |||
| edc2bc741e | |||
| a1a02dc0db | |||
| cd1dc8b469 | |||
| 459ac5ed45 | |||
| 9c4bb22e87 | |||
| 7fa5459381 | |||
| b77b8fa9d7 | |||
| c6c4a4d6a1 | |||
| e96b1dbab0 | |||
| 1b55d27645 | |||
| bdc246a8a4 | |||
| 5064bfe4e4 | |||
| 36ddbff557 | |||
| 043b42c895 | |||
| c8330b8106 | |||
| 7f20346423 | |||
| 85b3fd7269 | |||
| 649204ceac | |||
| c7a1376872 | |||
| 3323592211 | |||
| fb570a372d | |||
| 184d3a9c49 | |||
| 849eff5715 | |||
| b400f3ee6d | |||
| dedede0be0 | |||
| e96a23c8a3 | |||
| 7efd6c97e4 | |||
| 43e5e4fb26 | |||
| 39da69cf81 | |||
| 8743a6c6b6 | |||
| c635c951d6 | |||
| cb05342a4a | |||
| 59ef7bc6b6 | |||
| cfd19415ea | |||
| 39ca892b4e | |||
| fc2dad8b39 | |||
| d9bbd2e45f | |||
| 75bc039b4b | |||
| e79bd0fa4c | |||
| 65c5af4f63 | |||
| f58c8a7d99 | |||
| 9da06aaa50 | |||
| 508f11e968 | |||
| 2812f61f4f | |||
| 0ee0e95678 | |||
| e940ec06ac | |||
| 1976c62105 | |||
| aedda9d0de | |||
| 38afbba681 | |||
| cd941e64c1 | |||
| eec0ab7584 | |||
| a415a6f0f7 | |||
| 0008709813 | |||
| 8248f0c36f | |||
| 78556ab9ba | |||
| 02fd961a71 | |||
| b054bfa3b6 | |||
| 597ff59e51 | |||
| 3e5af52ae1 | |||
| 698b2a9e25 | |||
| b7477e008d | |||
| caf6166d49 | |||
| 61e07fbdde | |||
| 96f55f10b8 | |||
| 161cd23658 | |||
| 49e3009d7d | |||
| 3094c3d226 | |||
| 6cd84fed80 | |||
| b1a9bb2664 | |||
| 6796ccb76f | |||
| ad45bf5619 | |||
| 9f2e1249d5 | |||
| 12b5f0ed33 | |||
| 2b1841e362 | |||
| 9bd88dec48 | |||
| f0123e6be6 | |||
| 70a5853cd8 | |||
| ffd1c4d535 | |||
| d745cd114f | |||
| d1252d7c50 | |||
| 3277ef2b0a | |||
| ea2ab14d43 | |||
| 4f096a8096 | |||
| 01e764813d | |||
| 76ef3a9b7d | |||
| 5f35439503 | |||
| 598af42e2c | |||
| 3a1868baf5 | |||
| 72d7158034 | |||
| 504eb1dc47 | |||
| 1ff1c7b111 | |||
| 779f54f726 | |||
| a822eddc11 | |||
| 9cae2d2625 | |||
| fee73b4f77 | |||
| 4049eae3c6 | |||
| 874bbe87e7 | |||
| 950eabacfd | |||
| 6fe0a81cd2 | |||
| 8810699b14 | |||
| 7e29dddea3 | |||
| 63cf041067 | |||
| b4b75444e7 | |||
| 4ec33a4c78 | |||
| 5acfb2e7a9 | |||
| b64e97c94f | |||
| f4224c9c46 | |||
| 75dfb5461d | |||
| 1251cc9a9b | |||
| 4c52d1d1cf | |||
| fdc513cefe | |||
| 09f30f93aa | |||
| 6ccbabe788 | |||
| d752438a26 | |||
| 653e7d2031 | |||
| 2750189f66 | |||
| 29f0d9a937 | |||
| 5b5756bb33 | |||
| 10ac6ced32 | |||
| 3b0584ee83 | |||
| 4567cf525d | |||
| 77d56aae0c | |||
| 81c45e6336 | |||
| 00dcf6cbc8 | |||
| eab6dec219 | |||
| ca7309c6eb | |||
| 316010e210 | |||
| d11a3a8607 | |||
| 15b585c51a | |||
| 234f217a1c | |||
| 0267bf7c2f | |||
| 114c3f5a6b | |||
| ce9abed071 | |||
| c24efed184 | |||
| 6b88866307 | |||
| 436fd99b09 | |||
| 44722b6430 | |||
| cbced8df45 | |||
| 99993cd332 | |||
| f5db202b13 | |||
| 1658afc756 | |||
| f26cfbe9fb |
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "nextui-org/nextui" }
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
.now/*
|
||||
.next/*
|
||||
*.css
|
||||
examples/*
|
||||
.changeset
|
||||
dist
|
||||
esm/*
|
||||
public/*
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: "Install"
|
||||
description: "Sets up Node.js and runs install"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Setup Git User
|
||||
shell: bash
|
||||
run: |
|
||||
git config --global user.email "jrgarciadev@gmail.com"
|
||||
git config --global user.name "Junior Garcia"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm install
|
||||
@@ -0,0 +1,72 @@
|
||||
name: QA
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run ESLint
|
||||
run: pnpm lint
|
||||
|
||||
types:
|
||||
name: TypeScript
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
prettier:
|
||||
name: Prettier
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run prettier
|
||||
run: pnpm format:check
|
||||
@@ -1,128 +0,0 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore: ['**/README.md']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths-ignore: ['**/README.md']
|
||||
|
||||
jobs:
|
||||
avoid_reduncy:
|
||||
runs-on: ubuntu-20.04
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- name: Cancel Previous Redundant Builds
|
||||
uses: styfle/cancel-workflow-action@0.9.1
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.next/cache
|
||||
# Generate a new cache whenever packages or source files change.
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
tests:
|
||||
needs: [lint, build]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Test
|
||||
run: yarn coverage:update
|
||||
- uses: codecov/codecov-action@v2
|
||||
with:
|
||||
directory: './packages/react'
|
||||
|
||||
publish:
|
||||
needs: [build, tests]
|
||||
runs-on: ubuntu-20.04
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Pre puiblish NextUI package
|
||||
run: yarn pre-publish:nextui
|
||||
- uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
package: './packages/react/lib/package.json'
|
||||
- name: Show published version
|
||||
if: steps.publish.outputs.type != 'none'
|
||||
run: |
|
||||
echo "Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}"
|
||||
- name: Tag new version
|
||||
if: steps.publish.outputs.type != 'none'
|
||||
uses: Klemensas/action-autotag@stable
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
tag_prefix: "v"
|
||||
package_root: "./packages/react"
|
||||
|
||||
update_algolia_index:
|
||||
needs: [publish]
|
||||
runs-on: ubuntu-20.04
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Upload docs on Algolia
|
||||
run: yarn build:docs-meta
|
||||
env:
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_ADMIN_API_KEY: ${{ secrets.ALGOLIA_ADMIN_API_KEY }}
|
||||
ALGOLIA_SEARCH_API_KEY: ${{ secrets.ALGOLIA_SEARCH_API_KEY }}
|
||||
@@ -1,17 +1,51 @@
|
||||
name: Tagged Release
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
paths:
|
||||
- ".changeset/**"
|
||||
- "packages/**"
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
tagged-release:
|
||||
name: Tagged Release
|
||||
runs-on: ubuntu-18.04
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Tests
|
||||
run: pnpm test
|
||||
|
||||
- name: Create Release Pull Request or Publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
repo_token: "${{ github.token }}"
|
||||
prerelease: false
|
||||
publish: pnpm release
|
||||
commit: "ci(changesets): version packages"
|
||||
setupGitUser: false
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create @dev release
|
||||
if: steps.changesets.outputs.published != 'true'
|
||||
run: |
|
||||
git checkout main
|
||||
pnpm version:dev
|
||||
pnpm release:dev
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -20,6 +20,7 @@ coverage
|
||||
dist/
|
||||
storybook-static
|
||||
/packages/storybook/public/tailwind.css
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -59,3 +60,6 @@ apps/**/sitemap.xml
|
||||
# turbo
|
||||
.turbo
|
||||
packages/**/.turbo
|
||||
|
||||
# content layer
|
||||
.contentlayer
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
dist
|
||||
examples
|
||||
node_modules
|
||||
plop
|
||||
coverage
|
||||
.changeset
|
||||
.next
|
||||
build
|
||||
scripts
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
"company": null,
|
||||
"description": "Create custom web apps in minutes",
|
||||
"image": "/sponsors/327844.jpg",
|
||||
"email": null,
|
||||
"twitter": "https://twitter.com/lowdefy",
|
||||
"github": "https://github.com/lowdefy",
|
||||
"website": "https://lowdefy.com"
|
||||
@@ -110,16 +109,15 @@
|
||||
"role": "BACKER",
|
||||
"tier": "bronze sponsor 🥉",
|
||||
"isActive": true,
|
||||
"totalAmountDonated": 60,
|
||||
"totalAmountDonated": 70,
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-06-02 06:09",
|
||||
"lastTransactionAt": "2023-07-02 09:04",
|
||||
"lastTransactionAmount": 10,
|
||||
"profile": "https://opencollective.com/chartbrew",
|
||||
"name": "Chartbrew",
|
||||
"company": null,
|
||||
"description": "Open source visualization and reporting platform",
|
||||
"image": "/sponsors/374896.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": "https://chartbrew.com"
|
||||
@@ -160,7 +158,6 @@
|
||||
"company": null,
|
||||
"description": "LIKN is a powerful, but simple Web3 component and protocol. Connect Web2 content to Web3, and solely mint content NFTs by URL. Share and Trade in different platforms and marketplaces. OpenAI empowers LIKN which can automatically generate NFT metadata. ",
|
||||
"image": "/sponsors/395990.jpg",
|
||||
"email": null,
|
||||
"twitter": "https://twitter.com/oxlikn",
|
||||
"github": "https://github.com/0xLIKN",
|
||||
"website": "https://www.likn.co/?ref=opencollective"
|
||||
@@ -302,14 +299,14 @@
|
||||
"currency": "USD",
|
||||
"lastTransactionAt": "2023-06-13 13:51",
|
||||
"lastTransactionAmount": 5,
|
||||
"profile": "https://opencollective.com/jefferson-fonseca",
|
||||
"name": "Jefferson Fonseca",
|
||||
"company": null,
|
||||
"profile": "https://opencollective.com/encurme",
|
||||
"name": "EncurMe",
|
||||
"company": "EncurMe",
|
||||
"description": null,
|
||||
"image": "/sponsors/442878.jpg",
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": null,
|
||||
"website": null
|
||||
"website": "https://encur.me/"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,127 @@
|
||||
import type {Metadata} from "next";
|
||||
|
||||
import {notFound} from "next/navigation";
|
||||
import {allBlogPosts} from "contentlayer/generated";
|
||||
import {Link, User} from "@nextui-org/react";
|
||||
import {format, parseISO} from "date-fns";
|
||||
import NextLink from "next/link";
|
||||
import {Balancer} from "react-wrap-balancer";
|
||||
|
||||
import {MDXContent} from "@/components/mdx-content";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {ChevronRightLinearIcon} from "@/components/icons";
|
||||
|
||||
interface BlogPostProps {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function getBlogPostFromParams({params}: BlogPostProps) {
|
||||
const slug = params.slug || "";
|
||||
const post = allBlogPosts.find((post) => post.slugAsParams === slug);
|
||||
|
||||
if (!post) {
|
||||
null;
|
||||
}
|
||||
|
||||
const currentRoute: Route = {
|
||||
key: post?._id,
|
||||
title: post?.title,
|
||||
path: `/${post?._raw?.sourceFilePath}`,
|
||||
};
|
||||
|
||||
return {post, currentRoute};
|
||||
}
|
||||
|
||||
export async function generateMetadata({params}: BlogPostProps): Promise<Metadata> {
|
||||
const {post} = await getBlogPostFromParams({params});
|
||||
|
||||
if (!post) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
type: "article",
|
||||
url: post.url,
|
||||
images: [
|
||||
{
|
||||
url: post.imageAsParams || siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: post.title || siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: siteConfig.creator,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateStaticParams(): Promise<BlogPostProps["params"][]> {
|
||||
return allBlogPosts.map((doc) => ({
|
||||
slug: doc.slugAsParams,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function DocPage({params}: BlogPostProps) {
|
||||
const {post} = await getBlogPostFromParams({params});
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mt-12 flex flex-col justify-start items-center prose prose-neutral">
|
||||
<div className="w-full max-w-4xl">
|
||||
<Link
|
||||
isBlock
|
||||
as={NextLink}
|
||||
className="mb-8 -ml-3 text-default-500 hover:text-default-900"
|
||||
color="foreground"
|
||||
href="/blog"
|
||||
size="sm"
|
||||
>
|
||||
<ChevronRightLinearIcon className="rotate-180 inline-block mr-1" size={15} />
|
||||
Back to blog
|
||||
</Link>
|
||||
<time className="block text-small mb-2 text-default-500" dateTime={post.date}>
|
||||
{format(parseISO(post.date), "LLLL d, yyyy")}
|
||||
</time>
|
||||
<div className="mb-3 flex w-full flex-col items-start">
|
||||
<User
|
||||
isExternal
|
||||
as={Link}
|
||||
avatarProps={{
|
||||
className: "w-9 h-9 text-large",
|
||||
src: post.author?.avatar,
|
||||
}}
|
||||
className="hover:opacity-100"
|
||||
classNames={{
|
||||
base: "-ml-2 px-2 py-1.5 hover:bg-default-100 dark:hover:bg-default-50 cursor-pointer transition-colors",
|
||||
name: "text-foreground",
|
||||
}}
|
||||
description={post.author?.username}
|
||||
href={post.author?.link}
|
||||
name={post.author?.name}
|
||||
/>
|
||||
</div>
|
||||
<h1 className="mb-2 font-bold text-4xl">
|
||||
<Balancer>{post.title}</Balancer>
|
||||
</h1>
|
||||
<MDXContent code={post.body.code} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {Image} from "@nextui-org/react";
|
||||
|
||||
interface DocsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DocsLayout({children}: DocsLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<main className="relative container mx-auto max-w-7xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-70 -bottom-[40%] -left-[20%] z-0"
|
||||
>
|
||||
<Image removeWrapper alt="docs left background" src="/gradients/docs-left.png" />
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-70 -top-[80%] -right-[60%] 2xl:-top-[60%] 2xl:-right-[45%] z-0 rotate-12"
|
||||
>
|
||||
<Image removeWrapper alt="docs right background" src="/gradients/docs-right.png" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {allBlogPosts} from "contentlayer/generated";
|
||||
import {compareDesc} from "date-fns";
|
||||
|
||||
import {BlogPostList} from "@/components/blog-post";
|
||||
|
||||
export default function Blog() {
|
||||
const posts = allBlogPosts.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)));
|
||||
|
||||
return (
|
||||
<div className="w-full lg:px-16 mt-12">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-2 font-bold text-4xl">NextUI Latest Updates</h1>
|
||||
<h5 className="text-default-500 text-lg">All the latest news about NextUI.</h5>
|
||||
</div>
|
||||
<BlogPostList posts={posts} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,16 @@
|
||||
import type {Metadata} from "next";
|
||||
import type {MetaProps} from "@/libs/docs/meta";
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import {notFound} from "next/navigation";
|
||||
import {Spinner} from "@nextui-org/react";
|
||||
import rehypeSlugPlugin from "rehype-slug";
|
||||
import {serialize} from "next-mdx-remote/serialize";
|
||||
import matter from "gray-matter";
|
||||
import NextLink from "next/link";
|
||||
import {allDocs} from "contentlayer/generated";
|
||||
import {Link} from "@nextui-org/react";
|
||||
import {Suspense} from "react";
|
||||
|
||||
import {MDXRemote} from "@/components/mdx-remote";
|
||||
import manifest from "@/content/docs/manifest.json";
|
||||
import {findRouteByPath} from "@/libs/docs/page";
|
||||
import {extractHeadings, getAppSlug} from "@/libs/docs/utils";
|
||||
import {FooterNav} from "@/components/docs/footer-nav";
|
||||
import {DocsToc} from "@/components/docs/toc";
|
||||
import {MDXContent} from "@/components/mdx-content";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {DocsPager, DocsToc} from "@/components/docs";
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {GITHUB_URL, REPO_NAME} from "@/libs/github/constants";
|
||||
import {CONTENT_PATH, TAG} from "@/libs/docs/config";
|
||||
import {getHeadings} from "@/libs/docs/utils";
|
||||
|
||||
interface DocPageProps {
|
||||
params: {
|
||||
@@ -29,54 +18,40 @@ interface DocPageProps {
|
||||
};
|
||||
}
|
||||
|
||||
const cache = new Map();
|
||||
async function getDocFromParams({params}: DocPageProps) {
|
||||
const slug = params.slug?.join("/") || "";
|
||||
const doc = allDocs.find((doc) => doc.slugAsParams === slug);
|
||||
|
||||
export function getDocFromParams(params: {slug: string[]}) {
|
||||
let meta, doc;
|
||||
|
||||
const {slug} = getAppSlug(params);
|
||||
|
||||
if (!cache.has(slug)) {
|
||||
const folderPath = path.join(process.cwd(), "content");
|
||||
const filePath = path.join(folderPath, `${slug}.mdx`);
|
||||
const rawFileSource = fs.readFileSync(filePath);
|
||||
const {content, data} = matter(rawFileSource);
|
||||
|
||||
doc = content.toString();
|
||||
meta = data as MetaProps;
|
||||
|
||||
cache.set(slug, {doc, meta});
|
||||
} else {
|
||||
const cached = cache.get(slug);
|
||||
|
||||
doc = cached.doc;
|
||||
meta = cached.meta;
|
||||
if (!doc) {
|
||||
null;
|
||||
}
|
||||
|
||||
const currentRoute = findRouteByPath(slug, manifest.routes);
|
||||
const headings = getHeadings(doc?.body.raw);
|
||||
|
||||
return {
|
||||
doc,
|
||||
meta,
|
||||
currentRoute,
|
||||
const currentRoute: Route = {
|
||||
key: doc?._id,
|
||||
title: doc?.title,
|
||||
path: `/${doc?._raw?.sourceFilePath}`,
|
||||
};
|
||||
|
||||
return {doc, headings, currentRoute};
|
||||
}
|
||||
|
||||
export async function generateMetadata({params}: DocPageProps): Promise<Metadata> {
|
||||
const {doc, meta} = await getDocFromParams(params);
|
||||
const {doc} = await getDocFromParams({params});
|
||||
|
||||
if (!doc) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
openGraph: {
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
type: "article",
|
||||
url: meta.url,
|
||||
url: doc.url,
|
||||
images: [
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
@@ -88,74 +63,39 @@ export async function generateMetadata({params}: DocPageProps): Promise<Metadata
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: siteConfig.creator,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getDocData({params}: DocPageProps) {
|
||||
let meta, doc;
|
||||
|
||||
const {tag, slug} = getAppSlug(params);
|
||||
|
||||
const folderPath = path.join(process.cwd(), "content");
|
||||
const filePath = path.join(folderPath, `${slug}.mdx`);
|
||||
const rawFileSource = fs.readFileSync(filePath);
|
||||
const {content, data} = matter(rawFileSource);
|
||||
|
||||
doc = content.toString();
|
||||
meta = data;
|
||||
|
||||
const currentRoute = findRouteByPath(slug, manifest.routes);
|
||||
|
||||
const mdxSource = await serialize(doc, {
|
||||
mdxOptions: {
|
||||
rehypePlugins: [rehypeSlugPlugin as any],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
doc,
|
||||
tag,
|
||||
meta,
|
||||
slug,
|
||||
currentRoute,
|
||||
headings: extractHeadings(mdxSource.compiledSource),
|
||||
source: mdxSource,
|
||||
};
|
||||
export async function generateStaticParams(): Promise<DocPageProps["params"][]> {
|
||||
return allDocs.map((doc) => ({
|
||||
slug: doc.slugAsParams.split("/"),
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function DocPage({params}: DocPageProps) {
|
||||
const {doc, source, tag, slug, currentRoute, headings} = await getDocData({params});
|
||||
const {doc, headings, currentRoute} = await getDocFromParams({params});
|
||||
|
||||
if (!doc) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const editUrl = `${GITHUB_URL}/${REPO_NAME}/edit/${TAG}/${CONTENT_PATH}${currentRoute?.path}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-12 lg:col-span-10 xl:col-span-8 lg:px-16 mt-10">
|
||||
<div className="w-full prose prose-neutral">
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<MDXRemote source={source} />
|
||||
</Suspense>
|
||||
<MDXContent code={doc.body.code} />
|
||||
</div>
|
||||
{currentRoute && <FooterNav currentRoute={currentRoute} tag={tag} />}
|
||||
{currentRoute && <DocsPager currentRoute={currentRoute} />}
|
||||
<footer>
|
||||
{tag ? (
|
||||
<Link as={NextLink} href={slug || ""} size="sm">
|
||||
Go to the live version of this page
|
||||
</Link>
|
||||
) : (
|
||||
<Link isExternal showAnchorIcon href={editUrl} size="sm">
|
||||
Edit this page on GitHub
|
||||
</Link>
|
||||
)}
|
||||
<Link isExternal showAnchorIcon href={editUrl} size="sm">
|
||||
Edit this page on GitHub
|
||||
</Link>
|
||||
</footer>
|
||||
</div>
|
||||
{headings && headings.length > 0 && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Image} from "@nextui-org/react";
|
||||
|
||||
import manifest from "@/content/docs/manifest.json";
|
||||
import manifest from "@/config/routes.json";
|
||||
import {DocsSidebar} from "@/components/docs/sidebar";
|
||||
|
||||
interface DocsLayoutProps {
|
||||
@@ -10,7 +10,7 @@ interface DocsLayoutProps {
|
||||
export default function DocsLayout({children}: DocsLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<main className="relative container mx-auto max-w-7xl z-10 px-6 pt-16 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
|
||||
<main className="relative container mx-auto max-w-7xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="hidden relative z-10 lg:block lg:col-span-2 mt-8 pr-4">
|
||||
<DocsSidebar routes={manifest.routes} />
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardBody, CircularProgress} from "@nextui-org/react";
|
||||
|
||||
export default function ButtonDemo() {
|
||||
return (
|
||||
<main className="dark bg-background text-foreground">
|
||||
<div className="flex w-screen h-screen items-center justify-center">
|
||||
<Card className="w-[240px] h-[240px] bg-default-200 dark:bg-default-50">
|
||||
<CardBody className="justify-center items-center py-0">
|
||||
<CircularProgress
|
||||
classNames={{
|
||||
svg: "w-36 h-36 drop-shadow-md",
|
||||
indicator: "stroke-foreground",
|
||||
track: "stroke-foreground/10",
|
||||
value: "text-3xl font-semibold text-foreground",
|
||||
}}
|
||||
showValueLabel={true}
|
||||
strokeWidth={4}
|
||||
value={70}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
useDisclosure,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
ModalProps,
|
||||
} from "@nextui-org/react";
|
||||
import {useState} from "react";
|
||||
|
||||
export default function Page() {
|
||||
const {isOpen, onOpen, onOpenChange} = useDisclosure();
|
||||
const [modalPlacement, setModalPlacement] = useState("auto");
|
||||
|
||||
return (
|
||||
<div className="flex px-10 min-h-[80vh] justify-center items-center flex-col gap-4">
|
||||
<Button className="max-w-fit" onPress={onOpen}>
|
||||
Open Modal
|
||||
</Button>
|
||||
<RadioGroup
|
||||
label="Select modal placement"
|
||||
orientation="horizontal"
|
||||
value={modalPlacement}
|
||||
onValueChange={setModalPlacement}
|
||||
>
|
||||
<Radio value="auto">auto</Radio>
|
||||
<Radio value="top">top</Radio>
|
||||
<Radio value="bottom">bottom</Radio>
|
||||
<Radio value="center">center</Radio>
|
||||
<Radio value="top-center">top-center</Radio>
|
||||
<Radio value="bottom-center">bottom-center</Radio>
|
||||
</RadioGroup>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
placement={modalPlacement as ModalProps["placement"]}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">Modal Title</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non
|
||||
risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor
|
||||
quam.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non
|
||||
risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor
|
||||
quam.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
Action
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar isBordered>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
NavbarMenuToggle,
|
||||
Button,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
} from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
|
||||
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Log Out",
|
||||
];
|
||||
|
||||
return (
|
||||
<Navbar isBordered onMenuOpenChange={setIsMenuOpen}>
|
||||
<NavbarContent className="sm:hidden" justify="start">
|
||||
<NavbarMenuToggle aria-label={isMenuOpen ? "Close menu" : "Open menu"} />
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="sm:hidden pr-3" justify="center">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" color="warning" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="warning" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarMenu>
|
||||
{menuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
className="w-full"
|
||||
color={
|
||||
index === 2 ? "warning" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar
|
||||
classNames={{
|
||||
item: [
|
||||
"flex",
|
||||
"relative",
|
||||
"h-full",
|
||||
"items-center",
|
||||
"data-[active=true]:after:content-['']",
|
||||
"data-[active=true]:after:absolute",
|
||||
"data-[active=true]:after:bottom-0",
|
||||
"data-[active=true]:after:left-0",
|
||||
"data-[active=true]:after:right-0",
|
||||
"data-[active=true]:after:h-[2px]",
|
||||
"data-[active=true]:after:rounded-[2px]",
|
||||
"data-[active=true]:after:bg-primary",
|
||||
],
|
||||
}}
|
||||
>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Button,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
NavbarMenuToggle,
|
||||
} from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Log Out",
|
||||
];
|
||||
|
||||
return (
|
||||
<Navbar disableAnimation isBordered>
|
||||
<NavbarContent className="sm:hidden" justify="start">
|
||||
<NavbarMenuToggle />
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="sm:hidden pr-3" justify="center">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" color="warning" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="warning" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarMenu>
|
||||
{menuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
className="w-full"
|
||||
color={
|
||||
index === 2 ? "warning" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar isBordered isBlurred={false}>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar shouldHideOnScroll>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
const Content = () => (
|
||||
<div className="px-6 flex gap-4 flex-col pb-16 flex-grow">
|
||||
<h1 className="mt-4 font-bold text-4xl">Lorem ipsum dolor sit amet</h1>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua. Purus gravida quis blandit turpis. Augue neque gravida in
|
||||
fermentum et sollicitudin ac orci. Et sollicitudin ac orci phasellus egestas. Elementum tempus
|
||||
egestas sed sed risus pretium quam vulputate. Interdum velit euismod in pellentesque massa
|
||||
placerat duis ultricies.
|
||||
</p>
|
||||
<p>
|
||||
Rhoncus mattis rhoncus urna neque viverra justo nec ultrices dui. Praesent semper feugiat nibh
|
||||
sed pulvinar. Ultrices gravida dictum fusce ut placerat orci nulla pellentesque. Malesuada
|
||||
proin libero nunc consequat interdum varius sit amet. Lectus quam id leo in vitae. Sed viverra
|
||||
tellus in hac habitasse platea dictumst. Vivamus at augue eget arcu. Augue mauris augue neque
|
||||
gravida in.
|
||||
</p>
|
||||
<p>
|
||||
Tincidunt vitae semper quis lectus nulla at volutpat diam. Gravida dictum fusce ut placerat.
|
||||
Erat velit scelerisque in dictum non. Tempus quam pellentesque nec nam aliquam sem et tortor
|
||||
consequat. Eu nisl nunc mi ipsum faucibus. Cras fermentum odio eu feugiat pretium nibh. Vel
|
||||
pharetra vel turpis nunc eget lorem dolor sed viverra. Sollicitudin tempor id eu nisl nunc mi
|
||||
ipsum faucibus. Sed id semper risus in hendrerit gravida rutrum. Eget nulla facilisi etiam
|
||||
dignissim. Erat imperdiet sed euismod nisi. Risus in hendrerit gravida rutrum quisque non
|
||||
tellus orci ac.
|
||||
</p>
|
||||
<p>
|
||||
Tempor orci dapibus ultrices in iaculis nunc sed augue lacus. In pellentesque massa placerat
|
||||
duis ultricies. Sit amet massa vitae tortor condimentum. Morbi tincidunt augue interdum velit
|
||||
euismod. Aliquet enim tortor at auctor urna nunc id. A scelerisque purus semper eget. Vitae
|
||||
justo eget magna fermentum iaculis. Arcu non odio euismod lacinia at quis. Et leo duis ut diam
|
||||
quam nulla porttitor massa. Eget nunc scelerisque viverra mauris. Suscipit tellus mauris a
|
||||
diam maecenas sed enim. Cras sed felis eget velit aliquet. Est placerat in egestas erat
|
||||
imperdiet sed euismod nisi porta. In ante metus dictum at tempor commodo. In cursus turpis
|
||||
massa tincidunt dui ut ornare lectus. Tempus iaculis urna id volutpat. Iaculis eu non diam
|
||||
phasellus vestibulum lorem sed risus.
|
||||
</p>
|
||||
<p>
|
||||
Ridiculus mus mauris vitae ultricies leo integer malesuada nunc vel. Imperdiet massa tincidunt
|
||||
nunc pulvinar sapien et ligula ullamcorper malesuada. Faucibus pulvinar elementum integer enim
|
||||
neque volutpat. Gravida arcu ac tortor dignissim convallis aenean. Lectus quam id leo in
|
||||
vitae. Ultricies tristique nulla aliquet enim tortor. Nec tincidunt praesent semper feugiat
|
||||
nibh sed. Imperdiet proin fermentum leo vel orci porta non pulvinar neque. Praesent semper
|
||||
feugiat nibh sed pulvinar proin gravida. Dis parturient montes nascetur ridiculus mus mauris.
|
||||
Rhoncus dolor purus non enim praesent elementum facilisis leo vel. Ut lectus arcu bibendum at.
|
||||
Integer enim neque volutpat ac. Diam sit amet nisl suscipit. Eros donec ac odio tempor orci
|
||||
dapibus ultrices in iaculis. Ullamcorper a lacus vestibulum sed arcu non odio euismod. Quis
|
||||
lectus nulla at volutpat diam ut. Turpis egestas integer eget aliquet. Adipiscing tristique
|
||||
risus nec feugiat in fermentum posuere. Morbi tempus iaculis urna id. Amet commodo nulla
|
||||
facilisi nullam vehicula ipsum a arcu.
|
||||
</p>
|
||||
<p>
|
||||
Rhoncus mattis rhoncus urna neque viverra justo nec ultrices dui. Praesent semper feugiat nibh
|
||||
sed pulvinar. Ultrices gravida dictum fusce ut placerat orci nulla pellentesque. Malesuada
|
||||
proin libero nunc consequat interdum varius sit amet. Lectus quam id leo in vitae. Sed viverra
|
||||
tellus in hac habitasse platea dictumst. Vivamus at augue eget arcu. Augue mauris augue neque
|
||||
gravida in.
|
||||
</p>
|
||||
<p>
|
||||
Tincidunt vitae semper quis lectus nulla at volutpat diam. Gravida dictum fusce ut placerat.
|
||||
Erat velit scelerisque in dictum non. Tempus quam pellentesque nec nam aliquam sem et tortor
|
||||
consequat. Eu nisl nunc mi ipsum faucibus. Cras fermentum odio eu feugiat pretium nibh. Vel
|
||||
pharetra vel turpis nunc eget lorem dolor sed viverra. Sollicitudin tempor id eu nisl nunc mi
|
||||
ipsum faucibus. Sed id semper risus in hendrerit gravida rutrum. Eget nulla facilisi etiam
|
||||
dignissim. Erat imperdiet sed euismod nisi. Risus in hendrerit gravida rutrum quisque non
|
||||
tellus orci ac.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function NavbarExamplesLayout({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{children}
|
||||
<Content />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar position="static">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
Avatar,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" color="secondary" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent as="div" justify="end">
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
className="transition-transform"
|
||||
color="secondary"
|
||||
name="Jason Hughes"
|
||||
size="sm"
|
||||
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Profile Actions" variant="flat">
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p className="font-semibold">Signed in as</p>
|
||||
<p className="font-semibold">zoey@example.com</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem key="settings">My Settings</DropdownItem>
|
||||
<DropdownItem key="team_settings">Team Settings</DropdownItem>
|
||||
<DropdownItem key="analytics">Analytics</DropdownItem>
|
||||
<DropdownItem key="system">System</DropdownItem>
|
||||
<DropdownItem key="configurations">Configurations</DropdownItem>
|
||||
<DropdownItem key="help_and_feedback">Help & Feedback</DropdownItem>
|
||||
<DropdownItem key="logout" color="danger">
|
||||
Log Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
} from "@nextui-org/react";
|
||||
import {ChevronDown, Lock, Activity, Flash, Server, TagUser, Scale} from "@nextui-org/shared-icons";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const icons = {
|
||||
chevron: <ChevronDown fill="currentColor" size={16} />,
|
||||
scale: <Scale className="text-warning" fill="currentColor" size={30} />,
|
||||
lock: <Lock className="text-success" fill="currentColor" size={30} />,
|
||||
activity: <Activity className="text-secondary" fill="currentColor" size={30} />,
|
||||
flash: <Flash className="text-primary" fill="currentColor" size={30} />,
|
||||
server: <Server className="text-success" fill="currentColor" size={30} />,
|
||||
user: <TagUser className="text-danger" fill="currentColor" size={30} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<Navbar>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<Dropdown>
|
||||
<NavbarItem>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
disableRipple
|
||||
className="p-0 bg-transparent data-[hover=true]:bg-transparent"
|
||||
endContent={icons.chevron}
|
||||
radius="sm"
|
||||
variant="light"
|
||||
>
|
||||
Features
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
</NavbarItem>
|
||||
<DropdownMenu
|
||||
aria-label="ACME features"
|
||||
className="w-[340px]"
|
||||
itemClasses={{
|
||||
base: "gap-4",
|
||||
}}
|
||||
>
|
||||
<DropdownItem
|
||||
key="autoscaling"
|
||||
description="ACME scales apps to meet user demand, automagically, based on load."
|
||||
startContent={icons.scale}
|
||||
>
|
||||
Autoscaling
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="usage_metrics"
|
||||
description="Real-time metrics to debug issues. Slow query added? We’ll show you exactly where."
|
||||
startContent={icons.activity}
|
||||
>
|
||||
Usage Metrics
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="production_ready"
|
||||
description="ACME runs on ACME, join us and others serving requests at web scale."
|
||||
startContent={icons.flash}
|
||||
>
|
||||
Production Ready
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="99_uptime"
|
||||
description="Applications stay on the grid with high availability and high uptime guarantees."
|
||||
startContent={icons.server}
|
||||
>
|
||||
+99% Uptime
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="supreme_support"
|
||||
description="Overcome any challenge with a supporting team ready to respond."
|
||||
startContent={icons.user}
|
||||
>
|
||||
+Supreme Support
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
NavbarMenuToggle,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
} from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
|
||||
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Log Out",
|
||||
];
|
||||
|
||||
return (
|
||||
<Navbar onMenuOpenChange={setIsMenuOpen}>
|
||||
<NavbarContent>
|
||||
<NavbarMenuToggle
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="sm:hidden"
|
||||
/>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem className="hidden lg:flex">
|
||||
<Link href="#">Login</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarMenu>
|
||||
{menuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
className="w-full"
|
||||
color={
|
||||
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Input,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
Avatar,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
import {SearchLinearIcon} from "@/components/icons";
|
||||
|
||||
const AcmeLogo = () => (
|
||||
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Navbar isBordered>
|
||||
<NavbarContent justify="start">
|
||||
<NavbarBrand className="mr-4">
|
||||
<AcmeLogo />
|
||||
<p className="hidden sm:block font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex gap-3">
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" color="secondary" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Integrations
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</NavbarContent>
|
||||
<NavbarContent as="div" className="items-center" justify="end">
|
||||
<Input
|
||||
classNames={{
|
||||
base: "max-w-full sm:max-w-[10rem] h-10",
|
||||
input: "text-small",
|
||||
inputWrapper: "font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20",
|
||||
}}
|
||||
placeholder="Type to search..."
|
||||
startContent={<SearchLinearIcon size={18} />}
|
||||
type="search"
|
||||
/>
|
||||
<Dropdown placement="bottom-end">
|
||||
<NavbarItem>
|
||||
<DropdownTrigger>
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
className="transition-transform"
|
||||
color="secondary"
|
||||
name="Jason Hughes"
|
||||
size="sm"
|
||||
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
</NavbarItem>
|
||||
<DropdownMenu aria-label="Profile Actions" variant="flat">
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p className="font-semibold">Signed in as</p>
|
||||
<p className="font-semibold">zoey@example.com</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem key="settings">My Settings</DropdownItem>
|
||||
<DropdownItem key="team_settings">Team Settings</DropdownItem>
|
||||
<DropdownItem key="analytics">Analytics</DropdownItem>
|
||||
<DropdownItem key="system">System</DropdownItem>
|
||||
<DropdownItem key="configurations">Configurations</DropdownItem>
|
||||
<DropdownItem key="help_and_feedback">Help & Feedback</DropdownItem>
|
||||
<DropdownItem key="logout" color="danger">
|
||||
Log Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Button,
|
||||
Accordion,
|
||||
Tabs,
|
||||
Textarea,
|
||||
Input,
|
||||
Tab,
|
||||
AccordionItem,
|
||||
extendVariants,
|
||||
} from "@nextui-org/react";
|
||||
import {useState} from "react";
|
||||
|
||||
import {SearchLinearIcon} from "@/components/icons";
|
||||
|
||||
const MyRadioGroup = () => {
|
||||
const [radio, setRadio] = useState("1");
|
||||
|
||||
return (
|
||||
<RadioGroup value={radio} onValueChange={setRadio}>
|
||||
<Radio value="1">Radio 1</Radio>
|
||||
<Radio value="2">Radio 2</Radio>
|
||||
<Radio value="3">Radio 3</Radio>
|
||||
<Radio value="4">Radio 4</Radio>
|
||||
<Radio value="5">Radio 5</Radio>
|
||||
<Radio value="6">Radio 6</Radio>
|
||||
<Radio value="7">Radio 7</Radio>
|
||||
<Radio value="8">Radio 8</Radio>
|
||||
<Radio value="9">Radio 9</Radio>
|
||||
<Radio value="10">Radio 10</Radio>
|
||||
<Radio value="11">Radio 11</Radio>
|
||||
<Radio value="12">Radio 12</Radio>
|
||||
<Radio value="13">Radio 13</Radio>
|
||||
<Radio value="14">Radio 14</Radio>
|
||||
<Radio value="15">Radio 15</Radio>
|
||||
<Radio value="16">Radio 16</Radio>
|
||||
<Radio value="17">Radio 17</Radio>
|
||||
<Radio value="18">Radio 18</Radio>
|
||||
<Radio value="19">Radio 19</Radio>
|
||||
<Radio value="20">Radio 20</Radio>
|
||||
<Radio value="21">Radio 21</Radio>
|
||||
<Radio value="22">Radio 22</Radio>
|
||||
<Radio value="23">Radio 23</Radio>
|
||||
<Radio value="24">Radio 24</Radio>
|
||||
<Radio value="25">Radio 25</Radio>
|
||||
<Radio value="26">Radio 26</Radio>
|
||||
<Radio value="27">Radio 27</Radio>
|
||||
<Radio value="28">Radio 28</Radio>
|
||||
<Radio value="29">Radio 29</Radio>
|
||||
<Radio value="30">Radio 30</Radio>
|
||||
<Radio value="31">Radio 31</Radio>
|
||||
<Radio value="32">Radio 32</Radio>
|
||||
<Radio value="33">Radio 33</Radio>
|
||||
<Radio value="34">Radio 34</Radio>
|
||||
<Radio value="35">Radio 35</Radio>
|
||||
<Radio value="36">Radio 36</Radio>
|
||||
<Radio value="37">Radio 37</Radio>
|
||||
<Radio value="38">Radio 38</Radio>
|
||||
<Radio value="39">Radio 39</Radio>
|
||||
<Radio value="40">Radio 40</Radio>
|
||||
<Radio value="41">Radio 41</Radio>
|
||||
<Radio value="42">Radio 42</Radio>
|
||||
<Radio value="43">Radio 43</Radio>
|
||||
<Radio value="44">Radio 44</Radio>
|
||||
<Radio value="45">Radio 45</Radio>
|
||||
<Radio value="46">Radio 46</Radio>
|
||||
<Radio value="47">Radio 47</Radio>
|
||||
<Radio value="48">Radio 48</Radio>
|
||||
<Radio value="49">Radio 49</Radio>
|
||||
<Radio value="50">Radio 50</Radio>
|
||||
<Radio value="51">Radio 51</Radio>
|
||||
<Radio value="52">Radio 52</Radio>
|
||||
<Radio value="53">Radio 53</Radio>
|
||||
<Radio value="54">Radio 54</Radio>
|
||||
<Radio value="55">Radio 55</Radio>
|
||||
<Radio value="56">Radio 56</Radio>
|
||||
<Radio value="57">Radio 57</Radio>
|
||||
<Radio value="58">Radio 58</Radio>
|
||||
<Radio value="59">Radio 59</Radio>
|
||||
<Radio value="60">Radio 60</Radio>
|
||||
<Radio value="61">Radio 61</Radio>
|
||||
<Radio value="62">Radio 62</Radio>
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const MyInput = extendVariants(Input, {
|
||||
variants: {
|
||||
color: {
|
||||
stone: {
|
||||
inputWrapper: [
|
||||
"bg-zinc-100",
|
||||
"border",
|
||||
"shadow",
|
||||
"transition-colors",
|
||||
"focus-within:bg-zinc-100",
|
||||
"data-[hover=true]:border-zinc-600",
|
||||
"data-[hover=true]:bg-zinc-100",
|
||||
"group-data-[focus=true]:border-zinc-600",
|
||||
// dark theme
|
||||
"dark:bg-zinc-900",
|
||||
"dark:border-zinc-800",
|
||||
"dark:data-[hover=true]:bg-zinc-900",
|
||||
"dark:focus-within:bg-zinc-900",
|
||||
],
|
||||
input: [
|
||||
"text-zinc-800",
|
||||
"placeholder:text-zinc-600",
|
||||
// dark theme
|
||||
"dark:text-zinc-400",
|
||||
"dark:placeholder:text-zinc-600",
|
||||
],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
xs: {
|
||||
inputWrapper: "h-unit-6 min-h-unit-6 px-1",
|
||||
input: "text-tiny",
|
||||
},
|
||||
md: {
|
||||
inputWrapper: "h-unit-10 min-h-unit-10",
|
||||
input: "text-small",
|
||||
},
|
||||
xl: {
|
||||
inputWrapper: "h-unit-14 min-h-unit-14",
|
||||
input: "text-medium",
|
||||
},
|
||||
},
|
||||
radius: {
|
||||
xs: {
|
||||
inputWrapper: "rounded",
|
||||
},
|
||||
sm: {
|
||||
inputWrapper: "rounded-[4px]",
|
||||
},
|
||||
},
|
||||
textSize: {
|
||||
base: {
|
||||
input: "text-base",
|
||||
},
|
||||
},
|
||||
removeLabel: {
|
||||
true: {
|
||||
label: "hidden",
|
||||
},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
color: "stone",
|
||||
textSize: "base",
|
||||
removeLabel: true,
|
||||
},
|
||||
});
|
||||
|
||||
const MyButton2 = extendVariants(Button, {
|
||||
variants: {
|
||||
color: {
|
||||
foreground:
|
||||
"bg-foreground text-background data-[hover=true]:bg-foreground/90 data-[pressed=true]:bg-foreground/80",
|
||||
},
|
||||
isScalable: {
|
||||
true: "scale-125",
|
||||
false: "",
|
||||
},
|
||||
size: {
|
||||
xl: "size--xl",
|
||||
"2xl": "size--2xl",
|
||||
},
|
||||
mySize: {
|
||||
lg: "px-12 py-6 text-lg",
|
||||
xl: "px-12 py-6 text-xl",
|
||||
},
|
||||
},
|
||||
defaultVariants: {},
|
||||
});
|
||||
|
||||
export default function NextUIPerf() {
|
||||
const [textA, setTextA] = useState<string>("");
|
||||
const [textB, setTextB] = useState<string>("");
|
||||
const [textC, setTextC] = useState<string>("");
|
||||
|
||||
return (
|
||||
<div className="w-full p-24 gap-4 flex flex-col">
|
||||
<Accordion>
|
||||
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
|
||||
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
|
||||
reprehenderit commodo magna proident anim ipsum ex. Mollit id amet officia nisi excepteur
|
||||
eu. Commodo elit commodo nisi nisi aute eu aliquip aliquip voluptate exercitation ullamco
|
||||
ipsum eiusmod veniam. Magna in laborum anim amet anim ex elit aliqua nostrud mollit.
|
||||
Pariatur ullamco cillum proident aliqua nostrud. Labore ea veniam cillum duis veniam in
|
||||
cupidatat voluptate eu officia. Ut laborum sunt nostrud magna. Ex magna esse cillum enim
|
||||
incididunt pariatur qui veniam dolor. Exercitation id culpa et enim mollit duis duis
|
||||
aliquip. Magna ullamco est cupidatat laboris irure pariatur aliquip duis aute cillum.
|
||||
Officia irure do laboris ea nisi sunt reprehenderit laboris irure. Ex eiusmod in duis
|
||||
veniam excepteur. Sunt et et laboris culpa. Mollit excepteur occaecat elit anim officia.
|
||||
Laborum commodo proident cupidatat pariatur eu veniam id qui do culpa. Quis consectetur
|
||||
adipisicing anim ex ea velit excepteur. Deserunt laboris ex aute sunt laborum tempor ea
|
||||
enim dolore ut in. Id aliqua Lorem exercitation qui velit nostrud anim reprehenderit enim.
|
||||
Nisi elit fugiat deserunt elit. Sit excepteur ipsum enim excepteur irure irure sint veniam
|
||||
elit consequat ea id. Lorem ea qui sunt enim occaecat excepteur officia ex consequat
|
||||
nostrud. Tempor sint Lorem est culpa do.
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
|
||||
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
|
||||
reprehenderit commodo magna proident anim ipsum ex. Mollit id amet officia nisi excepteur
|
||||
eu. Commodo elit commodo nisi nisi aute eu aliquip aliquip voluptate exercitation ullamco
|
||||
ipsum eiusmod veniam. Magna in laborum anim amet anim ex elit aliqua nostrud mollit.
|
||||
Pariatur ullamco cillum proident aliqua nostrud. Labore ea veniam cillum duis veniam in
|
||||
cupidatat voluptate eu officia. Ut laborum sunt nostrud magna. Ex magna esse cillum enim
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
|
||||
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
|
||||
reprehenderit commodo magna proident anim ipsum ex. Mollit id amet officia nisi excepteur
|
||||
eu. Commodo elit commodo nisi nisi aute eu aliquip aliquip voluptate exercitation ullamco
|
||||
ipsum eiusmod veniam. Magna in laborum anim amet anim ex elit aliqua nostrud mollit.
|
||||
Pariatur ullamco cillum proident aliqua nostrud. Labore ea veniam cillum duis veniam in
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Tabs classNames={{panel: "flex flex-col gap-5"}} variant="underlined">
|
||||
<Tab title="Test 1">
|
||||
<Textarea defaultValue="ASdasd" label="Default value (uncontrolled)" />
|
||||
<Input label="Text B Tab 1" value={textB} onValueChange={setTextB} />
|
||||
<Textarea label="Text C Tab 1" value={textC} onValueChange={setTextC} />
|
||||
</Tab>
|
||||
<Tab title="Test 2">
|
||||
<Textarea label="Text B Tab 2" value={textB} onValueChange={setTextB} />
|
||||
<Textarea label="Text C Tab 2" value={textC} onValueChange={setTextC} />
|
||||
</Tab>
|
||||
<Tab title="Test 3">
|
||||
<Textarea label="Text B Tab 3" value={textB} onValueChange={setTextB} />
|
||||
<Textarea label="Text C Tab 3" value={textC} onValueChange={setTextC} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<h2>Outside</h2>
|
||||
<Textarea label="Text A" placeholder="Text A" value={textA} onValueChange={setTextA} />
|
||||
<Textarea label="Text B" placeholder="Text B" value={textB} onValueChange={setTextB} />
|
||||
<Textarea label="Text C" placeholder="Text C" value={textC} onValueChange={setTextC} />
|
||||
|
||||
<MyRadioGroup />
|
||||
|
||||
<MyInput
|
||||
isClearable
|
||||
placeholder="Search..."
|
||||
radius="md"
|
||||
size="md"
|
||||
startContent={<SearchLinearIcon className="text-zinc-500" size={16} />}
|
||||
/>
|
||||
|
||||
<Button>Click Me!</Button>
|
||||
|
||||
<MyButton2 color="foreground">Press Me!</MyButton2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
Pagination,
|
||||
} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useCallback, useMemo, useState} from "react";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const rowsPerPage = 10;
|
||||
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, cursor}) {
|
||||
// If no cursor is available, then we're loading the first page.
|
||||
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
|
||||
const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal});
|
||||
let json = await res.json();
|
||||
|
||||
setTotal(json.count);
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
cursor: json.next,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const pages = Math.ceil(total / rowsPerPage);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const start = (page - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
return list.items.slice(start, end);
|
||||
}, [page, list.items?.length]);
|
||||
|
||||
const onPaginationChange = useCallback(
|
||||
(page: number) => {
|
||||
setIsLoading(true);
|
||||
if (page >= list.items.length / rowsPerPage) {
|
||||
list.loadMore();
|
||||
}
|
||||
setPage(page);
|
||||
},
|
||||
[list.items.length],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
aria-label="Example table with client async pagination"
|
||||
bottomContent={
|
||||
pages > 0 ? (
|
||||
<div className="flex w-full justify-center">
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={onPaginationChange}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
classNames={{
|
||||
table: "min-h-[400px]",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name">Name</TableColumn>
|
||||
<TableColumn key="height">Height</TableColumn>
|
||||
<TableColumn key="mass">Mass</TableColumn>
|
||||
<TableColumn key="birth_year">Birth year</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={isLoading && !items.length}
|
||||
items={items}
|
||||
loadingContent={<Spinner />}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
/* eslint-disable jsx-a11y/no-onchange */
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Input,
|
||||
Button,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Selection,
|
||||
Chip,
|
||||
User,
|
||||
ChipProps,
|
||||
Pagination,
|
||||
SortDescriptor,
|
||||
} from "@nextui-org/react";
|
||||
import {ChevronDownIcon, SearchIcon} from "@nextui-org/shared-icons";
|
||||
import {useCallback, useMemo, useState} from "react";
|
||||
import {capitalize} from "lodash";
|
||||
|
||||
import {PlusLinearIcon} from "@/components/icons";
|
||||
import {VerticalDotsIcon} from "@/components/icons/vertical-dots";
|
||||
|
||||
const statusColorMap: Record<string, ChipProps["color"]> = {
|
||||
active: "success",
|
||||
paused: "danger",
|
||||
vacation: "warning",
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{name: "ID", uid: "id", sortable: true},
|
||||
{name: "NAME", uid: "name", sortable: true},
|
||||
{name: "AGE", uid: "age", sortable: true},
|
||||
{name: "ROLE", uid: "role", sortable: true},
|
||||
{name: "TEAM", uid: "team"},
|
||||
{name: "EMAIL", uid: "email"},
|
||||
{name: "STATUS", uid: "status", sortable: true},
|
||||
{name: "ACTIONS", uid: "actions"},
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{name: "Active", uid: "active"},
|
||||
{name: "Paused", uid: "paused"},
|
||||
{name: "Vacation", uid: "vacation"},
|
||||
];
|
||||
|
||||
const INITIAL_VISIBLE_COLUMNS = ["name", "role", "status", "actions"];
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Tony Reichert",
|
||||
role: "CEO",
|
||||
team: "Management",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
email: "tony.reichert@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Zoey Lang",
|
||||
role: "Tech Lead",
|
||||
team: "Development",
|
||||
status: "paused",
|
||||
age: "25",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
|
||||
email: "zoey.lang@example.com",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Jane Fisher",
|
||||
role: "Sr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://i.pravatar.cc/150?u=a04258114e29026702d",
|
||||
email: "jane.fisher@example.com",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "William Howard",
|
||||
role: "C.M.",
|
||||
team: "Marketing",
|
||||
status: "vacation",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?u=a048581f4e29026701d",
|
||||
email: "william.howard@example.com",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kristen Copper",
|
||||
role: "S. Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "24",
|
||||
avatar: "https://i.pravatar.cc/150?u=a092581d4ef9026700d",
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
email: "brian.kim@example.com",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Michael Hunt",
|
||||
role: "Designer",
|
||||
team: "Design",
|
||||
status: "paused",
|
||||
age: "27",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29027007d",
|
||||
email: "michael.hunt@example.com",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Samantha Brooks",
|
||||
role: "HR Manager",
|
||||
team: "HR",
|
||||
status: "active",
|
||||
age: "31",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e27027008d",
|
||||
email: "samantha.brooks@example.com",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Frank Harrison",
|
||||
role: "F. Manager",
|
||||
team: "Finance",
|
||||
status: "vacation",
|
||||
age: "33",
|
||||
avatar: "https://i.pravatar.cc/150?img=4",
|
||||
email: "frank.harrison@example.com",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Emma Adams",
|
||||
role: "Ops Manager",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "35",
|
||||
avatar: "https://i.pravatar.cc/150?img=5",
|
||||
email: "emma.adams@example.com",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Brandon Stevens",
|
||||
role: "Jr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://i.pravatar.cc/150?img=8",
|
||||
email: "brandon.stevens@example.com",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Megan Richards",
|
||||
role: "P. Manager",
|
||||
team: "Product",
|
||||
status: "paused",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?img=10",
|
||||
email: "megan.richards@example.com",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Oliver Scott",
|
||||
role: "S. Manager",
|
||||
team: "Security",
|
||||
status: "active",
|
||||
age: "37",
|
||||
avatar: "https://i.pravatar.cc/150?img=12",
|
||||
email: "oliver.scott@example.com",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Grace Allen",
|
||||
role: "M. Specialist",
|
||||
team: "Marketing",
|
||||
status: "active",
|
||||
age: "30",
|
||||
avatar: "https://i.pravatar.cc/150?img=16",
|
||||
email: "grace.allen@example.com",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Noah Carter",
|
||||
role: "IT Specialist",
|
||||
team: "I. Technology",
|
||||
status: "paused",
|
||||
age: "31",
|
||||
avatar: "https://i.pravatar.cc/150?img=15",
|
||||
email: "noah.carter@example.com",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Ava Perez",
|
||||
role: "Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?img=20",
|
||||
email: "ava.perez@example.com",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Liam Johnson",
|
||||
role: "Data Analyst",
|
||||
team: "Analysis",
|
||||
status: "active",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?img=33",
|
||||
email: "liam.johnson@example.com",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Sophia Taylor",
|
||||
role: "QA Analyst",
|
||||
team: "Testing",
|
||||
status: "active",
|
||||
age: "27",
|
||||
avatar: "https://i.pravatar.cc/150?img=29",
|
||||
email: "sophia.taylor@example.com",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Lucas Harris",
|
||||
role: "Administrator",
|
||||
team: "Information Technology",
|
||||
status: "paused",
|
||||
age: "32",
|
||||
avatar: "https://i.pravatar.cc/150?img=50",
|
||||
email: "lucas.harris@example.com",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Mia Robinson",
|
||||
role: "Coordinator",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "26",
|
||||
avatar: "https://i.pravatar.cc/150?img=45",
|
||||
email: "mia.robinson@example.com",
|
||||
},
|
||||
];
|
||||
|
||||
type User = typeof users[0];
|
||||
|
||||
export default function Page() {
|
||||
const [filterValue, setFilterValue] = useState("");
|
||||
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
||||
const [visibleColumns, setVisibleColumns] = useState<Selection>(new Set(INITIAL_VISIBLE_COLUMNS));
|
||||
const [statusFilter, setStatusFilter] = useState<Selection>("all");
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||
column: "age",
|
||||
direction: "ascending",
|
||||
});
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const pages = Math.ceil(users.length / rowsPerPage);
|
||||
|
||||
const hasSearchFilter = Boolean(filterValue);
|
||||
|
||||
const headerColumns = useMemo(() => {
|
||||
if (visibleColumns === "all") return columns;
|
||||
|
||||
return columns.filter((column) => Array.from(visibleColumns).includes(column.uid));
|
||||
}, [visibleColumns]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
let filteredUsers = [...users];
|
||||
|
||||
if (hasSearchFilter) {
|
||||
filteredUsers = filteredUsers.filter((user) =>
|
||||
user.name.toLowerCase().includes(filterValue.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (statusFilter !== "all" && Array.from(statusFilter).length !== statusOptions.length) {
|
||||
filteredUsers = filteredUsers.filter((user) =>
|
||||
Array.from(statusFilter).includes(user.status),
|
||||
);
|
||||
}
|
||||
|
||||
return filteredUsers;
|
||||
}, [users, filterValue, statusFilter]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const start = (page - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
return filteredItems.slice(start, end);
|
||||
}, [page, filteredItems, rowsPerPage]);
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
return [...items].sort((a: User, b: User) => {
|
||||
const first = a[sortDescriptor.column as keyof User] as number;
|
||||
const second = b[sortDescriptor.column as keyof User] as number;
|
||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||
|
||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
||||
});
|
||||
}, [sortDescriptor, items]);
|
||||
|
||||
const renderCell = useCallback((user: User, columnKey: React.Key) => {
|
||||
const cellValue = user[columnKey as keyof User];
|
||||
|
||||
switch (columnKey) {
|
||||
case "name":
|
||||
return (
|
||||
<User
|
||||
avatarProps={{radius: "full", size: "sm", src: user.avatar}}
|
||||
classNames={{
|
||||
description: "text-default-500",
|
||||
}}
|
||||
description={user.email}
|
||||
name={cellValue}
|
||||
>
|
||||
{user.email}
|
||||
</User>
|
||||
);
|
||||
case "role":
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<p className="text-bold text-small capitalize">{cellValue}</p>
|
||||
<p className="text-bold text-tiny capitalize text-default-500">{user.team}</p>
|
||||
</div>
|
||||
);
|
||||
case "status":
|
||||
return (
|
||||
<Chip
|
||||
className="capitalize border-none gap-1 text-default-600"
|
||||
color={statusColorMap[user.status]}
|
||||
size="sm"
|
||||
variant="dot"
|
||||
>
|
||||
{cellValue}
|
||||
</Chip>
|
||||
);
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex justify-end items-center gap-2">
|
||||
<Dropdown className="bg-background border-1 border-default-200">
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly radius="full" size="sm" variant="light">
|
||||
<VerticalDotsIcon className="text-default-400" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem>View</DropdownItem>
|
||||
<DropdownItem>Edit</DropdownItem>
|
||||
<DropdownItem>Delete</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onRowsPerPageChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setRowsPerPage(Number(e.target.value));
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback((value?: string) => {
|
||||
if (value) {
|
||||
setFilterValue(value);
|
||||
setPage(1);
|
||||
} else {
|
||||
setFilterValue("");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const topContent = useMemo(() => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between gap-3 items-end">
|
||||
<Input
|
||||
isClearable
|
||||
classNames={{
|
||||
base: "w-full sm:max-w-[44%]",
|
||||
inputWrapper: "border-1",
|
||||
}}
|
||||
placeholder="Search by name..."
|
||||
size="sm"
|
||||
startContent={<SearchIcon className="text-default-300" />}
|
||||
value={filterValue}
|
||||
variant="bordered"
|
||||
onClear={() => setFilterValue("")}
|
||||
onValueChange={onSearchChange}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<Dropdown>
|
||||
<DropdownTrigger className="hidden sm:flex">
|
||||
<Button
|
||||
endContent={<ChevronDownIcon className="text-small" />}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
Status
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Table Columns"
|
||||
closeOnSelect={false}
|
||||
selectedKeys={statusFilter}
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={setStatusFilter}
|
||||
>
|
||||
{statusOptions.map((status) => (
|
||||
<DropdownItem key={status.uid} className="capitalize">
|
||||
{capitalize(status.name)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<DropdownTrigger className="hidden sm:flex">
|
||||
<Button
|
||||
endContent={<ChevronDownIcon className="text-small" />}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
Columns
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Table Columns"
|
||||
closeOnSelect={false}
|
||||
selectedKeys={visibleColumns}
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={setVisibleColumns}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<DropdownItem key={column.uid} className="capitalize">
|
||||
{capitalize(column.name)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Button
|
||||
className="bg-foreground text-background"
|
||||
endContent={<PlusLinearIcon />}
|
||||
size="sm"
|
||||
>
|
||||
Add New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-default-400 text-small">Total {users.length} users</span>
|
||||
<label className="flex items-center text-default-400 text-small">
|
||||
Rows per page:
|
||||
<select
|
||||
className="bg-transparent outline-none text-default-400 text-small"
|
||||
onChange={onRowsPerPageChange}
|
||||
>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="15">15</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
filterValue,
|
||||
statusFilter,
|
||||
visibleColumns,
|
||||
onSearchChange,
|
||||
onRowsPerPageChange,
|
||||
users.length,
|
||||
hasSearchFilter,
|
||||
]);
|
||||
|
||||
const bottomContent = useMemo(() => {
|
||||
return (
|
||||
<div className="py-2 px-2 flex justify-between items-center">
|
||||
<Pagination
|
||||
showControls
|
||||
classNames={{
|
||||
cursor: "bg-foreground text-background",
|
||||
}}
|
||||
color="default"
|
||||
isDisabled={hasSearchFilter}
|
||||
page={page}
|
||||
total={pages}
|
||||
variant="light"
|
||||
onChange={setPage}
|
||||
/>
|
||||
<span className="text-small text-default-400">
|
||||
{selectedKeys === "all"
|
||||
? "All items selected"
|
||||
: `${selectedKeys.size} of ${items.length} selected`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}, [selectedKeys, items.length, page, pages, hasSearchFilter]);
|
||||
|
||||
const classNames = useMemo(
|
||||
() => ({
|
||||
wrapper: ["max-h-[382px]", "max-w-3xl"],
|
||||
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
|
||||
td: [
|
||||
// changing the rows border radius
|
||||
// first
|
||||
"group-data-[first=true]:first:before:rounded-none",
|
||||
"group-data-[first=true]:last:before:rounded-none",
|
||||
// middle
|
||||
"group-data-[middle=true]:before:rounded-none",
|
||||
// last
|
||||
"group-data-[last=true]:first:before:rounded-none",
|
||||
"group-data-[last=true]:last:before:rounded-none",
|
||||
],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
isCompact
|
||||
removeWrapper
|
||||
aria-label="Example table with custom cells, pagination and sorting"
|
||||
bottomContent={bottomContent}
|
||||
bottomContentPlacement="outside"
|
||||
checkboxesProps={{
|
||||
classNames: {
|
||||
wrapper: "after:bg-foreground after:text-background text-background",
|
||||
},
|
||||
}}
|
||||
classNames={classNames}
|
||||
selectedKeys={selectedKeys}
|
||||
selectionMode="multiple"
|
||||
sortDescriptor={sortDescriptor}
|
||||
topContent={topContent}
|
||||
topContentPlacement="outside"
|
||||
onSelectionChange={setSelectedKeys}
|
||||
onSortChange={setSortDescriptor}
|
||||
>
|
||||
<TableHeader columns={headerColumns}>
|
||||
{(column) => (
|
||||
<TableColumn
|
||||
key={column.uid}
|
||||
align={column.uid === "actions" ? "center" : "start"}
|
||||
allowsSorting={column.sortable}
|
||||
>
|
||||
{column.name}
|
||||
</TableColumn>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody emptyContent={"No users found"} items={sortedItems}>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";
|
||||
import {useState} from "react";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, cursor}) {
|
||||
if (cursor) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
// If no cursor is available, then we're loading the first page.
|
||||
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
|
||||
const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal});
|
||||
let json = await res.json();
|
||||
|
||||
setHasMore(json.next !== null);
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
cursor: json.next,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const [loaderRef, scrollerRef] = useInfiniteScroll({hasMore, onLoadMore: list.loadMore});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
isHeaderSticky
|
||||
aria-label="Example table with infinite pagination"
|
||||
baseRef={scrollerRef}
|
||||
bottomContent={
|
||||
hasMore ? (
|
||||
<div className="flex w-full justify-center">
|
||||
<Spinner ref={loaderRef} color="white" />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
classNames={{
|
||||
base: "max-h-[520px] overflow-scroll",
|
||||
table: "min-h-[400px]",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name">Name</TableColumn>
|
||||
<TableColumn key="height">Height</TableColumn>
|
||||
<TableColumn key="mass">Mass</TableColumn>
|
||||
<TableColumn key="birth_year">Birth year</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={isLoading}
|
||||
items={list.items}
|
||||
loadingContent={<Spinner color="white" />}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useState} from "react";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, cursor}) {
|
||||
if (cursor) {
|
||||
setPage((prev) => prev + 1);
|
||||
}
|
||||
|
||||
// If no cursor is available, then we're loading the first page.
|
||||
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
|
||||
const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal});
|
||||
let json = await res.json();
|
||||
|
||||
if (!cursor) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
cursor: json.next,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const hasMore = page < 9;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
isHeaderSticky
|
||||
aria-label="Example table with client side sorting"
|
||||
bottomContent={
|
||||
hasMore && !isLoading ? (
|
||||
<div className="flex w-full justify-center">
|
||||
<Button isDisabled={list.isLoading} variant="flat" onPress={list.loadMore}>
|
||||
{list.isLoading && <Spinner color="white" size="sm" />}
|
||||
Load More
|
||||
</Button>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
classNames={{
|
||||
base: "max-h-[520px] overflow-scroll",
|
||||
table: "min-h-[420px]",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name">Name</TableColumn>
|
||||
<TableColumn key="height">Height</TableColumn>
|
||||
<TableColumn key="mass">Mass</TableColumn>
|
||||
<TableColumn key="birth_year">Birth year</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={isLoading}
|
||||
items={list.items}
|
||||
loadingContent={<Spinner label="Loading..." />}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useState} from "react";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal}) {
|
||||
let res = await fetch(`https://swapi.py4e.com/api/people/?search`, {
|
||||
signal,
|
||||
});
|
||||
let json = await res.json();
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
};
|
||||
},
|
||||
async sort({items, sortDescriptor}) {
|
||||
return {
|
||||
items: items.sort((a, b) => {
|
||||
let first = a[sortDescriptor.column as keyof SWCharacter];
|
||||
let second = b[sortDescriptor.column as keyof SWCharacter];
|
||||
let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1;
|
||||
|
||||
if (sortDescriptor.direction === "descending") {
|
||||
cmp *= -1;
|
||||
}
|
||||
|
||||
return cmp;
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
aria-label="Example table with client side sorting"
|
||||
classNames={{
|
||||
table: "min-h-[400px]",
|
||||
}}
|
||||
sortDescriptor={list.sortDescriptor}
|
||||
onSortChange={list.sort}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name" allowsSorting>
|
||||
Name
|
||||
</TableColumn>
|
||||
<TableColumn key="height" allowsSorting>
|
||||
Height
|
||||
</TableColumn>
|
||||
<TableColumn key="mass" allowsSorting>
|
||||
Mass
|
||||
</TableColumn>
|
||||
<TableColumn key="birth_year" allowsSorting>
|
||||
Birth year
|
||||
</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={isLoading}
|
||||
items={list.items}
|
||||
loadingContent={<Spinner label="Loading..." />}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
/* eslint-disable jsx-a11y/no-onchange */
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Input,
|
||||
Button,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Selection,
|
||||
Chip,
|
||||
User,
|
||||
ChipProps,
|
||||
Pagination,
|
||||
SortDescriptor,
|
||||
} from "@nextui-org/react";
|
||||
import {ChevronDownIcon, SearchIcon} from "@nextui-org/shared-icons";
|
||||
import {useCallback, useMemo, useState} from "react";
|
||||
import {capitalize} from "lodash";
|
||||
|
||||
import {PlusLinearIcon} from "@/components/icons";
|
||||
import {VerticalDotsIcon} from "@/components/icons/vertical-dots";
|
||||
|
||||
const statusColorMap: Record<string, ChipProps["color"]> = {
|
||||
active: "success",
|
||||
paused: "danger",
|
||||
vacation: "warning",
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{name: "ID", uid: "id", sortable: true},
|
||||
{name: "NAME", uid: "name", sortable: true},
|
||||
{name: "AGE", uid: "age", sortable: true},
|
||||
{name: "ROLE", uid: "role", sortable: true},
|
||||
{name: "TEAM", uid: "team"},
|
||||
{name: "EMAIL", uid: "email"},
|
||||
{name: "STATUS", uid: "status", sortable: true},
|
||||
{name: "ACTIONS", uid: "actions"},
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{name: "Active", uid: "active"},
|
||||
{name: "Paused", uid: "paused"},
|
||||
{name: "Vacation", uid: "vacation"},
|
||||
];
|
||||
|
||||
const INITIAL_VISIBLE_COLUMNS = ["name", "role", "status", "actions"];
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Tony Reichert",
|
||||
role: "CEO",
|
||||
team: "Management",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
email: "tony.reichert@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Zoey Lang",
|
||||
role: "Tech Lead",
|
||||
team: "Development",
|
||||
status: "paused",
|
||||
age: "25",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
|
||||
email: "zoey.lang@example.com",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Jane Fisher",
|
||||
role: "Sr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://i.pravatar.cc/150?u=a04258114e29026702d",
|
||||
email: "jane.fisher@example.com",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "William Howard",
|
||||
role: "C.M.",
|
||||
team: "Marketing",
|
||||
status: "vacation",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?u=a048581f4e29026701d",
|
||||
email: "william.howard@example.com",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kristen Copper",
|
||||
role: "S. Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "24",
|
||||
avatar: "https://i.pravatar.cc/150?u=a092581d4ef9026700d",
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
email: "brian.kim@example.com",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Michael Hunt",
|
||||
role: "Designer",
|
||||
team: "Design",
|
||||
status: "paused",
|
||||
age: "27",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e29027007d",
|
||||
email: "michael.hunt@example.com",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Samantha Brooks",
|
||||
role: "HR Manager",
|
||||
team: "HR",
|
||||
status: "active",
|
||||
age: "31",
|
||||
avatar: "https://i.pravatar.cc/150?u=a042581f4e27027008d",
|
||||
email: "samantha.brooks@example.com",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Frank Harrison",
|
||||
role: "F. Manager",
|
||||
team: "Finance",
|
||||
status: "vacation",
|
||||
age: "33",
|
||||
avatar: "https://i.pravatar.cc/150?img=4",
|
||||
email: "frank.harrison@example.com",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Emma Adams",
|
||||
role: "Ops Manager",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "35",
|
||||
avatar: "https://i.pravatar.cc/150?img=5",
|
||||
email: "emma.adams@example.com",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Brandon Stevens",
|
||||
role: "Jr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://i.pravatar.cc/150?img=8",
|
||||
email: "brandon.stevens@example.com",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Megan Richards",
|
||||
role: "P. Manager",
|
||||
team: "Product",
|
||||
status: "paused",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?img=10",
|
||||
email: "megan.richards@example.com",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Oliver Scott",
|
||||
role: "S. Manager",
|
||||
team: "Security",
|
||||
status: "active",
|
||||
age: "37",
|
||||
avatar: "https://i.pravatar.cc/150?img=12",
|
||||
email: "oliver.scott@example.com",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Grace Allen",
|
||||
role: "M. Specialist",
|
||||
team: "Marketing",
|
||||
status: "active",
|
||||
age: "30",
|
||||
avatar: "https://i.pravatar.cc/150?img=16",
|
||||
email: "grace.allen@example.com",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Noah Carter",
|
||||
role: "IT Specialist",
|
||||
team: "I. Technology",
|
||||
status: "paused",
|
||||
age: "31",
|
||||
avatar: "https://i.pravatar.cc/150?img=15",
|
||||
email: "noah.carter@example.com",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Ava Perez",
|
||||
role: "Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://i.pravatar.cc/150?img=20",
|
||||
email: "ava.perez@example.com",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Liam Johnson",
|
||||
role: "Data Analyst",
|
||||
team: "Analysis",
|
||||
status: "active",
|
||||
age: "28",
|
||||
avatar: "https://i.pravatar.cc/150?img=33",
|
||||
email: "liam.johnson@example.com",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Sophia Taylor",
|
||||
role: "QA Analyst",
|
||||
team: "Testing",
|
||||
status: "active",
|
||||
age: "27",
|
||||
avatar: "https://i.pravatar.cc/150?img=29",
|
||||
email: "sophia.taylor@example.com",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Lucas Harris",
|
||||
role: "Administrator",
|
||||
team: "Information Technology",
|
||||
status: "paused",
|
||||
age: "32",
|
||||
avatar: "https://i.pravatar.cc/150?img=50",
|
||||
email: "lucas.harris@example.com",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Mia Robinson",
|
||||
role: "Coordinator",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "26",
|
||||
avatar: "https://i.pravatar.cc/150?img=45",
|
||||
email: "mia.robinson@example.com",
|
||||
},
|
||||
];
|
||||
|
||||
type User = typeof users[0];
|
||||
|
||||
export default function Page() {
|
||||
const [filterValue, setFilterValue] = useState("");
|
||||
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
|
||||
const [visibleColumns, setVisibleColumns] = useState<Selection>(new Set(INITIAL_VISIBLE_COLUMNS));
|
||||
const [statusFilter, setStatusFilter] = useState<Selection>("all");
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
|
||||
column: "age",
|
||||
direction: "ascending",
|
||||
});
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const hasSearchFilter = Boolean(filterValue);
|
||||
|
||||
const headerColumns = useMemo(() => {
|
||||
if (visibleColumns === "all") return columns;
|
||||
|
||||
return columns.filter((column) => Array.from(visibleColumns).includes(column.uid));
|
||||
}, [visibleColumns]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
let filteredUsers = [...users];
|
||||
|
||||
if (hasSearchFilter) {
|
||||
filteredUsers = filteredUsers.filter((user) =>
|
||||
user.name.toLowerCase().includes(filterValue.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (statusFilter !== "all" && Array.from(statusFilter).length !== statusOptions.length) {
|
||||
filteredUsers = filteredUsers.filter((user) =>
|
||||
Array.from(statusFilter).includes(user.status),
|
||||
);
|
||||
}
|
||||
|
||||
return filteredUsers;
|
||||
}, [users, filterValue, statusFilter]);
|
||||
|
||||
const pages = Math.ceil(filteredItems.length / rowsPerPage);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const start = (page - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
return filteredItems.slice(start, end);
|
||||
}, [page, filteredItems, rowsPerPage]);
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
return [...items].sort((a: User, b: User) => {
|
||||
const first = a[sortDescriptor.column as keyof User] as number;
|
||||
const second = b[sortDescriptor.column as keyof User] as number;
|
||||
const cmp = first < second ? -1 : first > second ? 1 : 0;
|
||||
|
||||
return sortDescriptor.direction === "descending" ? -cmp : cmp;
|
||||
});
|
||||
}, [sortDescriptor, items]);
|
||||
|
||||
const renderCell = useCallback((user: User, columnKey: React.Key) => {
|
||||
const cellValue = user[columnKey as keyof User];
|
||||
|
||||
switch (columnKey) {
|
||||
case "name":
|
||||
return (
|
||||
<User
|
||||
avatarProps={{radius: "lg", src: user.avatar}}
|
||||
description={user.email}
|
||||
name={cellValue}
|
||||
>
|
||||
{user.email}
|
||||
</User>
|
||||
);
|
||||
case "role":
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<p className="text-bold text-small capitalize">{cellValue}</p>
|
||||
<p className="text-bold text-tiny capitalize text-default-400">{user.team}</p>
|
||||
</div>
|
||||
);
|
||||
case "status":
|
||||
return (
|
||||
<Chip className="capitalize" color={statusColorMap[user.status]} size="sm" variant="flat">
|
||||
{cellValue}
|
||||
</Chip>
|
||||
);
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex justify-end items-center gap-2">
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly size="sm" variant="light">
|
||||
<VerticalDotsIcon className="text-default-300" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem>View</DropdownItem>
|
||||
<DropdownItem>Edit</DropdownItem>
|
||||
<DropdownItem>Delete</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onNextPage = useCallback(() => {
|
||||
if (page < pages) {
|
||||
setPage(page + 1);
|
||||
}
|
||||
}, [page, pages]);
|
||||
|
||||
const onPreviousPage = useCallback(() => {
|
||||
if (page > 1) {
|
||||
setPage(page - 1);
|
||||
}
|
||||
}, [page]);
|
||||
|
||||
const onRowsPerPageChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setRowsPerPage(Number(e.target.value));
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const onSearchChange = useCallback((value?: string) => {
|
||||
if (value) {
|
||||
setFilterValue(value);
|
||||
setPage(1);
|
||||
} else {
|
||||
setFilterValue("");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
setFilterValue("");
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const topContent = useMemo(() => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between gap-3 items-end">
|
||||
<Input
|
||||
isClearable
|
||||
className="w-full sm:max-w-[44%]"
|
||||
placeholder="Search by name..."
|
||||
startContent={<SearchIcon />}
|
||||
value={filterValue}
|
||||
onClear={() => onClear()}
|
||||
onValueChange={onSearchChange}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<Dropdown>
|
||||
<DropdownTrigger className="hidden sm:flex">
|
||||
<Button endContent={<ChevronDownIcon className="text-small" />} variant="flat">
|
||||
Status
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Table Columns"
|
||||
closeOnSelect={false}
|
||||
selectedKeys={statusFilter}
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={setStatusFilter}
|
||||
>
|
||||
{statusOptions.map((status) => (
|
||||
<DropdownItem key={status.uid} className="capitalize">
|
||||
{capitalize(status.name)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<DropdownTrigger className="hidden sm:flex">
|
||||
<Button endContent={<ChevronDownIcon className="text-small" />} variant="flat">
|
||||
Columns
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Table Columns"
|
||||
closeOnSelect={false}
|
||||
selectedKeys={visibleColumns}
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={setVisibleColumns}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<DropdownItem key={column.uid} className="capitalize">
|
||||
{capitalize(column.name)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Button color="primary" endContent={<PlusLinearIcon />}>
|
||||
Add New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-default-400 text-small">Total {users.length} users</span>
|
||||
<label className="flex items-center text-default-400 text-small">
|
||||
Rows per page:
|
||||
<select
|
||||
className="bg-transparent outline-none text-default-400 text-small"
|
||||
onChange={onRowsPerPageChange}
|
||||
>
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="15">15</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
filterValue,
|
||||
statusFilter,
|
||||
visibleColumns,
|
||||
onSearchChange,
|
||||
onRowsPerPageChange,
|
||||
users.length,
|
||||
hasSearchFilter,
|
||||
]);
|
||||
|
||||
const bottomContent = useMemo(() => {
|
||||
return (
|
||||
<div className="py-2 px-2 flex justify-between items-center">
|
||||
<span className="w-[30%] text-small text-default-400">
|
||||
{selectedKeys === "all"
|
||||
? "All items selected"
|
||||
: `${selectedKeys.size} of ${filteredItems.length} selected`}
|
||||
</span>
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={setPage}
|
||||
/>
|
||||
<div className="hidden sm:flex w-[30%] justify-end gap-2">
|
||||
<Button isDisabled={pages === 1} size="sm" variant="flat" onPress={onPreviousPage}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button isDisabled={pages === 1} size="sm" variant="flat" onPress={onNextPage}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [selectedKeys, items.length, page, pages, hasSearchFilter]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Table
|
||||
isHeaderSticky
|
||||
aria-label="Example table with custom cells, pagination and sorting"
|
||||
bottomContent={bottomContent}
|
||||
bottomContentPlacement="outside"
|
||||
classNames={{
|
||||
wrapper: "max-h-[382px]",
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
selectionMode="multiple"
|
||||
sortDescriptor={sortDescriptor}
|
||||
topContent={topContent}
|
||||
topContentPlacement="outside"
|
||||
onSelectionChange={setSelectedKeys}
|
||||
onSortChange={setSortDescriptor}
|
||||
>
|
||||
<TableHeader columns={headerColumns}>
|
||||
{(column) => (
|
||||
<TableColumn
|
||||
key={column.uid}
|
||||
align={column.uid === "actions" ? "center" : "start"}
|
||||
allowsSorting={column.sortable}
|
||||
>
|
||||
{column.name}
|
||||
</TableColumn>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody emptyContent={"No users found"} items={sortedItems}>
|
||||
{(item) => (
|
||||
<TableRow key={item.id}>
|
||||
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Rss from "rss";
|
||||
import {allBlogPosts} from "contentlayer/generated";
|
||||
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {allCoreContent} from "@/libs/contentlayer";
|
||||
|
||||
export async function GET() {
|
||||
const feed = new Rss({
|
||||
title: siteConfig.name,
|
||||
description: siteConfig.description,
|
||||
feed_url: `${siteConfig.siteUrl}/feed.xml`,
|
||||
site_url: siteConfig.siteUrl,
|
||||
webMaster: `${siteConfig.author} <${siteConfig.email}>`,
|
||||
managingEditor: `${siteConfig.author} <${siteConfig.email}>`,
|
||||
language: "en-US",
|
||||
});
|
||||
|
||||
allCoreContent(allBlogPosts).forEach((post) => {
|
||||
const author = post.author ? post.author : siteConfig.author;
|
||||
|
||||
feed.item({
|
||||
title: post.title,
|
||||
description: post.description ?? "",
|
||||
url: `${siteConfig.siteUrl}/blog/${post.slug}`,
|
||||
guid: `${siteConfig.siteUrl}/blog/${post.slug}`,
|
||||
date: post.date,
|
||||
author: `${author} <${siteConfig.email}>`,
|
||||
categories: post.tags ?? [],
|
||||
});
|
||||
});
|
||||
|
||||
return new Response(feed.xml(), {
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {Button, Image, Link} from "@nextui-org/react";
|
||||
|
||||
import {Blockquote} from "@/components/docs/components/blockquote";
|
||||
|
||||
export default function FigmaPage() {
|
||||
return (
|
||||
<>
|
||||
<main className="relative container mx-auto max-w-7xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
|
||||
<section className="w-full flex flex-col items-center lg:px-16 mt-12 gap-6">
|
||||
<div className="text-center max-w-xl">
|
||||
<h1 className="mb-2 font-bold text-4xl">NextUI Figma Kit</h1>
|
||||
<h5 className="text-default-500 text-lg">
|
||||
A Figma file that contains the basis of the NextUI design system to help you design
|
||||
your applications.
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
className="aspect-video w-full border border-transparent dark:border-default-200/50 object-fit rounded-xl shadow-lg"
|
||||
height="600"
|
||||
src="https://embed.figma.com/file/1267584376234254760/hf_embed?community_viewer=true&embed_host=nextui"
|
||||
title="NextUI Figma Kit"
|
||||
width="800"
|
||||
/>
|
||||
<div className="text-center max-w-2xl m-auto">
|
||||
<Button className="max-w-fit" color="default" variant="bordered">
|
||||
<Link
|
||||
isExternal
|
||||
showAnchorIcon
|
||||
className="text-current"
|
||||
href="https://www.figma.com/community/file/1267584376234254760"
|
||||
>
|
||||
Open in Figma
|
||||
</Link>
|
||||
</Button>
|
||||
<Blockquote color="warning">
|
||||
This file is still in development and will be continuously updated.
|
||||
</Blockquote>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-70 -bottom-[40%] -left-[20%] z-0"
|
||||
>
|
||||
<Image removeWrapper alt="docs left background" src="/gradients/docs-left.png" />
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="fixed hidden dark:md:block dark:opacity-70 -top-[80%] -right-[60%] 2xl:-top-[60%] 2xl:-right-[45%] z-0 rotate-12"
|
||||
>
|
||||
<Image removeWrapper alt="docs right background" src="/gradients/docs-right.png" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,15 +2,17 @@ import "@/styles/globals.css";
|
||||
import "@/styles/sandpack.css";
|
||||
import {Metadata} from "next";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {Analytics} from "@vercel/analytics/react";
|
||||
|
||||
import {Providers} from "./providers";
|
||||
|
||||
import {Cmdk} from "@/components/cmdk";
|
||||
import manifest from "@/content/docs/manifest.json";
|
||||
import manifest from "@/config/routes.json";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {fontSans} from "@/config/fonts";
|
||||
import {Navbar} from "@/components/navbar";
|
||||
import {Footer} from "@/components/footer";
|
||||
import {__PROD__} from "@/utils";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
@@ -38,7 +40,7 @@ export const metadata: Metadata = {
|
||||
],
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
shortcut: "/favicon-16x16.png",
|
||||
shortcut: "/favicon-32x32.png",
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
manifest: "/manifest.json",
|
||||
@@ -57,17 +59,18 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function RootLayout({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<html suppressHydrationWarning lang="en">
|
||||
<html suppressHydrationWarning dir="ltr" lang="en">
|
||||
<head />
|
||||
<body className={clsx("min-h-screen bg-background font-sans antialiased", fontSans.variable)}>
|
||||
<Providers themeProps={{attribute: "class", defaultTheme: "dark"}}>
|
||||
<div className="relative flex flex-col" id="app-container">
|
||||
<Navbar routes={manifest.routes} />
|
||||
<Navbar mobileRoutes={manifest.mobileRoutes} routes={manifest.routes} />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
<Cmdk />
|
||||
</Providers>
|
||||
{__PROD__ && <Analytics />}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Spacer} from "@nextui-org/react";
|
||||
import {Spacer} from "@nextui-org/spacer";
|
||||
|
||||
import {Hero} from "@/components/marketing/hero";
|
||||
import {FeaturesGrid} from "@/components/marketing/features-grid";
|
||||
@@ -29,7 +29,7 @@ export default async function Home() {
|
||||
const data = await getData();
|
||||
|
||||
return (
|
||||
<main className="container mx-auto max-w-7xl px-6 pt-16 flex-grow">
|
||||
<main className="container mx-auto max-w-7xl px-6 flex-grow">
|
||||
<section className="flex flex-col items-center justify-center">
|
||||
<Hero />
|
||||
<FeaturesGrid features={landingContent.topFeatures} />
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import React, {useCallback, useEffect} from "react";
|
||||
import Script from "next/script";
|
||||
|
||||
import carbonOptimize from "./carbon-optimize";
|
||||
|
||||
import {loadScript} from "@/utils/scripts";
|
||||
import {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
import {__PROD__} from "@/utils";
|
||||
|
||||
const EA_PROVIDER_RATIO = 0.85;
|
||||
|
||||
export const CarbonAd: React.FC<unknown> = () => {
|
||||
const carbonRef = React.useRef(null);
|
||||
const [showEthicalAds, setShowEthicalAds] = React.useState(false);
|
||||
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const loadEthicalAds = useCallback(() => {
|
||||
return new Promise((resolve) => {
|
||||
const script = document.createElement("script");
|
||||
|
||||
script.src = "https://media.ethicalads.io/media/client/ethicalads.min.js";
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
// @ts-ignore
|
||||
resolve(window.ethicalads);
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(script);
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const shouldShowEthicalAds = Math.random() < EA_PROVIDER_RATIO;
|
||||
|
||||
let loadCarbon: any = null;
|
||||
|
||||
const loadCarbonAds = () => {
|
||||
setShowEthicalAds(false);
|
||||
// The isolation logic of carbonads is flawed.
|
||||
// Once the script starts loading, it will asynchronous resolve, with no way to stop it.
|
||||
// This leads to duplication of the ad. To solve the issue, we debounce the load action.
|
||||
loadCarbon =
|
||||
isMounted &&
|
||||
setTimeout(() => {
|
||||
const script = loadScript(
|
||||
"https://cdn.carbonads.com/carbon.js?serve=CESIC53Y&placement=nextuiorg",
|
||||
carbonRef.current,
|
||||
);
|
||||
|
||||
script.id = "_carbonads_js";
|
||||
carbonOptimize.init();
|
||||
});
|
||||
};
|
||||
|
||||
const loadAdProvider = async () => {
|
||||
if (shouldShowEthicalAds) {
|
||||
try {
|
||||
const ethicalads = await loadEthicalAds();
|
||||
|
||||
if (!ethicalads) {
|
||||
loadCarbonAds();
|
||||
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
ethicalads.wait.then((placements) => {
|
||||
if (!placements.length) {
|
||||
loadCarbonAds();
|
||||
} else {
|
||||
setShowEthicalAds(true);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
loadCarbonAds();
|
||||
}
|
||||
} else {
|
||||
loadCarbonAds();
|
||||
}
|
||||
};
|
||||
|
||||
loadAdProvider();
|
||||
|
||||
return () => {
|
||||
loadCarbon && clearTimeout(loadCarbon);
|
||||
};
|
||||
}, [isMounted]);
|
||||
|
||||
if (!__PROD__) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<Script async src="https://media.ethicalads.io/media/client/ethicalads.min.js" />
|
||||
<div
|
||||
className="ea-container horizontal"
|
||||
data-ea-campaign-types="paid|publisher-house|community"
|
||||
data-ea-publisher="nextuiorg"
|
||||
data-ea-type="image"
|
||||
style={{display: showEthicalAds ? "block" : "none"}}
|
||||
/>
|
||||
</>
|
||||
<div className="carbon-ad-container" style={{display: showEthicalAds ? "none" : "block"}}>
|
||||
<span ref={carbonRef} id="carbon-ad" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
export default {
|
||||
isRefreshAble: function () {
|
||||
return !(
|
||||
typeof document.addEventListener === "undefined" || this.browserSupport().hidden === undefined
|
||||
);
|
||||
},
|
||||
browserSupport: function () {
|
||||
let hidden;
|
||||
let visibilityChange;
|
||||
|
||||
if (typeof document.hidden !== "undefined") {
|
||||
// Opera 12.10 and Firefox 18 and later support
|
||||
hidden = "hidden";
|
||||
visibilityChange = "visibilitychange";
|
||||
} else if (typeof document.msHidden !== "undefined") {
|
||||
hidden = "msHidden";
|
||||
visibilityChange = "msvisibilitychange";
|
||||
} else if (typeof document.webkitHidden !== "undefined") {
|
||||
hidden = "webkitHidden";
|
||||
visibilityChange = "webkitvisibilitychange";
|
||||
}
|
||||
|
||||
return {
|
||||
hidden: hidden,
|
||||
visibilityChange: visibilityChange,
|
||||
};
|
||||
},
|
||||
handleVisibilityChange: function () {
|
||||
const isElementInViewport = function (el) {
|
||||
let element = document.querySelector(el);
|
||||
let bounding = element.getBoundingClientRect();
|
||||
let isVisible;
|
||||
|
||||
if (
|
||||
bounding.top >= 0 &&
|
||||
bounding.left >= 0 &&
|
||||
bounding.right <= window.innerWidth &&
|
||||
bounding.bottom <= window.innerHeight
|
||||
) {
|
||||
isVisible = true;
|
||||
} else {
|
||||
isVisible = false;
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
};
|
||||
|
||||
if (!document.hidden) {
|
||||
if (typeof _carbonads !== "undefined" && isElementInViewport("#carbonads")) {
|
||||
// eslint-disable-next-line no-undef
|
||||
_carbonads.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
init: function () {
|
||||
if (this.isRefreshAble()) {
|
||||
document.addEventListener(
|
||||
this.browserSupport().visibilityChange,
|
||||
this.handleVisibilityChange,
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./carbon-ad";
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import {BlogPost} from "contentlayer/generated";
|
||||
import {Card, CardFooter, CardBody, CardHeader, Link, Avatar, Image} from "@nextui-org/react";
|
||||
import Balancer from "react-wrap-balancer";
|
||||
import {format, parseISO} from "date-fns";
|
||||
import NextLink from "next/link";
|
||||
import {AnimatePresence, motion} from "framer-motion";
|
||||
|
||||
import {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
|
||||
const BlogPostCard = (post: BlogPost) => {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isMounted && (
|
||||
<motion.div
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: 5}}
|
||||
initial={{opacity: 0, y: 5}}
|
||||
transition={{duration: 0.3}}
|
||||
>
|
||||
<Card
|
||||
isBlurred
|
||||
as={NextLink}
|
||||
className="p-2 border-transparent text-start bg-white/5 dark:bg-default-400/10 backdrop-blur-lg backdrop-saturate-[1.8]"
|
||||
href={post.url}
|
||||
isPressable={!!post.url}
|
||||
>
|
||||
<CardHeader>
|
||||
<Link
|
||||
as={NextLink}
|
||||
className="font-semibold "
|
||||
href={post.url}
|
||||
size="lg"
|
||||
underline="hover"
|
||||
>
|
||||
<Balancer>{post.title}</Balancer>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardBody className="pt-0 px-2 pb-1">
|
||||
<Image className="mb-3" src={post.image} />
|
||||
<p className="font-normal px-1 text-default-600">{post.description}</p>
|
||||
</CardBody>
|
||||
<CardFooter className="flex justify-between items-center">
|
||||
<time className="block text-small text-default-500" dateTime={post.date}>
|
||||
{format(parseISO(post.date), "LLLL d, yyyy")}
|
||||
</time>
|
||||
<Avatar size="sm" src={post.author?.avatar} />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlogPostList = ({posts}: {posts: BlogPost[]}) => {
|
||||
return (
|
||||
<div className="mt-10 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{posts.map((post, idx) => (
|
||||
<BlogPostCard key={idx} {...post} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./nextjs-templates";
|
||||
export * from "./video-in-view";
|
||||
@@ -0,0 +1,34 @@
|
||||
import {NewNextJSIcon} from "@/components/icons";
|
||||
import {FeaturesGrid} from "@/components/marketing/features-grid";
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
title: "Next.js 13 (App) Template",
|
||||
isExternal: true,
|
||||
description:
|
||||
"A Next.js 13 with app directory template pre-configured with NextUI (v2) and Tailwind CSS.",
|
||||
icon: <NewNextJSIcon height={40} width={40} />,
|
||||
href: "https://github.com/nextui-org/next-app-template",
|
||||
},
|
||||
{
|
||||
title: "Next.js 13 (Pages) Template",
|
||||
isExternal: true,
|
||||
description:
|
||||
"A Next.js 13 with pages directory template pre-configured with NextUI (v2) and Tailwind CSS.",
|
||||
icon: <NewNextJSIcon height={40} width={40} />,
|
||||
href: "https://github.com/nextui-org/next-pages-template",
|
||||
},
|
||||
];
|
||||
|
||||
export const NextJsTemplates = () => {
|
||||
return (
|
||||
<FeaturesGrid
|
||||
classNames={{
|
||||
base: "mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4",
|
||||
iconWrapper: "bg-default-300/20",
|
||||
body: "py-0",
|
||||
}}
|
||||
features={frameworks}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
"use client";
|
||||
|
||||
import {useInView} from "framer-motion";
|
||||
import {useRef, FC, useEffect, useState, useCallback} from "react";
|
||||
import {Button, cn, Spinner, Tooltip} from "@nextui-org/react";
|
||||
|
||||
import {PlayBoldIcon, PauseBoldIcon} from "@/components/icons";
|
||||
import {RotateLeftLinearIcon} from "@/components/icons";
|
||||
|
||||
interface VideoInViewProps {
|
||||
src: string;
|
||||
playMode?: "auto" | "manual";
|
||||
autoPlay?: boolean;
|
||||
poster?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
controls?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const VideoInView: FC<VideoInViewProps> = ({
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
poster,
|
||||
autoPlay = true,
|
||||
playMode = "manual",
|
||||
controls = false,
|
||||
className,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const isVisible = useInView(videoRef);
|
||||
|
||||
// play video when it is visible and playMode is auto
|
||||
useEffect(() => {
|
||||
if (playMode !== "auto") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
videoRef.current?.play();
|
||||
} else {
|
||||
videoRef.current?.pause();
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
const handleCanPlay = useCallback(() => {
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const videoEl = videoRef.current;
|
||||
|
||||
if (videoEl) {
|
||||
if (videoEl.readyState > 3) {
|
||||
// HAVE_FUTURE_DATA: enough data to start playing
|
||||
handleCanPlay();
|
||||
} else {
|
||||
videoEl.addEventListener("canplaythrough", handleCanPlay);
|
||||
}
|
||||
|
||||
// Cleanup the event listener
|
||||
return () => {
|
||||
videoEl.removeEventListener("canplaythrough", handleCanPlay);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onRestart = useCallback(() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime = 0;
|
||||
videoRef.current.play();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onTogglePlay = useCallback(() => {
|
||||
if (videoRef.current) {
|
||||
if (!isPlaying) {
|
||||
videoRef.current.play();
|
||||
} else {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
|
||||
setIsPlaying((v) => !v);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative data-[playing=true]:after:opacity-0 data-[playing=true]:after:z-[-1] after:content-[''] after:absolute after:inset-0 after:bg-black/30 after:z-20 after:transition-opacity"
|
||||
data-playing={isPlaying}
|
||||
>
|
||||
{isLoading && (
|
||||
<Spinner
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
color="secondary"
|
||||
size="lg"
|
||||
/>
|
||||
)}
|
||||
<Tooltip content={isPlaying ? "Pause" : "Play"} delay={1000}>
|
||||
<Button
|
||||
isIconOnly
|
||||
className="absolute z-50 right-12 top-3 border-1 border-transparent bg-transparent before:bg-white/10 before:content-[''] before:block before:z-[-1] before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-100 before:rounded-lg"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onPress={onTogglePlay}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<PauseBoldIcon className="text-white" size={16} />
|
||||
) : (
|
||||
<PlayBoldIcon className="text-white" size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Restart" delay={1000}>
|
||||
<Button
|
||||
isIconOnly
|
||||
className="absolute z-50 right-3 top-3 border-1 border-transparent bg-transparent before:bg-white/10 before:content-[''] before:block before:z-[-1] before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-100 before:rounded-lg"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onPress={onRestart}
|
||||
>
|
||||
<RotateLeftLinearIcon className="text-white" size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<video
|
||||
ref={videoRef}
|
||||
loop
|
||||
muted
|
||||
autoPlay={autoPlay && playMode === "auto"}
|
||||
className={cn(
|
||||
"w-full border border-transparent dark:border-default-200/50 object-fit rounded-xl shadow-lg",
|
||||
className,
|
||||
)}
|
||||
controls={controls}
|
||||
height={height}
|
||||
poster={poster}
|
||||
src={src}
|
||||
width={width}
|
||||
onCanPlay={handleCanPlay}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -7,11 +7,11 @@ import {matchSorter} from "match-sorter";
|
||||
import {Button, ButtonProps, Kbd, Modal, ModalContent} from "@nextui-org/react";
|
||||
import {CloseIcon} from "@nextui-org/shared-icons";
|
||||
import {tv} from "tailwind-variants";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {usePathname, useRouter} from "next/navigation";
|
||||
import MultiRef from "react-multi-ref";
|
||||
import {cn} from "@nextui-org/theme";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import {isAppleDevice} from "@react-aria/utils";
|
||||
import {isAppleDevice, isWebKit} from "@react-aria/utils";
|
||||
import {create} from "zustand";
|
||||
import {intersectionBy, isEmpty} from "lodash";
|
||||
import {writeStorage, useLocalStorage} from "@rehooks/local-storage";
|
||||
@@ -23,9 +23,11 @@ import {
|
||||
SearchLinearIcon,
|
||||
} from "./icons";
|
||||
|
||||
import searchData from "@/content/docs/search-meta.json";
|
||||
import searchData from "@/config/search-meta.json";
|
||||
import {useUpdateEffect} from "@/hooks/use-update-effect";
|
||||
|
||||
const hideOnPaths = ["examples"];
|
||||
|
||||
export interface CmdkStore {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -83,12 +85,12 @@ const cmdk = tv({
|
||||
"data-[active=true]:bg-primary",
|
||||
"data-[active=true]:text-primary-foreground",
|
||||
],
|
||||
leftWrapper: ["flex", "gap-3", "items-center"],
|
||||
leftWrapper: ["flex", "gap-3", "items-center", "w-full", "max-w-full"],
|
||||
leftIcon: [
|
||||
"text-default-500 dark:text-default-300",
|
||||
"group-data-[active=true]:text-primary-foreground",
|
||||
],
|
||||
itemContent: ["flex", "flex-col", "gap-0", "justify-center"],
|
||||
itemContent: ["flex", "flex-col", "gap-0", "justify-center", "max-w-[80%]"],
|
||||
itemParentTitle: [
|
||||
"text-default-400",
|
||||
"text-xs",
|
||||
@@ -96,6 +98,7 @@ const cmdk = tv({
|
||||
"select-none",
|
||||
],
|
||||
itemTitle: [
|
||||
"truncate",
|
||||
"text-default-500",
|
||||
"group-data-[active=true]:text-primary-foreground",
|
||||
"select-none",
|
||||
@@ -127,6 +130,8 @@ export const Cmdk: FC<{}> = () => {
|
||||
const [menuNodes] = useState(() => new MultiRef<number, HTMLElement>());
|
||||
const slots = useMemo(() => cmdk(), []);
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const eventRef = useRef<"mouse" | "keyboard">();
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
@@ -189,8 +194,6 @@ export const Cmdk: FC<{}> = () => {
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [isOpen]);
|
||||
|
||||
const onItemSelect = useCallback(
|
||||
@@ -269,11 +272,12 @@ export const Cmdk: FC<{}> = () => {
|
||||
return (
|
||||
<Button
|
||||
isIconOnly
|
||||
className={cn(
|
||||
className={clsx(
|
||||
"border data-[hover=true]:bg-content2 border-default-400 dark:border-default-100",
|
||||
className,
|
||||
)}
|
||||
size="xs"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onPress={onPress}
|
||||
>
|
||||
@@ -331,14 +335,17 @@ export const Cmdk: FC<{}> = () => {
|
||||
[activeItem, onItemSelect, CloseButton, slots],
|
||||
);
|
||||
|
||||
const shouldOpen = !hideOnPaths.some((path) => pathname.includes(path));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hideCloseButton
|
||||
backdrop="opaque"
|
||||
classNames={{
|
||||
wrapper: "md:items-start",
|
||||
base: [
|
||||
"mt-[20vh]",
|
||||
"border-small",
|
||||
"dark:border-default-100",
|
||||
"supports-[backdrop-filter]:bg-background/80",
|
||||
"dark:supports-[backdrop-filter]:bg-background/30",
|
||||
"supports-[backdrop-filter]:backdrop-blur-md",
|
||||
@@ -346,7 +353,7 @@ export const Cmdk: FC<{}> = () => {
|
||||
],
|
||||
backdrop: ["bg-black/80"],
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
isOpen={isOpen && shouldOpen}
|
||||
motionProps={{
|
||||
onAnimationComplete: () => {
|
||||
if (!isOpen) {
|
||||
@@ -354,16 +361,17 @@ export const Cmdk: FC<{}> = () => {
|
||||
}
|
||||
},
|
||||
}}
|
||||
placement="top-center"
|
||||
scrollBehavior="inside"
|
||||
size="xl"
|
||||
onClose={onClose}
|
||||
onClose={() => onClose()}
|
||||
>
|
||||
<ModalContent>
|
||||
<Command className={slots.base()} label="Quick search command" shouldFilter={false}>
|
||||
<div className={slots.header()}>
|
||||
<SearchLinearIcon className={slots.searchIcon()} strokeWidth={2} />
|
||||
<Command.Input
|
||||
autoFocus
|
||||
autoFocus={!isWebKit()}
|
||||
className={slots.input()}
|
||||
placeholder="Search documentation"
|
||||
value={query}
|
||||
|
||||
@@ -45,7 +45,7 @@ export const DemoCodeModal: FC<DemoCodeModalProps> = ({isOpen, code, title, subt
|
||||
setIsCodeVisible(isOpen);
|
||||
},
|
||||
}}
|
||||
radius={isMobile ? "none" : "2xl"}
|
||||
radius={isMobile ? "none" : "lg"}
|
||||
size={isMobile ? "full" : "2xl"}
|
||||
onClose={onClose}
|
||||
>
|
||||
|
||||
@@ -22,9 +22,8 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
return (
|
||||
<Card
|
||||
isBlurred
|
||||
className={clsx("border-none dark:bg-black/40 bg-background/60", className)}
|
||||
radius="2xl"
|
||||
shadow="2xl"
|
||||
className={clsx("border-none bg-background/60 dark:bg-default-100/50", className)}
|
||||
shadow="sm"
|
||||
{...otherProps}
|
||||
>
|
||||
<CardBody>
|
||||
@@ -34,11 +33,8 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
alt="Album cover"
|
||||
as={NextImage}
|
||||
className="object-cover"
|
||||
classNames={{
|
||||
wrapper: "shadow-black/20",
|
||||
}}
|
||||
height={300}
|
||||
shadow="lg"
|
||||
shadow="md"
|
||||
src="/images/album-cover.png"
|
||||
width={240}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
tv,
|
||||
VariantProps,
|
||||
CircularProgress,
|
||||
CircularProgressProps,
|
||||
circularProgress,
|
||||
} from "@nextui-org/react";
|
||||
import {FC} from "react";
|
||||
|
||||
const speedProgress = tv({
|
||||
extend: circularProgress,
|
||||
slots: {
|
||||
svg: "",
|
||||
label: "",
|
||||
value: "",
|
||||
},
|
||||
variants: {
|
||||
color: {
|
||||
olive: {
|
||||
svg: "text-[#84cc16]",
|
||||
},
|
||||
orange: {
|
||||
svg: "text-[#ff8c00]",
|
||||
},
|
||||
violet: {
|
||||
svg: "text-[#8b5cf6]",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
sm: {
|
||||
svg: "w-10 h-10",
|
||||
label: "text-small",
|
||||
value: "text-[0.5rem]",
|
||||
},
|
||||
md: {
|
||||
svg: "w-12 h-12",
|
||||
label: "text-medium",
|
||||
value: "text-small",
|
||||
},
|
||||
lg: {
|
||||
svg: "w-14 h-14",
|
||||
label: "text-medium",
|
||||
value: "text-[0.6rem]",
|
||||
},
|
||||
xl: {
|
||||
svg: "w-16 h-16",
|
||||
label: "text-large",
|
||||
value: "text-small",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
color: "olive",
|
||||
size: "sm",
|
||||
},
|
||||
});
|
||||
|
||||
type SpeedProgressVariants = VariantProps<typeof speedProgress>;
|
||||
|
||||
export interface SpeedProgressProps extends Omit<CircularProgressProps, "color" | "size"> {
|
||||
color?: SpeedProgressVariants["color"];
|
||||
size?: SpeedProgressVariants["size"];
|
||||
}
|
||||
|
||||
export const SpeedProgress: FC<SpeedProgressProps> = ({color, size, ...otherProps}) => {
|
||||
const slots = speedProgress({size, color});
|
||||
|
||||
return (
|
||||
<CircularProgress
|
||||
classNames={{
|
||||
svg: slots.svg(),
|
||||
label: slots.label(),
|
||||
value: slots.value(),
|
||||
}}
|
||||
// color={color} // not needed because is being passed from slots
|
||||
formatOptions={{style: "unit", unit: "kilometer"}}
|
||||
label="Speed"
|
||||
showValueLabel={true}
|
||||
// size={size}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// const MyApp = () => {
|
||||
// return (
|
||||
// <SpeedProgress color="orange" />
|
||||
// );
|
||||
// }
|
||||
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
import React from "react";
|
||||
|
||||
type Variants = {[K: string]: {[P: string]: string}};
|
||||
|
||||
type StringToBoolean<T> = T extends "true" | "false" ? boolean : T;
|
||||
|
||||
type StylesProps<V extends Variants> = {
|
||||
[K in keyof V]?: StringToBoolean<keyof V[K]>;
|
||||
} & {
|
||||
variants: V;
|
||||
[x: string]: any;
|
||||
};
|
||||
|
||||
type ComponentType<P = {}> = React.ComponentType<P> | React.ForwardRefExoticComponent<P>;
|
||||
|
||||
export function extendStyles<P extends Variants>(
|
||||
Component: ComponentType<StylesProps<P>>,
|
||||
styles: {variants: P},
|
||||
) {
|
||||
return React.forwardRef<HTMLButtonElement, StylesProps<P>>((props, ref) => {
|
||||
return <Component ref={ref} {...styles} {...props} />;
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export const UserTwitterCard: FC<UserTwitterCardProps> = ({className}) => {
|
||||
<Avatar
|
||||
isBordered
|
||||
ImgComponent={NextImage}
|
||||
alt="Zoey Lang"
|
||||
imgProps={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import {FC} from "react";
|
||||
import {tv, VariantProps} from "tailwind-variants";
|
||||
|
||||
const blockquoteStyles = tv({
|
||||
base: "border pl-4 bg-default-50 my-6 py-3 rounded-xl [&>p]:m-0",
|
||||
base: "border px-4 bg-default-50 my-6 py-3 rounded-xl [&>p]:m-0",
|
||||
variants: {
|
||||
color: {
|
||||
default: "border-default-100 bg-default-50/20",
|
||||
default: "border-default-200 dark:border-default-100 bg-default-200/20",
|
||||
primary: "border-primary-100 bg-primary-50/20",
|
||||
secondary: "border-secondary-100 bg-secondary-50/20",
|
||||
success: "border-success-100 bg-success-50/20",
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
import React, {useCallback, useMemo, useRef} from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import {Skeleton, Tab, Tabs} from "@nextui-org/react";
|
||||
import {motion, useInView} from "framer-motion";
|
||||
import {useInView} from "framer-motion";
|
||||
|
||||
import {useCodeDemo, UseCodeDemoProps} from "./use-code-demo";
|
||||
import WindowResizer, {WindowResizerProps} from "./window-resizer";
|
||||
|
||||
import {GradientBoxProps} from "@/components/gradient-box";
|
||||
|
||||
const DynamicReactLiveDemo = dynamic(
|
||||
() => import("./react-live-demo").then((m) => m.ReactLiveDemo),
|
||||
@@ -22,47 +25,52 @@ const DynamicSandpack = dynamic(() => import("../../../sandpack").then((m) => m.
|
||||
loading: () => <Skeleton className="w-full h-32 rounded-xl" />,
|
||||
});
|
||||
|
||||
interface CodeDemoProps extends UseCodeDemoProps {
|
||||
interface CodeDemoProps extends UseCodeDemoProps, WindowResizerProps {
|
||||
title?: string;
|
||||
component?: string;
|
||||
asIframe?: boolean;
|
||||
showSandpackPreview?: boolean;
|
||||
initialEditorOpen?: boolean;
|
||||
enableResize?: boolean;
|
||||
showTabs?: boolean;
|
||||
showPreview?: boolean;
|
||||
showOpenInCodeSandbox?: boolean;
|
||||
isPreviewCentered?: boolean;
|
||||
resizeEnabled?: boolean;
|
||||
displayMode?: "always" | "visible";
|
||||
isGradientBox?: boolean;
|
||||
gradientColor?: GradientBoxProps["color"];
|
||||
defaultExpanded?: boolean;
|
||||
showWindowActions?: boolean;
|
||||
iframeSrc?: string;
|
||||
asIframe?: boolean;
|
||||
iframeInitialWidth?: number;
|
||||
previewHeight?: string | number;
|
||||
overflow?: "auto" | "visible" | "hidden";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CodeDemo: React.FC<CodeDemoProps> = ({
|
||||
files = {},
|
||||
component,
|
||||
title,
|
||||
showEditor = true,
|
||||
showPreview = true,
|
||||
asIframe = false,
|
||||
resizeEnabled = true,
|
||||
showSandpackPreview = false,
|
||||
isPreviewCentered = false,
|
||||
showOpenInCodeSandbox,
|
||||
showWindowActions = false,
|
||||
enableResize = false,
|
||||
isGradientBox = false,
|
||||
defaultExpanded = false,
|
||||
previewHeight = "auto",
|
||||
overflow = "visible",
|
||||
displayMode = "always",
|
||||
showTabs = true,
|
||||
gradientColor,
|
||||
highlightedLines,
|
||||
iframeInitialWidth,
|
||||
iframeSrc,
|
||||
className,
|
||||
}) => {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, {
|
||||
margin: "100px",
|
||||
once: true,
|
||||
margin: "600px",
|
||||
});
|
||||
|
||||
const {noInline, code} = useCodeDemo({
|
||||
@@ -72,44 +80,52 @@ export const CodeDemo: React.FC<CodeDemoProps> = ({
|
||||
const renderContent = useCallback(
|
||||
(content: React.ReactNode) => {
|
||||
if (displayMode === "always") return content;
|
||||
|
||||
if (displayMode === "visible") {
|
||||
return (
|
||||
<motion.div
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
initial={false}
|
||||
variants={{
|
||||
visible: {opacity: 1},
|
||||
hidden: {opacity: 0},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</motion.div>
|
||||
);
|
||||
if (!isInView) {
|
||||
return <div style={{height: previewHeight}} />;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
},
|
||||
[displayMode, isInView],
|
||||
[displayMode, previewHeight, isInView],
|
||||
);
|
||||
|
||||
const previewContent = useMemo(() => {
|
||||
if (!showPreview) return null;
|
||||
|
||||
const content = (
|
||||
<DynamicReactLiveDemo
|
||||
code={code}
|
||||
enableResize={enableResize || asIframe}
|
||||
height={previewHeight}
|
||||
const content = asIframe ? (
|
||||
<WindowResizer
|
||||
iframeHeight={previewHeight}
|
||||
iframeInitialWidth={iframeInitialWidth}
|
||||
iframeSrc={iframeSrc}
|
||||
iframeTitle={component}
|
||||
iframeTitle={title}
|
||||
resizeEnabled={resizeEnabled}
|
||||
/>
|
||||
) : (
|
||||
<DynamicReactLiveDemo
|
||||
className={className}
|
||||
code={code}
|
||||
gradientColor={gradientColor}
|
||||
height={previewHeight}
|
||||
isCentered={isPreviewCentered}
|
||||
isGradientBox={isGradientBox}
|
||||
noInline={noInline}
|
||||
overflow={overflow}
|
||||
showWindowActions={showWindowActions || asIframe}
|
||||
/>
|
||||
);
|
||||
|
||||
return renderContent(content);
|
||||
}, [displayMode, showPreview, isInView]);
|
||||
}, [
|
||||
displayMode,
|
||||
isGradientBox,
|
||||
gradientColor,
|
||||
previewHeight,
|
||||
asIframe,
|
||||
showPreview,
|
||||
isInView,
|
||||
className,
|
||||
]);
|
||||
|
||||
const editorContent = useMemo(() => {
|
||||
if (!showEditor) return null;
|
||||
@@ -138,9 +154,17 @@ export const CodeDemo: React.FC<CodeDemoProps> = ({
|
||||
showOpenInCodeSandbox,
|
||||
]);
|
||||
|
||||
const shouldRenderTabs = useMemo(() => {
|
||||
if (!showTabs) return false;
|
||||
if (!showPreview) return false;
|
||||
if (!showEditor) return false;
|
||||
|
||||
return true;
|
||||
}, [showTabs, showPreview, showEditor]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="flex flex-col gap-2">
|
||||
{showTabs && showPreview ? (
|
||||
{shouldRenderTabs ? (
|
||||
<Tabs
|
||||
disableAnimation
|
||||
aria-label="Code demo tabs"
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React from "react";
|
||||
import {LivePreview, LiveProvider, LiveError} from "react-live";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import * as Components from "@nextui-org/react";
|
||||
|
||||
import {BgGridContainer} from "@/components/bg-grid-container";
|
||||
import {GradientBox, GradientBoxProps} from "@/components/gradient-box";
|
||||
|
||||
export interface ReactLiveDemoProps {
|
||||
code: string;
|
||||
height?: string | number;
|
||||
noInline?: boolean;
|
||||
showWindowActions?: boolean;
|
||||
iframeSrc?: string;
|
||||
iframeTitle?: string;
|
||||
iframeInitialWidth?: number;
|
||||
enableResize?: boolean;
|
||||
height?: string | number;
|
||||
isCentered?: boolean;
|
||||
isGradientBox?: boolean;
|
||||
className?: string;
|
||||
gradientColor?: GradientBoxProps["color"];
|
||||
overflow?: "auto" | "visible" | "hidden";
|
||||
}
|
||||
|
||||
@@ -20,13 +21,46 @@ export const scope = {
|
||||
...Components,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
export const ReactLiveDemo: React.FC<ReactLiveDemoProps> = ({code, noInline}) => {
|
||||
export const ReactLiveDemo: React.FC<ReactLiveDemoProps> = ({
|
||||
code,
|
||||
isGradientBox,
|
||||
gradientColor = "orange",
|
||||
isCentered = false,
|
||||
height,
|
||||
className,
|
||||
noInline,
|
||||
}) => {
|
||||
const content = (
|
||||
<>
|
||||
<LivePreview
|
||||
className={clsx("live-preview flex h-full w-full not-prose", {
|
||||
"justify-center items-center": isCentered,
|
||||
})}
|
||||
style={{height}}
|
||||
/>
|
||||
<LiveError />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<LiveProvider code={code} noInline={noInline} scope={scope}>
|
||||
<BgGridContainer>
|
||||
<LivePreview className="live-preview flex h-full w-full not-prose" />
|
||||
<LiveError />
|
||||
</BgGridContainer>
|
||||
{isGradientBox ? (
|
||||
<GradientBox
|
||||
isCentered
|
||||
className={clsx(
|
||||
className,
|
||||
"relative overflow-y-hidden flex items-center border border-default-200 dark:border-default-100 px-2 py-4 rounded-lg overflow-hidden",
|
||||
)}
|
||||
color={gradientColor}
|
||||
to="top-right"
|
||||
>
|
||||
<div className="max-w-full py-4 px-2 w-full h-full scrollbar-hide overflow-x-scroll">
|
||||
{content}
|
||||
</div>
|
||||
</GradientBox>
|
||||
) : (
|
||||
<BgGridContainer className={className}>{content}</BgGridContainer>
|
||||
)}
|
||||
</LiveProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import React from "react";
|
||||
import {motion, useMotionValue, useTransform} from "framer-motion";
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
import {useIsMobile} from "@/hooks/use-media-query";
|
||||
import {useIsomorphicLayoutEffect} from "@/hooks/use-isomorphic-layout-effect";
|
||||
import {WindowActions} from "@/components/code-window/window-actions";
|
||||
|
||||
const resizer = tv({
|
||||
slots: {
|
||||
base: "flex items-center justify-end absolute right-[5px] z-10 w-auto xs:hidden",
|
||||
main: "relative w-full",
|
||||
barWrapper:
|
||||
"cursor-ew-resize select-none absolute d-flex justify-center flex items-center w-[10px] h-auto active:opacity-80",
|
||||
barInner: "relative z-10",
|
||||
bar: "w-[6px] h-[40px] rounded-full bg-default-400",
|
||||
iframeWrapper:
|
||||
"relative z-10 w-full h-full border border-default-200 dark:border-default-100 rounded-lg overflow-hidden",
|
||||
iframe: "w-full h-[calc(100%_-_2rem)] border-none z-10 overflow-scroll",
|
||||
},
|
||||
variants: {
|
||||
hasInitialWidth: {
|
||||
true: {
|
||||
base: "justify-start",
|
||||
},
|
||||
},
|
||||
isMobile: {
|
||||
true: {
|
||||
barInner: "hidden",
|
||||
},
|
||||
},
|
||||
enablePointerEvents: {
|
||||
true: {
|
||||
iframe: "pointer-events-auto",
|
||||
iframeWrapper: "pointer-events-auto",
|
||||
},
|
||||
false: {
|
||||
iframe: "pointer-events-none select-none",
|
||||
iframeWrapper: "pointer-events-none select-none",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
hasInitialWidth: false,
|
||||
isMobile: false,
|
||||
enablePointerEvents: true,
|
||||
},
|
||||
});
|
||||
|
||||
export interface WindowResizerProps {
|
||||
resizeEnabled?: boolean;
|
||||
iframeHeight?: string | number;
|
||||
iframeMinWidth?: number;
|
||||
iframeSrc?: string;
|
||||
iframeInitialWidth?: number;
|
||||
iframeTitle?: string;
|
||||
}
|
||||
|
||||
const MIN_WIDTH = 320;
|
||||
|
||||
const WindowResizer: React.FC<WindowResizerProps> = (props) => {
|
||||
let constraintsResizerRef = React.useRef<HTMLDivElement>(null);
|
||||
let resizerRef = React.useRef<HTMLDivElement>(null);
|
||||
let iframeRef = React.useRef<HTMLIFrameElement>(null);
|
||||
const [enablePointerEvents, setEnablePointerEvents] = React.useState(true);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const {
|
||||
iframeSrc,
|
||||
iframeTitle,
|
||||
resizeEnabled,
|
||||
iframeHeight: height = "420px",
|
||||
iframeInitialWidth,
|
||||
iframeMinWidth: minWidth = MIN_WIDTH,
|
||||
} = props;
|
||||
const hasInitialWidth = iframeInitialWidth !== undefined;
|
||||
|
||||
const {main, base, barInner, barWrapper, bar, iframe, iframeWrapper} = resizer({
|
||||
hasInitialWidth,
|
||||
isMobile,
|
||||
enablePointerEvents,
|
||||
});
|
||||
|
||||
const resizerX = useMotionValue(0);
|
||||
const browserWidth = useTransform(resizerX, (x) =>
|
||||
hasInitialWidth ? iframeInitialWidth + x + 14 : `calc(100% + ${x}px - 14px)`,
|
||||
);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
let observer = new window.ResizeObserver(() => {
|
||||
if (constraintsResizerRef.current && resizerRef.current) {
|
||||
let width = constraintsResizerRef.current.offsetWidth - resizerRef.current.offsetWidth;
|
||||
|
||||
if (resizerX.get() > width) {
|
||||
resizerX.set(width);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
constraintsResizerRef.current && observer.observe(constraintsResizerRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!resizerRef.current) {
|
||||
return;
|
||||
}
|
||||
resizerRef.current.onselectstart = () => false;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={main()} style={{height}}>
|
||||
<motion.div
|
||||
className={iframeWrapper()}
|
||||
style={{
|
||||
width: isMobile ? "100%" : browserWidth,
|
||||
}}
|
||||
>
|
||||
<WindowActions className="bg-default-100 dark:bg-default-50" />
|
||||
<motion.iframe ref={iframeRef} className={iframe()} src={iframeSrc} title={iframeTitle} />
|
||||
</motion.div>
|
||||
{resizeEnabled && (
|
||||
<div
|
||||
ref={constraintsResizerRef}
|
||||
className={base({
|
||||
className: "z-1 top-0 bottom-0 right-0 xs:w-mw-xs",
|
||||
})}
|
||||
style={{
|
||||
width: `calc(100% - ${hasInitialWidth ? iframeInitialWidth : minWidth}px - 20px)`,
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
ref={resizerRef}
|
||||
_dragX={resizerX}
|
||||
className={barWrapper()}
|
||||
drag="x"
|
||||
dragConstraints={constraintsResizerRef}
|
||||
dragElastic={0}
|
||||
dragMomentum={false}
|
||||
style={{x: resizerX}}
|
||||
onDragEnd={() => {
|
||||
document.documentElement.classList.remove("dragging-ew");
|
||||
iframeRef.current?.classList.remove("dragging-ew");
|
||||
setEnablePointerEvents(true);
|
||||
}}
|
||||
onDragStart={() => {
|
||||
document.documentElement.classList.add("dragging-ew");
|
||||
iframeRef.current?.classList.add("dragging-ew");
|
||||
setEnablePointerEvents(false);
|
||||
}}
|
||||
>
|
||||
<div className={barInner()}>
|
||||
<div className={bar()} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WindowResizer;
|
||||
@@ -87,15 +87,14 @@ const Codeblock = forwardRef<HTMLPreElement, CodeblockProps>(
|
||||
{...lineProps}
|
||||
className={clsx(
|
||||
lineProps.className,
|
||||
language === "bash" || removeIndent ? "pr-4" : "px-4",
|
||||
removeIndent ? "pr-4" : "px-4",
|
||||
"relative [&>span]:relative [&>span]:z-10",
|
||||
{
|
||||
"px-2": showLines,
|
||||
},
|
||||
{
|
||||
"before:content-[''] before:w-full before:h-full before:absolute before:z-0 before:left-0 before:bg-gradient-to-r before:from-white/10 before:to-code-background": shouldHighlightLine(
|
||||
i,
|
||||
),
|
||||
"before:content-[''] before:w-full before:h-full before:absolute before:z-0 before:left-0 before:bg-gradient-to-r before:from-white/10 before:to-code-background":
|
||||
shouldHighlightLine(i),
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,19 +1,56 @@
|
||||
import {Button, Link} from "@nextui-org/react";
|
||||
import {Button, ButtonProps, Code, Link, Tooltip} from "@nextui-org/react";
|
||||
import {ReactNode} from "react";
|
||||
import Balancer from "react-wrap-balancer";
|
||||
|
||||
import {GithubIcon, NpmIcon, AdobeIcon, StorybookIcon} from "@/components/icons";
|
||||
import {GithubIcon, NpmIcon, AdobeIcon, StorybookIcon, NextJsIcon} from "@/components/icons";
|
||||
import {COMPONENT_PATH, COMPONENT_THEME_PATH} from "@/libs/github/constants";
|
||||
|
||||
export interface ComponentLinksProps {
|
||||
component: string;
|
||||
styles?: string;
|
||||
storybook?: string;
|
||||
rscCompatible?: boolean;
|
||||
reactAriaHook?: string;
|
||||
}
|
||||
|
||||
const ButtonLink = ({
|
||||
children,
|
||||
href,
|
||||
startContent,
|
||||
tooltip,
|
||||
...props
|
||||
}: ButtonProps & {
|
||||
href: string;
|
||||
tooltip?: string | ReactNode;
|
||||
}) => {
|
||||
const button = (
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="!text-small py-4 bg-default-100 dark:bg-default-50 text-default-700"
|
||||
href={href}
|
||||
size="sm"
|
||||
startContent={startContent}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<Tooltip className="max-w-[230px]" content={tooltip}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
};
|
||||
|
||||
export const ComponentLinks = ({
|
||||
component,
|
||||
storybook,
|
||||
styles,
|
||||
rscCompatible,
|
||||
reactAriaHook,
|
||||
}: ComponentLinksProps) => {
|
||||
if (!component) {
|
||||
@@ -22,68 +59,55 @@ export const ComponentLinks = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3 mt-6">
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-default-700"
|
||||
href={`https://storiesv2.nextui.org/?path=/story/components-${storybook || component}`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
<ButtonLink
|
||||
href={`https://storybook.nextui.org/?path=/story/components-${storybook || component}`}
|
||||
startContent={<StorybookIcon className="text-lg text-[#ff4785]" />}
|
||||
variant="flat"
|
||||
>
|
||||
Storybook
|
||||
</Button>
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-default-700"
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
href={`https://www.npmjs.com/package/@nextui-org/${component}`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
startContent={<NpmIcon className="text-2xl text-[#E53E3E]" />}
|
||||
variant="flat"
|
||||
>
|
||||
{`@nextui-org/${component}`}
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
{reactAriaHook && (
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-default-700"
|
||||
<ButtonLink
|
||||
href={`https://react-spectrum.adobe.com/react-aria/${reactAriaHook}.html`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
startContent={<AdobeIcon className="text-lg text-[#E1251B]" />}
|
||||
variant="flat"
|
||||
>
|
||||
React Aria
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
)}
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-default-700"
|
||||
href={`${COMPONENT_PATH}/${component}`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
startContent={<GithubIcon />}
|
||||
variant="flat"
|
||||
>
|
||||
{rscCompatible && (
|
||||
<ButtonLink
|
||||
href="https://nextjs.org/docs/getting-started/react-essentials#server-components"
|
||||
startContent={<NextJsIcon size={18} />}
|
||||
tooltip={
|
||||
<p>
|
||||
<Balancer>
|
||||
This component doesn't use the
|
||||
<Code className="font-normal bg-transparent px-0 py-0 text-code-mdx">
|
||||
`use client;`
|
||||
</Code>
|
||||
directive making it compatible with RSC.
|
||||
</Balancer>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
Server component
|
||||
</ButtonLink>
|
||||
)}
|
||||
|
||||
<ButtonLink href={`${COMPONENT_PATH}/${component}`} startContent={<GithubIcon size={20} />}>
|
||||
Source
|
||||
</Button>
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-default-700"
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
href={`${COMPONENT_THEME_PATH}/${styles || component}.ts`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
startContent={<GithubIcon />}
|
||||
variant="flat"
|
||||
startContent={<GithubIcon size={20} />}
|
||||
>
|
||||
Styles source
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,4 +5,5 @@ export * from "./community";
|
||||
export * from "./frameworks";
|
||||
export * from "./code-demo";
|
||||
export * from "./import-tabs";
|
||||
export * from "./swatch-colors-set";
|
||||
export {default as Codeblock} from "./codeblock";
|
||||
|
||||
@@ -60,10 +60,10 @@ export const PackageManagers = ({commands}: PackageManagersProps) => {
|
||||
classNames={{
|
||||
base: "bg-code-background text-code-foreground",
|
||||
pre: "font-light text-base",
|
||||
copyButton: "text-lg text-code-foreground/50",
|
||||
copyButton: "text-lg text-zinc-500 mr-2",
|
||||
}}
|
||||
>
|
||||
<Codeblock codeString={commands[name] as string} language="bash" />
|
||||
<Codeblock removeIndent codeString={commands[name] as string} language="bash" />
|
||||
</Snippet>
|
||||
</Tab>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,641 @@
|
||||
import {parseToRgba, readableColor} from "color2k";
|
||||
import {Button, Tooltip} from "@nextui-org/react";
|
||||
import {commonColors, semanticColors} from "@nextui-org/theme";
|
||||
import {useClipboard} from "@nextui-org/use-clipboard";
|
||||
import {useState} from "react";
|
||||
import {useTheme} from "next-themes";
|
||||
import {get, isEmpty} from "lodash";
|
||||
|
||||
type ColorsItem = {
|
||||
color: string;
|
||||
scale?: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
};
|
||||
|
||||
type SwatchColors = {
|
||||
title: string;
|
||||
items: ColorsItem[];
|
||||
};
|
||||
|
||||
type SwatchSetProps = {
|
||||
colors: SwatchColors[];
|
||||
isSematic?: boolean;
|
||||
};
|
||||
|
||||
const scaleIndexMap: Record<number, string> = {
|
||||
0: "50",
|
||||
1: "100",
|
||||
2: "200",
|
||||
3: "300",
|
||||
4: "400",
|
||||
5: "500",
|
||||
6: "600",
|
||||
7: "700",
|
||||
8: "800",
|
||||
9: "900",
|
||||
};
|
||||
|
||||
const Swatch = ({color, scale}: {color: string; scale?: string}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const {copy} = useClipboard();
|
||||
|
||||
const colorText = color
|
||||
? `#${parseToRgba(color)
|
||||
.slice(0, 3)
|
||||
.map((x) => x.toString(16).padStart(2, "0"))
|
||||
.join("")
|
||||
.toUpperCase()}`
|
||||
: "N/A";
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(colorText);
|
||||
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip content="Copied" isOpen={copied}>
|
||||
<Button
|
||||
className="flex flex-col gap-1 items-center justify-center w-24 h-24 m-2 rounded-medium shadow-small"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
onPress={handleCopy}
|
||||
>
|
||||
<span
|
||||
className="text-small font-semibold"
|
||||
style={{
|
||||
color: readableColor(color),
|
||||
}}
|
||||
>
|
||||
{colorText}
|
||||
</span>
|
||||
{scale && (
|
||||
<span
|
||||
className="text-tiny font-semibold"
|
||||
style={{
|
||||
color: readableColor(color),
|
||||
}}
|
||||
>
|
||||
{scale}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const SematicSwatch = ({
|
||||
color,
|
||||
className,
|
||||
textClassName,
|
||||
}: {
|
||||
color: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const {copy} = useClipboard();
|
||||
|
||||
const {theme} = useTheme();
|
||||
let value: string = "";
|
||||
const [colorName, colorScale] = color.split("-");
|
||||
|
||||
let currentPalette = get(semanticColors, theme ?? "", {});
|
||||
|
||||
if (!colorScale) {
|
||||
value = get(currentPalette, `${colorName}.DEFAULT`, "");
|
||||
} else {
|
||||
value = get(currentPalette, `${colorName}.${colorScale}`, "");
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(value);
|
||||
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip content="Copied" isOpen={copied}>
|
||||
<Button
|
||||
className={`${className} gap-1 flex flex-col items-center justify-center w-24 h-24 m-2 rounded-medium shadow-small`}
|
||||
onPress={handleCopy}
|
||||
>
|
||||
<span className={`${textClassName} text-tiny font-semibold`}>{color}</span>
|
||||
{!isEmpty(value) && (
|
||||
<span className={`${textClassName} text-tiny font-semibold`}>{value}</span>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const SwatchSet = ({colors, isSematic = false}: SwatchSetProps) => (
|
||||
<div className="flex flex-row flex-wrap items-center justify-center">
|
||||
{colors.map(({title, items}) => (
|
||||
<div key={title} className="flex flex-col items-start w-full h-full">
|
||||
<h2 className="text-xl font-bold text-foreground">{title}</h2>
|
||||
<div className="flex flex-row flex-wrap items-center justify-start w-full h-full px-4 py-1">
|
||||
{items.map((c, index) =>
|
||||
isSematic ? (
|
||||
<SematicSwatch
|
||||
key={`${c.color}-${index}`}
|
||||
className={c.className}
|
||||
color={c.color}
|
||||
textClassName={c.textClassName}
|
||||
/>
|
||||
) : (
|
||||
<Swatch key={`${c.color}-${index}`} color={c.color} scale={c.scale} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const getCommonItems = (colors: string[]) => {
|
||||
return colors.map((color, index, array) => ({
|
||||
color,
|
||||
scale: array.length > 2 ? scaleIndexMap[index] : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
export const CommonColors = () => {
|
||||
return (
|
||||
<SwatchSet
|
||||
colors={[
|
||||
{
|
||||
title: "White & Black",
|
||||
items: getCommonItems([commonColors.white, commonColors.black]),
|
||||
},
|
||||
{
|
||||
title: "Blue",
|
||||
items: getCommonItems([...Object.values(commonColors.blue)]),
|
||||
},
|
||||
{
|
||||
title: "Purple",
|
||||
items: getCommonItems([...Object.values(commonColors.purple)]),
|
||||
},
|
||||
{
|
||||
title: "Green",
|
||||
items: getCommonItems([...Object.values(commonColors.green)]),
|
||||
},
|
||||
{
|
||||
title: "Red",
|
||||
items: getCommonItems([...Object.values(commonColors.red)]),
|
||||
},
|
||||
{
|
||||
title: "Pink",
|
||||
items: getCommonItems([...Object.values(commonColors.pink)]),
|
||||
},
|
||||
{
|
||||
title: "Yellow",
|
||||
items: getCommonItems([...Object.values(commonColors.yellow)]),
|
||||
},
|
||||
{
|
||||
title: "Cyan",
|
||||
items: getCommonItems([...Object.values(commonColors.cyan)]),
|
||||
},
|
||||
{
|
||||
title: "Zinc",
|
||||
items: getCommonItems([...Object.values(commonColors.zinc)]),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SemanticColors = () => {
|
||||
return (
|
||||
<SwatchSet
|
||||
isSematic
|
||||
colors={[
|
||||
{
|
||||
title: "Layout",
|
||||
items: [
|
||||
{
|
||||
color: "background",
|
||||
className: "bg-background",
|
||||
textClassName: "text-foreground",
|
||||
},
|
||||
{
|
||||
color: "foreground",
|
||||
className: "bg-foreground",
|
||||
textClassName: "text-background",
|
||||
},
|
||||
{
|
||||
color: "divider",
|
||||
className: "bg-divider",
|
||||
textClassName: "text-foreground",
|
||||
},
|
||||
{
|
||||
color: "focus",
|
||||
className: "bg-focus",
|
||||
textClassName: "text-primary-foreground",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Content",
|
||||
items: [
|
||||
{
|
||||
color: "content1",
|
||||
className: "bg-content1",
|
||||
textClassName: "text-content1-foreground",
|
||||
},
|
||||
{
|
||||
color: "content2",
|
||||
className: "bg-content2",
|
||||
textClassName: "text-content2-foreground",
|
||||
},
|
||||
{
|
||||
color: "content3",
|
||||
className: "bg-content3",
|
||||
textClassName: "text-content3-foreground",
|
||||
},
|
||||
{
|
||||
color: "content4",
|
||||
className: "bg-content4",
|
||||
textClassName: "text-content4-foreground",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Base",
|
||||
items: [
|
||||
{
|
||||
color: "default",
|
||||
className: "bg-default",
|
||||
textClassName: "text-default-foreground",
|
||||
},
|
||||
{
|
||||
color: "primary",
|
||||
className: "bg-primary",
|
||||
textClassName: "text-primary-foreground",
|
||||
},
|
||||
{
|
||||
color: "secondary",
|
||||
className: "bg-secondary",
|
||||
textClassName: "text-secondary-foreground",
|
||||
},
|
||||
{
|
||||
color: "success",
|
||||
className: "bg-success",
|
||||
textClassName: "text-success-foreground",
|
||||
},
|
||||
{
|
||||
color: "warning",
|
||||
className: "bg-warning",
|
||||
textClassName: "text-warning-foreground",
|
||||
},
|
||||
{
|
||||
color: "danger",
|
||||
className: "bg-danger",
|
||||
textClassName: "text-danger-foreground",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Default",
|
||||
items: [
|
||||
{
|
||||
color: "default-50",
|
||||
className: "bg-default-50",
|
||||
textClassName: "text-default-900",
|
||||
},
|
||||
{
|
||||
color: "default-100",
|
||||
className: "bg-default-100",
|
||||
textClassName: "text-default-900",
|
||||
},
|
||||
{
|
||||
color: "default-200",
|
||||
className: "bg-default-200",
|
||||
textClassName: "text-default-800",
|
||||
},
|
||||
{
|
||||
color: "default-300",
|
||||
className: "bg-default-300",
|
||||
textClassName: "text-default-800",
|
||||
},
|
||||
{
|
||||
color: "default-400",
|
||||
className: "bg-default-400",
|
||||
textClassName: "text-default-800",
|
||||
},
|
||||
{
|
||||
color: "default-500",
|
||||
className: "bg-default-500",
|
||||
textClassName: "text-default-foreground",
|
||||
},
|
||||
{
|
||||
color: "default-600",
|
||||
className: "bg-default-600",
|
||||
textClassName: "text-default-50",
|
||||
},
|
||||
{
|
||||
color: "default-700",
|
||||
className: "bg-default-700",
|
||||
textClassName: "text-default-100",
|
||||
},
|
||||
{
|
||||
color: "default-800",
|
||||
className: "bg-default-800",
|
||||
textClassName: "text-default-100",
|
||||
},
|
||||
{
|
||||
color: "default-900",
|
||||
className: "bg-default-900",
|
||||
textClassName: "text-default-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Primary",
|
||||
items: [
|
||||
{
|
||||
color: "primary-50",
|
||||
className: "bg-primary-50",
|
||||
textClassName: "text-primary-900",
|
||||
},
|
||||
{
|
||||
color: "primary-100",
|
||||
className: "bg-primary-100",
|
||||
textClassName: "text-primary-900",
|
||||
},
|
||||
{
|
||||
color: "primary-200",
|
||||
className: "bg-primary-200",
|
||||
textClassName: "text-primary-800",
|
||||
},
|
||||
{
|
||||
color: "primary-300",
|
||||
className: "bg-primary-300",
|
||||
textClassName: "text-primary-800",
|
||||
},
|
||||
{
|
||||
color: "primary-400",
|
||||
className: "bg-primary-400",
|
||||
textClassName: "text-primary-800",
|
||||
},
|
||||
{
|
||||
color: "primary-500",
|
||||
className: "bg-primary-500",
|
||||
textClassName: "text-primary-foreground",
|
||||
},
|
||||
{
|
||||
color: "primary-600",
|
||||
className: "bg-primary-600",
|
||||
textClassName: "text-primary-50",
|
||||
},
|
||||
{
|
||||
color: "primary-700",
|
||||
className: "bg-primary-700",
|
||||
textClassName: "text-primary-100",
|
||||
},
|
||||
{
|
||||
color: "primary-800",
|
||||
className: "bg-primary-800",
|
||||
textClassName: "text-primary-100",
|
||||
},
|
||||
{
|
||||
color: "primary-900",
|
||||
className: "bg-primary-900",
|
||||
textClassName: "text-primary-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Secondary",
|
||||
items: [
|
||||
{
|
||||
color: "secondary-50",
|
||||
className: "bg-secondary-50",
|
||||
textClassName: "text-secondary-900",
|
||||
},
|
||||
{
|
||||
color: "secondary-100",
|
||||
className: "bg-secondary-100",
|
||||
textClassName: "text-secondary-900",
|
||||
},
|
||||
{
|
||||
color: "secondary-200",
|
||||
className: "bg-secondary-200",
|
||||
textClassName: "text-secondary-800",
|
||||
},
|
||||
{
|
||||
color: "secondary-300",
|
||||
className: "bg-secondary-300",
|
||||
textClassName: "text-secondary-800",
|
||||
},
|
||||
{
|
||||
color: "secondary-400",
|
||||
className: "bg-secondary-400",
|
||||
textClassName: "text-secondary-800",
|
||||
},
|
||||
{
|
||||
color: "secondary-500",
|
||||
className: "bg-secondary-500",
|
||||
textClassName: "text-secondary-foreground",
|
||||
},
|
||||
{
|
||||
color: "secondary-600",
|
||||
className: "bg-secondary-600",
|
||||
textClassName: "text-secondary-50",
|
||||
},
|
||||
{
|
||||
color: "secondary-700",
|
||||
className: "bg-secondary-700",
|
||||
textClassName: "text-secondary-100",
|
||||
},
|
||||
{
|
||||
color: "secondary-800",
|
||||
className: "bg-secondary-800",
|
||||
textClassName: "text-secondary-100",
|
||||
},
|
||||
{
|
||||
color: "secondary-900",
|
||||
className: "bg-secondary-900",
|
||||
textClassName: "text-secondary-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Success",
|
||||
items: [
|
||||
{
|
||||
color: "success-50",
|
||||
className: "bg-success-50",
|
||||
textClassName: "text-success-900",
|
||||
},
|
||||
{
|
||||
color: "success-100",
|
||||
className: "bg-success-100",
|
||||
textClassName: "text-success-900",
|
||||
},
|
||||
{
|
||||
color: "success-200",
|
||||
className: "bg-success-200",
|
||||
textClassName: "text-success-800",
|
||||
},
|
||||
{
|
||||
color: "success-300",
|
||||
className: "bg-success-300",
|
||||
textClassName: "text-success-800",
|
||||
},
|
||||
{
|
||||
color: "success-400",
|
||||
className: "bg-success-400",
|
||||
textClassName: "text-success-800",
|
||||
},
|
||||
{
|
||||
color: "success-500",
|
||||
className: "bg-success-500",
|
||||
textClassName: "text-success-foreground",
|
||||
},
|
||||
{
|
||||
color: "success-600",
|
||||
className: "bg-success-600",
|
||||
textClassName: "text-success-50",
|
||||
},
|
||||
{
|
||||
color: "success-700",
|
||||
className: "bg-success-700",
|
||||
textClassName: "text-success-100",
|
||||
},
|
||||
{
|
||||
color: "success-800",
|
||||
className: "bg-success-800",
|
||||
textClassName: "text-success-100",
|
||||
},
|
||||
{
|
||||
color: "success-900",
|
||||
className: "bg-success-900",
|
||||
textClassName: "text-success-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Warning",
|
||||
items: [
|
||||
{
|
||||
color: "warning-50",
|
||||
className: "bg-warning-50",
|
||||
textClassName: "text-warning-900",
|
||||
},
|
||||
{
|
||||
color: "warning-100",
|
||||
className: "bg-warning-100",
|
||||
textClassName: "text-warning-900",
|
||||
},
|
||||
{
|
||||
color: "warning-200",
|
||||
className: "bg-warning-200",
|
||||
textClassName: "text-warning-800",
|
||||
},
|
||||
{
|
||||
color: "warning-300",
|
||||
className: "bg-warning-300",
|
||||
textClassName: "text-warning-800",
|
||||
},
|
||||
{
|
||||
color: "warning-400",
|
||||
className: "bg-warning-400",
|
||||
textClassName: "text-warning-800",
|
||||
},
|
||||
{
|
||||
color: "warning-500",
|
||||
className: "bg-warning-500",
|
||||
textClassName: "text-warning-foreground",
|
||||
},
|
||||
{
|
||||
color: "warning-600",
|
||||
className: "bg-warning-600",
|
||||
textClassName: "text-warning-50",
|
||||
},
|
||||
{
|
||||
color: "warning-700",
|
||||
className: "bg-warning-700",
|
||||
textClassName: "text-warning-100",
|
||||
},
|
||||
{
|
||||
color: "warning-800",
|
||||
className: "bg-warning-800",
|
||||
textClassName: "text-warning-100",
|
||||
},
|
||||
{
|
||||
color: "warning-900",
|
||||
className: "bg-warning-900",
|
||||
textClassName: "text-warning-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Danger",
|
||||
items: [
|
||||
{
|
||||
color: "danger-50",
|
||||
className: "bg-danger-50",
|
||||
textClassName: "text-danger-900",
|
||||
},
|
||||
{
|
||||
color: "danger-100",
|
||||
className: "bg-danger-100",
|
||||
textClassName: "text-danger-900",
|
||||
},
|
||||
{
|
||||
color: "danger-200",
|
||||
className: "bg-danger-200",
|
||||
textClassName: "text-danger-800",
|
||||
},
|
||||
{
|
||||
color: "danger-300",
|
||||
className: "bg-danger-300",
|
||||
textClassName: "text-danger-800",
|
||||
},
|
||||
{
|
||||
color: "danger-400",
|
||||
className: "bg-danger-400",
|
||||
textClassName: "text-danger-800",
|
||||
},
|
||||
{
|
||||
color: "danger-500",
|
||||
className: "bg-danger-500",
|
||||
textClassName: "text-danger-foreground",
|
||||
},
|
||||
{
|
||||
color: "danger-600",
|
||||
className: "bg-danger-600",
|
||||
textClassName: "text-danger-50",
|
||||
},
|
||||
{
|
||||
color: "danger-700",
|
||||
className: "bg-danger-700",
|
||||
textClassName: "text-danger-100",
|
||||
},
|
||||
{
|
||||
color: "danger-800",
|
||||
className: "bg-danger-800",
|
||||
textClassName: "text-danger-100",
|
||||
},
|
||||
{
|
||||
color: "danger-900",
|
||||
className: "bg-danger-900",
|
||||
textClassName: "text-danger-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "./sidebar";
|
||||
export * from "./toc";
|
||||
export * from "./footer-nav";
|
||||
export * from "./pager";
|
||||
export * from "./components/code-demo";
|
||||
|
||||
@@ -5,17 +5,16 @@ import NextLink from "next/link";
|
||||
import {Link} from "@nextui-org/react";
|
||||
import {ChevronIcon} from "@nextui-org/shared-icons";
|
||||
|
||||
import manifest from "@/content/docs/manifest.json";
|
||||
import manifest from "@/config/routes.json";
|
||||
import {removeFromLast} from "@/utils";
|
||||
import {Route, addTagToSlug} from "@/libs/docs/page";
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {useDocsRoute} from "@/hooks/use-docs-route";
|
||||
|
||||
export interface FooterNavProps {
|
||||
tag?: string;
|
||||
currentRoute?: Route;
|
||||
}
|
||||
|
||||
export const FooterNav: React.FC<FooterNavProps> = ({currentRoute, tag}) => {
|
||||
export const DocsPager: React.FC<FooterNavProps> = ({currentRoute}) => {
|
||||
const {prevRoute, nextRoute} = useDocsRoute(manifest.routes, currentRoute);
|
||||
|
||||
return (
|
||||
@@ -26,7 +25,7 @@ export const FooterNav: React.FC<FooterNavProps> = ({currentRoute, tag}) => {
|
||||
as={NextLink}
|
||||
className="flex gap-2"
|
||||
color="foreground"
|
||||
href={addTagToSlug(removeFromLast(prevRoute.path || "", "."), tag)}
|
||||
href={removeFromLast(prevRoute.path || "", ".")}
|
||||
>
|
||||
<ChevronIcon className="text-primary" height={20} width={20} />
|
||||
{prevRoute.title}
|
||||
@@ -40,7 +39,7 @@ export const FooterNav: React.FC<FooterNavProps> = ({currentRoute, tag}) => {
|
||||
as={NextLink}
|
||||
className="flex gap-1 items-center"
|
||||
color="foreground"
|
||||
href={addTagToSlug(removeFromLast(nextRoute.path || "", "."), tag)}
|
||||
href={removeFromLast(nextRoute.path || "", ".")}
|
||||
>
|
||||
{nextRoute.title}
|
||||
<ChevronIcon className="rotate-180 text-primary" height={20} width={20} />
|
||||
@@ -10,10 +10,16 @@ import {TreeState, useTreeState} from "@react-stately/tree";
|
||||
import {useSelectableCollection} from "@react-aria/selection";
|
||||
import {usePress} from "@react-aria/interactions";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {SpacerProps, Spacer, Link as NextUILink, cn, Chip} from "@nextui-org/react";
|
||||
import {
|
||||
SpacerProps,
|
||||
Spacer,
|
||||
Link as NextUILink,
|
||||
Chip,
|
||||
dataFocusVisibleClasses,
|
||||
} from "@nextui-org/react";
|
||||
import Link from "next/link";
|
||||
import {isEmpty} from "lodash";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {usePathname, useRouter} from "next/navigation";
|
||||
|
||||
import {getRoutePaths} from "./utils";
|
||||
|
||||
@@ -52,6 +58,7 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
const {key, rendered, childNodes} = item;
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const paths = item.props.path
|
||||
? getRoutePaths(item.props.path, item.props?.tag)
|
||||
@@ -63,7 +70,11 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
const isNew = item.props?.newPost;
|
||||
|
||||
const isExpanded = state.expandedKeys.has(key);
|
||||
const isSelected = state.selectionManager.isSelected(key) || paths.pathname === item.props.slug;
|
||||
const isSelected =
|
||||
state.selectionManager.isSelected(key) ||
|
||||
paths.pathname === item.props.slug ||
|
||||
paths.pathname === pathname ||
|
||||
paths.pagePath === pathname;
|
||||
|
||||
const ref = useRef<any>(null);
|
||||
|
||||
@@ -92,12 +103,7 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
className={clsx(
|
||||
"flex flex-col gap-3 outline-none w-full tap-highlight-transparent",
|
||||
// focus ring
|
||||
"data-[focus-visible=true]:outline-none",
|
||||
"data-[focus-visible=true]:ring-2",
|
||||
"data-[focus-visible=true]:ring-primary",
|
||||
"data-[focus-visible=true]:ring-offset-2",
|
||||
"data-[focus-visible=true]:ring-offset-background",
|
||||
"data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
...dataFocusVisibleClasses,
|
||||
)}
|
||||
data-focus-visible={isFocusVisible}
|
||||
data-focused={isFocused}
|
||||
@@ -132,21 +138,24 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
href={paths.pathname}
|
||||
>
|
||||
<span
|
||||
className={clsx({
|
||||
"opacity-100": isSelected,
|
||||
"opacity-60": !isSelected,
|
||||
"pointer-events-none": item.props?.comingSoon,
|
||||
})}
|
||||
className={clsx(
|
||||
isSelected
|
||||
? "text-primary font-medium dark:text-foreground"
|
||||
: "opacity-80 dark:opacity-60",
|
||||
{
|
||||
"pointer-events-none": item.props?.comingSoon,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{rendered}
|
||||
</span>
|
||||
{isNew && (
|
||||
<Chip className="ml-2" color="primary" size="xs" variant="flat">
|
||||
<Chip className="ml-2" color="primary" size="sm" variant="flat">
|
||||
New
|
||||
</Chip>
|
||||
)}
|
||||
{item.props?.comingSoon && (
|
||||
<Chip className="ml-2" color="default" size="xs" variant="flat">
|
||||
<Chip className="ml-2" color="default" size="sm" variant="flat">
|
||||
Coming soon
|
||||
</Chip>
|
||||
)}
|
||||
@@ -229,14 +238,14 @@ export interface DocsSidebarProps {
|
||||
export const DocsSidebar: FC<DocsSidebarProps> = ({routes, slug, tag, className}) => {
|
||||
const expandedKeys = routes?.reduce((keys, route) => {
|
||||
if (route.defaultOpen) {
|
||||
keys.push(route.key);
|
||||
keys.push(route.key as string);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}, [] as string[]);
|
||||
|
||||
return (
|
||||
<div className={cn("lg:fixed lg:top-20 mt-2 z-0 lg:h-[calc(100vh-121px)]", className)}>
|
||||
<div className={clsx("lg:fixed lg:top-20 mt-2 z-0 lg:h-[calc(100vh-121px)]", className)}>
|
||||
<Tree defaultExpandedKeys={expandedKeys} items={routes || []}>
|
||||
{(route) => (
|
||||
<Item
|
||||
|
||||
@@ -71,8 +71,9 @@ export const DocsToc: FC<DocsTocProps> = ({headings}) => {
|
||||
className={clsx(
|
||||
"transition-colors",
|
||||
"font-normal",
|
||||
"flex items-center text-sm font-normal text-default-300",
|
||||
"flex items-center text-sm font-normal text-default-500 dark:text-default-300",
|
||||
"data-[active=true]:text-foreground",
|
||||
"dark:data-[active=true]:text-foreground",
|
||||
"before:content-['']",
|
||||
"before:opacity-0",
|
||||
"data-[active=true]:before:opacity-100",
|
||||
@@ -98,7 +99,7 @@ export const DocsToc: FC<DocsTocProps> = ({headings}) => {
|
||||
<Divider />
|
||||
<Spacer y={2} />
|
||||
<a
|
||||
className="flex gap-2 items-center text-sm text-foreground/30 hover:text-foreground/80 pl-4 transition-opacity"
|
||||
className="flex gap-2 items-center text-sm text-default-500 dark:text-foreground/30 hover:text-foreground/80 pl-4 transition-opacity"
|
||||
href={`#${firstId}`}
|
||||
>
|
||||
Back to top
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import {usePathname} from "next/navigation";
|
||||
|
||||
import {siteConfig} from "@/config/site";
|
||||
|
||||
@@ -8,6 +9,12 @@ const Link = dynamic(() => import("@nextui-org/react").then((mod) => mod.Link));
|
||||
const VercelCallout = dynamic(() => import("./vercel-callout").then((mod) => mod.VercelCallout));
|
||||
|
||||
export const Footer = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
if (pathname.includes("/examples")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<footer className="container mx-auto max-w-7xl pb-12 px-12">
|
||||
<div className="flex flex-col justify-center items-center gap-1">
|
||||
|
||||
@@ -13,3 +13,6 @@ export * from "./shuffle";
|
||||
export * from "./info";
|
||||
export * from "./document-code";
|
||||
export * from "./hash";
|
||||
export * from "./more-square";
|
||||
export * from "./play";
|
||||
export * from "./pause";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const MoreSquareBoldIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16.19 2H7.81C4.17 2 2 4.17 2 7.81V16.18C2 19.83 4.17 22 7.81 22H16.18C19.82 22 21.99 19.83 21.99 16.19V7.81C22 4.17 19.83 2 16.19 2ZM8 13C7.44 13 7 12.55 7 12C7 11.45 7.45 11 8 11C8.55 11 9 11.45 9 12C9 12.55 8.56 13 8 13ZM12 13C11.44 13 11 12.55 11 12C11 11.45 11.45 11 12 11C12.55 11 13 11.45 13 12C13 12.55 12.56 13 12 13ZM16 13C15.44 13 15 12.55 15 12C15 11.45 15.45 11 16 11C16.55 11 17 11.45 17 12C17 12.55 16.56 13 16 13Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const PauseBoldIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10.65 19.11V4.89C10.65 3.54 10.08 3 8.64 3H5.01C3.57 3 3 3.54 3 4.89V19.11C3 20.46 3.57 21 5.01 21H8.64C10.08 21 10.65 20.46 10.65 19.11Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M21 19.11V4.89C21 3.54 20.43 3 18.99 3H15.36C13.93 3 13.35 3.54 13.35 4.89V19.11C13.35 20.46 13.92 21 15.36 21H18.99C20.43 21 21 20.46 21 19.11Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const PlayBoldIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4 11.9999V8.43989C4 4.01989 7.13 2.2099 10.96 4.4199L14.05 6.1999L17.14 7.9799C20.97 10.1899 20.97 13.8099 17.14 16.0199L14.05 17.7999L10.96 19.5799C7.13 21.7899 4 19.9799 4 15.5599V11.9999Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -18,3 +18,5 @@ export * from "./rotate-right";
|
||||
export * from "./hash";
|
||||
export * from "./chevron-right";
|
||||
export * from "./search";
|
||||
export * from "./simple-grid";
|
||||
export * from "./rotate-left";
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const RotateLeftLinearIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9.11008 5.0799C9.98008 4.8199 10.9401 4.6499 12.0001 4.6499C16.7901 4.6499 20.6701 8.5299 20.6701 13.3199C20.6701 18.1099 16.7901 21.9899 12.0001 21.9899C7.21008 21.9899 3.33008 18.1099 3.33008 13.3199C3.33008 11.5399 3.87008 9.8799 4.79008 8.4999"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M7.87012 5.32L10.7601 2"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M7.87012 5.32007L11.2401 7.78007"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const SimpleGridIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M9 2V22"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const VerticalDotsIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 12c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -119,6 +119,7 @@ export const A11yOtb = () => {
|
||||
classNames={{
|
||||
base: "grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4",
|
||||
header: "pb-3",
|
||||
card: "bg-white dark:bg-default-400/10",
|
||||
iconWrapper: "bg-default-100 dark:bg-transparent text-default-500/50",
|
||||
}}
|
||||
features={a11yItems}
|
||||
@@ -128,7 +129,7 @@ export const A11yOtb = () => {
|
||||
as={Link}
|
||||
className="max-w-fit"
|
||||
color="success"
|
||||
href="/docs/theme/customize-theme"
|
||||
href="/docs/customization/customize-theme"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
@@ -161,6 +162,7 @@ export const A11yOtb = () => {
|
||||
isOpen={isDropdownOpen}
|
||||
placement="bottom"
|
||||
portalContainer={ref.current}
|
||||
shouldBlockScroll={false}
|
||||
shouldFlip={isMobile}
|
||||
onOpenChange={(open) => setIsDropdownOpen(open)}
|
||||
>
|
||||
@@ -175,7 +177,7 @@ export const A11yOtb = () => {
|
||||
color="default"
|
||||
variant="flat"
|
||||
>
|
||||
<DropdownSection title="Actions">
|
||||
<DropdownSection showDivider title="Actions">
|
||||
<DropdownItem
|
||||
key="new"
|
||||
description="Create a new file"
|
||||
|
||||
@@ -60,10 +60,10 @@ type Tab = {id: string; title: () => JSX.Element; icon: () => JSX.Element};
|
||||
const itemSizes = ["xs", "s", "m", "l", "xl"];
|
||||
|
||||
const codeHighlights = {
|
||||
nextui: "6-15",
|
||||
modern: "22-31",
|
||||
elegant: "38-47",
|
||||
retro: "54-67",
|
||||
nextui: "6-19",
|
||||
modern: "26-39",
|
||||
elegant: "46-59",
|
||||
retro: "66-84",
|
||||
};
|
||||
|
||||
const CustomThemesExample = ({
|
||||
@@ -117,7 +117,7 @@ const CustomThemesExample = ({
|
||||
/>
|
||||
)}
|
||||
</Tabs>
|
||||
<Card className={slots.wrapper()} radius="2xl">
|
||||
<Card className={slots.wrapper()} radius="lg">
|
||||
<CardBody className="relative flex-col md:flex-row md:items-center gap-4 md:gap-9 overflow-visible">
|
||||
<div className={slots.imageWrapper()}>
|
||||
<Image
|
||||
@@ -152,7 +152,7 @@ const CustomThemesExample = ({
|
||||
classNames={{
|
||||
wrapper: "hidden",
|
||||
labelWrapper: slots.sizeOption(),
|
||||
label: "text-sm font-semibold text-inherit",
|
||||
label: slots.sizeOptionLabel(),
|
||||
}}
|
||||
value={itemSize}
|
||||
>
|
||||
@@ -164,7 +164,6 @@ const CustomThemesExample = ({
|
||||
<Button
|
||||
className={slots.buyButton()}
|
||||
color="primary"
|
||||
radius="xl"
|
||||
variant={selectedTheme === "nextui" ? "shadow" : "solid"}
|
||||
>
|
||||
Buy now
|
||||
@@ -172,7 +171,7 @@ const CustomThemesExample = ({
|
||||
<Button
|
||||
className={slots.addToBagButton()}
|
||||
color="primary"
|
||||
radius="xl"
|
||||
radius="full"
|
||||
variant="bordered"
|
||||
>
|
||||
Add to bag
|
||||
@@ -195,9 +194,9 @@ const CustomThemesExample = ({
|
||||
<Button
|
||||
aria-label="Learn more about theme customization"
|
||||
as={NextLink}
|
||||
className="max-w-fit"
|
||||
className={slots.learnMoreButton()}
|
||||
color="primary"
|
||||
href="/docs/theme/customize-theme"
|
||||
href="/docs/customization/customize-theme"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
|
||||
@@ -56,10 +56,18 @@ export const shopCartStyles = tv({
|
||||
"items-center",
|
||||
"text-sm",
|
||||
"rounded-full",
|
||||
// focus ring
|
||||
"group-data-[focus-visible=true]:outline-none",
|
||||
"group-data-[focus-visible=true]:ring-2",
|
||||
"group-data-[focus-visible=true]:ring-focus",
|
||||
"group-data-[focus-visible=true]:ring-offset-2",
|
||||
"group-data-[focus-visible=true]:ring-offset-background",
|
||||
],
|
||||
sizeOptionLabel: "text-sm font-semibold text-inherit",
|
||||
buyButton: ["text-sm", "font-normal"],
|
||||
addToBagButton: ["text-sm", "font-normal"],
|
||||
starButton: "absolute top-3 right-3 text-default-400 data-[liked=true]:text-warning",
|
||||
learnMoreButton: "max-w-fit",
|
||||
},
|
||||
variants: {
|
||||
theme: {
|
||||
@@ -67,9 +75,10 @@ export const shopCartStyles = tv({
|
||||
card: "py-5",
|
||||
imageWrapper: ["before:rounded-2xl", "before:from-[#010187] before:to-[#18000E]"],
|
||||
sizeOption: [
|
||||
"group-data-[checked=true]:bg-primary",
|
||||
"group-data-[checked=true]:text-primary-foreground",
|
||||
"group-data-[selected=true]:bg-primary",
|
||||
"group-data-[selected=true]:text-primary-foreground",
|
||||
],
|
||||
learnMoreButton: "bg-primary/20 text-primary",
|
||||
},
|
||||
modern: {
|
||||
wrapper: "rounded-3xl",
|
||||
@@ -84,25 +93,34 @@ export const shopCartStyles = tv({
|
||||
title: "text-2xl",
|
||||
description: "text-sm",
|
||||
sizeOption: [
|
||||
"group-data-[checked=true]:bg-secondary",
|
||||
"group-data-[checked=true]:text-secondary-foreground",
|
||||
"group-data-[selected=true]:bg-secondary",
|
||||
"group-data-[selected=true]:text-secondary-foreground",
|
||||
"group-data-[focus-visible=true]:ring-secondary",
|
||||
],
|
||||
percentOff: "text-pink-500",
|
||||
buyButton: ["bg-secondary", "text-sm", "font-normal", "rounded-full"],
|
||||
buyButton: [
|
||||
"bg-secondary",
|
||||
"text-sm",
|
||||
"font-normal",
|
||||
"rounded-full",
|
||||
"data-[focus-visible=true]:ring-secondary",
|
||||
],
|
||||
addToBagButton: [
|
||||
"border-secondary",
|
||||
"text-secondary",
|
||||
"text-sm",
|
||||
"font-normal",
|
||||
"rounded-full",
|
||||
"data-[focus-visible=true]:ring-secondary",
|
||||
],
|
||||
learnMoreButton: "bg-secondary/20 text-secondary",
|
||||
},
|
||||
elegant: {
|
||||
wrapper: "rounded-md",
|
||||
wrapper: "rounded-sm",
|
||||
contentWrapper: "ml-3",
|
||||
imageWrapper: [
|
||||
"lg:scale-[1.3]",
|
||||
"before:rounded-md",
|
||||
"before:rounded-sm",
|
||||
"before:from-[#323232] before:to-[#000000]",
|
||||
"shadow-xl",
|
||||
],
|
||||
@@ -110,20 +128,32 @@ export const shopCartStyles = tv({
|
||||
title: "text-xl font-mono font-thin",
|
||||
description: "text-sm font-mono font-light",
|
||||
sizeOption: [
|
||||
"group-data-[checked=true]:bg-foreground",
|
||||
"group-data-[checked=true]:text-background",
|
||||
"rounded-sm w-7 h-7",
|
||||
"group-data-[selected=true]:bg-foreground",
|
||||
"group-data-[selected=true]:text-background",
|
||||
"group-data-[focus-visible=true]:ring-foreground",
|
||||
],
|
||||
sizeOptionLabel: "font-normal",
|
||||
price: "font-mono font-thin",
|
||||
previousPrice: "font-mono font-light ml-2",
|
||||
percentOff: "text-default-500 font-mono text-sm",
|
||||
buyButton: ["bg-foreground", "text-background", "text-sm", "font-normal", "rounded-sm"],
|
||||
buyButton: [
|
||||
"bg-foreground",
|
||||
"text-background",
|
||||
"text-sm",
|
||||
"font-normal",
|
||||
"rounded-sm",
|
||||
"data-[focus-visible=true]:ring-foreground",
|
||||
],
|
||||
addToBagButton: [
|
||||
"border-foreground",
|
||||
"text-foreground",
|
||||
"text-sm",
|
||||
"font-normal",
|
||||
"rounded-sm",
|
||||
"data-[focus-visible=true]:ring-foreground",
|
||||
],
|
||||
learnMoreButton: "bg-foreground/10 text-foreground",
|
||||
},
|
||||
retro: {
|
||||
wrapper: "bg-[#F4E8D1] dark:bg-[#E1CA9E] rounded-sm",
|
||||
@@ -145,8 +175,11 @@ export const shopCartStyles = tv({
|
||||
description: "text-[0.7rem] uppercase text-black",
|
||||
sizeOption: [
|
||||
"text-black/80",
|
||||
"group-data-[checked=true]:bg-[#EE457E]",
|
||||
"group-data-[checked=true]:text-white",
|
||||
"group-data-[selected=true]:bg-[#EE457E]",
|
||||
"group-data-[selected=true]:text-white",
|
||||
"group-data-[focus-visible=true]:ring-[#EE457E]",
|
||||
"group-data-[focus-visible=true]:ring-offset-[#F4E8D1]",
|
||||
"group-data-[focus-visible=true]:dark:ring-offset-[#F4E8D1]",
|
||||
],
|
||||
price: "text-black",
|
||||
previousPrice: "text-black/60 ml-2",
|
||||
@@ -158,6 +191,9 @@ export const shopCartStyles = tv({
|
||||
"uppercase",
|
||||
"text-sm",
|
||||
"rounded-sm",
|
||||
"data-[focus-visible=true]:ring-[#EE457E]",
|
||||
"data-[focus-visible=true]:ring-offset-[#F4E8D1]",
|
||||
"data-[focus-visible=true]:dark:ring-offset-[#F4E8D1]",
|
||||
],
|
||||
addToBagButton: [
|
||||
"border-[#FFD34E]",
|
||||
@@ -166,7 +202,11 @@ export const shopCartStyles = tv({
|
||||
"font-medium",
|
||||
"text-sm",
|
||||
"rounded-sm",
|
||||
"data-[focus-visible=true]:ring-[#EE457E]",
|
||||
"data-[focus-visible=true]:ring-offset-[#F4E8D1]",
|
||||
"data-[focus-visible=true]:dark:ring-offset-[#F4E8D1]",
|
||||
],
|
||||
learnMoreButton: "bg-warning/20 text-warning",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@ export const Customization = () => {
|
||||
aria-label="Learn more about customization"
|
||||
as={NextLink}
|
||||
className="max-w-fit bg-pink-100 text-pink-500 dark:bg-pink-900 dark:text-pink-300"
|
||||
href="/docs/theme/customize-theme"
|
||||
href="/docs/customization/customize-theme"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
|
||||
@@ -56,7 +56,7 @@ export const DarkMode = () => {
|
||||
</Tooltip>
|
||||
<ThemeSwitch
|
||||
classNames={{
|
||||
base: "p-1.5 bg-transparent rounded-xl",
|
||||
base: "mt-1 bg-transparent rounded-xl",
|
||||
wrapper: "!text-white/70 dark:!text-black/70",
|
||||
}}
|
||||
/>
|
||||
@@ -78,7 +78,7 @@ export const DarkMode = () => {
|
||||
as={NextLink}
|
||||
className="max-w-fit"
|
||||
color="warning"
|
||||
href="/docs/theme/dark-mode"
|
||||
href="/docs/customization/dark-mode"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
|
||||
@@ -9,7 +9,7 @@ import {LinkIcon} from "@nextui-org/shared-icons";
|
||||
const styles = tv({
|
||||
slots: {
|
||||
base: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4",
|
||||
card: "dark:border-transparent bg-white/5 dark:bg-default-400/10 backdrop-blur-lg backdrop-saturate-[1.8]",
|
||||
card: "border-transparent bg-white/5 dark:bg-default-400/10 backdrop-blur-lg backdrop-saturate-[1.8]",
|
||||
header: "gap-2 pb-0",
|
||||
body: "",
|
||||
iconWrapper:
|
||||
@@ -65,13 +65,13 @@ export const FeaturesGrid: React.FC<FeaturesGridProps> = ({features, classNames,
|
||||
<p className={slots.title({class: classNames?.title})}>{feat.title}</p>
|
||||
{feat.isExternal && <LinkIcon className="text-white" height={18} width={18} />}
|
||||
</CardHeader>
|
||||
{feat.description && (
|
||||
{feat.description ? (
|
||||
<CardBody className={slots.body({class: classNames?.body})}>
|
||||
<p className={slots.description({class: classNames?.description})}>
|
||||
{feat.description}
|
||||
</p>
|
||||
</CardBody>
|
||||
)}
|
||||
) : null}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export const BgLooper = () => {
|
||||
"data-[mounted=true]:opacity-100 transition-opacity",
|
||||
"bg-left bg-no-repeat bg-[url('/gradients/looper-pattern.svg')]",
|
||||
"after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:z-[-1]",
|
||||
"after:bg-gradient-to-r after:from-transparent after:to-white dark:after:to-black after:z-[-1]",
|
||||
"after:bg-gradient-to-r after:from-transparent after:to-background dark:after:to-black after:z-[-1]",
|
||||
)}
|
||||
data-mounted={isMounted}
|
||||
/>
|
||||
|
||||
@@ -38,14 +38,13 @@ export const FloatingComponents: React.FC<{}> = () => {
|
||||
<>
|
||||
<Switch
|
||||
classNames={{
|
||||
base:
|
||||
"absolute -top-[220px] -right-[40px] animate-[levitate_13s_ease_infinite_1s_reverse]",
|
||||
base: "absolute -top-[220px] -right-[40px] animate-[levitate_13s_ease_infinite_1s_reverse]",
|
||||
wrapper: "shadow-sm",
|
||||
startContent: "text-white",
|
||||
}}
|
||||
endContent={<MoonFilledIcon />}
|
||||
isSelected={isSelected}
|
||||
size="xl"
|
||||
size="lg"
|
||||
startContent={<SunFilledIcon />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
@@ -56,7 +55,6 @@ export const FloatingComponents: React.FC<{}> = () => {
|
||||
defaultValue="NextUI"
|
||||
label="Input"
|
||||
labelPlacement="outside"
|
||||
radius="xl"
|
||||
variant="bordered"
|
||||
onClear={() => {}}
|
||||
/>
|
||||
@@ -64,7 +62,6 @@ export const FloatingComponents: React.FC<{}> = () => {
|
||||
<Card
|
||||
isFooterBlurred
|
||||
className="absolute -top-[260px] right-[100px] h-[120px] animate-[levitate_12s_ease_infinite_1s] z-0 max-w-fit"
|
||||
radius="2xl"
|
||||
>
|
||||
<Image
|
||||
alt="Professional camera"
|
||||
@@ -74,7 +71,7 @@ export const FloatingComponents: React.FC<{}> = () => {
|
||||
src="/images/card-example-6.webp"
|
||||
width={120}
|
||||
/>
|
||||
<CardFooter className="before:bg-black/10 before:border before:border-white/20 overflow-hidden justify-between py-2 absolute before:rounded-xl rounded-xl bottom-1 w-[calc(100%_-_8px)] shadow-lg ml-1 z-10">
|
||||
<CardFooter className="before:bg-black/10 border-white/20 border-1 overflow-hidden justify-between py-2 absolute before:rounded-xl rounded-xl bottom-1 w-[calc(100%_-_8px)] shadow-lg ml-1 z-10">
|
||||
<p className="text-xs font-semibold text-white/80">Camera</p>
|
||||
<p className="text-xs font-semibold text-white/80">$525</p>
|
||||
</CardFooter>
|
||||
@@ -84,7 +81,10 @@ export const FloatingComponents: React.FC<{}> = () => {
|
||||
|
||||
<UserTwitterCard className="absolute left-[80px] -top-[80px] animate-[levitate_16s_ease_infinite] border-none" />
|
||||
|
||||
<Card className="absolute right-[110px] -top-[60px] animate-[levitate_18s_ease_infinite] shadow-lg z-10 max-w-fit border-none">
|
||||
<Card
|
||||
className="absolute right-[110px] -top-[60px] animate-[levitate_18s_ease_infinite] z-10 max-w-fit border-none"
|
||||
shadow="lg"
|
||||
>
|
||||
<CardBody>
|
||||
<NextUILogo small size={60} />
|
||||
</CardBody>
|
||||
@@ -96,10 +96,7 @@ export const FloatingComponents: React.FC<{}> = () => {
|
||||
showControls
|
||||
showShadow
|
||||
classNames={{
|
||||
base: "shadow-sm rounded-xl",
|
||||
item: "bg-background dark:bg-content1",
|
||||
prev: "bg-background dark:bg-content1",
|
||||
next: "bg-background dark:bg-content1",
|
||||
base: "rounded-xl",
|
||||
}}
|
||||
initialPage={6}
|
||||
total={10}
|
||||
@@ -126,16 +123,18 @@ export const FloatingComponents: React.FC<{}> = () => {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Card className="absolute right-[10px] top-[30px] animate-[levitate_16s_ease_infinite] shadow-lg z-10 max-w-fit border-none">
|
||||
<Card
|
||||
className="absolute right-[10px] top-[30px] animate-[levitate_16s_ease_infinite] z-10 max-w-fit border-none"
|
||||
shadow="lg"
|
||||
>
|
||||
<CardBody>
|
||||
<Spinner color="secondary" size="xl" />
|
||||
<Spinner color="secondary" size="lg" />
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
isFooterBlurred
|
||||
className="absolute right-[60px] top-[100px] animate-[levitate_12s_ease_infinite_1s] z-0 max-w-fit"
|
||||
radius="2xl"
|
||||
>
|
||||
<Image
|
||||
alt="Woman listing to music"
|
||||
@@ -145,9 +144,15 @@ export const FloatingComponents: React.FC<{}> = () => {
|
||||
src="/images/hero-card.webp"
|
||||
width={200}
|
||||
/>
|
||||
<CardFooter className="before:bg-white/10 overflow-hidden justify-between py-2 absolute before:rounded-xl rounded-xl bottom-1 w-[calc(100%_-_8px)] shadow-lg ml-1 z-10">
|
||||
<p className="text-xs text-white/80">Available soon.</p>
|
||||
<Button color="secondary" radius="full" size="xs">
|
||||
<CardFooter className="justify-between before:bg-white/10 border-white/20 border-1 overflow-hidden py-1 absolute before:rounded-xl rounded-large bottom-1 w-[calc(100%_-_8px)] shadow-small ml-1 z-10">
|
||||
<p className="text-tiny text-white/80">Available soon.</p>
|
||||
<Button
|
||||
className="text-tiny text-white bg-black/20"
|
||||
color="default"
|
||||
radius="lg"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
Notify me
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
@@ -9,10 +9,10 @@ export const FloatingTabs: React.FC<{}> = () => {
|
||||
className=""
|
||||
classNames={{
|
||||
base: "absolute left-[170px] -top-[160px] h-10 animate-[levitate_17s_ease_infinite_1s]",
|
||||
tabList: "max-w-[200px] bg-content1 shadow-sm",
|
||||
tabList: "max-w-[200px] shadow-sm",
|
||||
}}
|
||||
radius="full"
|
||||
size="xs"
|
||||
size="sm"
|
||||
>
|
||||
<Tab key="notes" title="Notes" />
|
||||
<Tab key="tasks" title="Tasks" />
|
||||
|
||||
@@ -35,7 +35,7 @@ export const Hero = () => {
|
||||
color="primary"
|
||||
endContent={
|
||||
<ArrowRightIcon
|
||||
className="group-data-[hover=true]:translate-x-0.5 transition-transform"
|
||||
className="group-data-[hover=true]:translate-x-0.5 outline-none transition-transform"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Button, Link} from "@nextui-org/react";
|
||||
import {ArrowRightIcon} from "@nextui-org/shared-icons";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import NextLink from "next/link";
|
||||
import {Code} from "@nextui-org/react";
|
||||
|
||||
import {FeaturesGrid} from "./features-grid";
|
||||
|
||||
@@ -21,8 +22,11 @@ const bannerSuggestions = [
|
||||
},
|
||||
{
|
||||
title: "NextUI + Next.js",
|
||||
description:
|
||||
"NextUI is totally compatible with Next.js you just need to customize the _app.jsx entry file to load the provider.",
|
||||
description: (
|
||||
<>
|
||||
NextUI is fully compatible with the new Next.js 13 <Code>app/</Code> directory structure.
|
||||
</>
|
||||
),
|
||||
icon: <NextJsIcon className="text-pink-500" />,
|
||||
href: "/docs/frameworks/nextjs",
|
||||
},
|
||||
@@ -58,7 +62,7 @@ export const InstallBanner = () => {
|
||||
color="secondary"
|
||||
endContent={
|
||||
<ArrowRightIcon
|
||||
className="group-data-[hover=true]:translate-x-0.5 transition-transform"
|
||||
className="group-data-[hover=true]:translate-x-0.5 outline-none transition-transform"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -58,10 +58,10 @@ const getSponsorSize = (sponsor: Sponsor, isMobile: boolean) => {
|
||||
size = isMobile ? "sm" : "md";
|
||||
break;
|
||||
case SPONSOR_TIERS.GOLD:
|
||||
size = isMobile ? "lg" : "xl";
|
||||
size = isMobile ? "md" : "lg";
|
||||
break;
|
||||
case SPONSOR_TIERS.PLATINUM:
|
||||
size = isMobile ? "lg" : "xl";
|
||||
size = isMobile ? "md" : "lg";
|
||||
break;
|
||||
default:
|
||||
size = isMobile ? "sm" : "md";
|
||||
@@ -174,7 +174,7 @@ export const Support: FC<SupportProps> = ({sponsors = []}) => {
|
||||
color="secondary"
|
||||
content={"Become a sponsor"}
|
||||
offset={10}
|
||||
radius="xl"
|
||||
radius="full"
|
||||
>
|
||||
<Button
|
||||
isIconOnly
|
||||
|
||||
@@ -4,8 +4,12 @@ import * as Components from "@nextui-org/react";
|
||||
import {Language} from "prism-react-renderer";
|
||||
import NextImage from "next/image";
|
||||
|
||||
import {ThemeSwitch} from "./theme-switch";
|
||||
|
||||
import {Sandpack} from "@/components/sandpack";
|
||||
import {CarbonAd} from "@/components/ads/carbon-ad";
|
||||
import * as DocsComponents from "@/components/docs/components";
|
||||
import * as BlogComponents from "@/components/blog/components";
|
||||
import {Codeblock} from "@/components/docs/components";
|
||||
import {VirtualAnchor, virtualAnchorEncode} from "@/components";
|
||||
|
||||
@@ -98,19 +102,31 @@ const List: React.FC<{children?: React.ReactNode}> = ({children}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const InlineCode = ({children}: {children?: React.ReactNode}) => {
|
||||
return (
|
||||
<Components.Code className="font-normal bg-transparent px-0 py-0 text-code-mdx">
|
||||
{children}
|
||||
</Components.Code>
|
||||
);
|
||||
};
|
||||
|
||||
const Code = ({
|
||||
className,
|
||||
children,
|
||||
metastring,
|
||||
meta,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
metastring?: string;
|
||||
className?: string;
|
||||
meta?: string;
|
||||
}) => {
|
||||
const isMultiLine = (children as string).split("\n").length > 2;
|
||||
const isMultiLine = (children as string)?.split?.("\n")?.length > 2;
|
||||
const language = (className?.replace(/language-/, "") ?? "jsx") as Language;
|
||||
const codeString = String(children).trim();
|
||||
|
||||
if (!className) {
|
||||
return <InlineCode>{children}</InlineCode>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Components.Snippet
|
||||
disableTooltip
|
||||
@@ -129,7 +145,7 @@ const Code = ({
|
||||
}}
|
||||
codeString={codeString}
|
||||
>
|
||||
<Codeblock codeString={codeString} language={language} metastring={metastring} />
|
||||
<Codeblock codeString={codeString} language={language} metastring={meta} />
|
||||
</Components.Snippet>
|
||||
);
|
||||
};
|
||||
@@ -144,14 +160,6 @@ const Link = ({href, children}: {href?: string; children?: React.ReactNode}) =>
|
||||
);
|
||||
};
|
||||
|
||||
const InlineCode = ({children}: {children?: React.ReactNode}) => {
|
||||
return (
|
||||
<Components.Code className="font-normal bg-transparent px-0 py-0 text-code-mdx">
|
||||
{children}
|
||||
</Components.Code>
|
||||
);
|
||||
};
|
||||
|
||||
export const MDXComponents = {
|
||||
/**
|
||||
* Next.js components
|
||||
@@ -166,6 +174,11 @@ export const MDXComponents = {
|
||||
*/
|
||||
...DocsComponents,
|
||||
Sandpack,
|
||||
ThemeSwitch,
|
||||
/**
|
||||
* Blog components
|
||||
*/
|
||||
...BlogComponents,
|
||||
/**
|
||||
* Markdown components
|
||||
*/
|
||||
@@ -183,16 +196,13 @@ export const MDXComponents = {
|
||||
thead: Thead,
|
||||
tr: Trow,
|
||||
td: Tcol,
|
||||
// Playground,
|
||||
// CarbonAd,
|
||||
CarbonAd,
|
||||
code: Code,
|
||||
ul: List,
|
||||
a: (props: React.HTMLAttributes<HTMLAnchorElement>) => <Link {...props} />,
|
||||
blockquote: (props: Omit<React.HTMLAttributes<HTMLElement>, "color">) => (
|
||||
<DocsComponents.Blockquote {...props} />
|
||||
),
|
||||
inlineCode: InlineCode,
|
||||
InlineCode,
|
||||
kbd: (props: React.HTMLAttributes<HTMLElement>) => (
|
||||
<Components.Kbd {...props} className="py-0.5 px-1.5" />
|
||||
),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable react/display-name */
|
||||
"use client";
|
||||
import type {MDXComponents as MDXComponentsType} from "mdx/types";
|
||||
|
||||
import {useMDXComponent} from "next-contentlayer/hooks";
|
||||
|
||||
import {MDXComponents} from "./mdx-components";
|
||||
|
||||
interface MDXContentProps {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export function MDXContent({code}: MDXContentProps) {
|
||||
const Component = useMDXComponent(code);
|
||||
|
||||
return (
|
||||
<div className="mdx">
|
||||
<Component components={MDXComponents as MDXComponentsType} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {FC} from "react";
|
||||
import {MDXRemote as BaseMDXRemote, MDXRemoteSerializeResult} from "next-mdx-remote";
|
||||
|
||||
import {MDXComponents} from "@/components/mdx-components";
|
||||
import * as componentsContent from "@/content/components";
|
||||
|
||||
const scope = {
|
||||
// Markdown content
|
||||
...componentsContent,
|
||||
};
|
||||
|
||||
export interface MDXRemoteProps {
|
||||
source?: MDXRemoteSerializeResult;
|
||||
}
|
||||
|
||||
export const MDXRemote: FC<MDXRemoteProps> = ({source}) => {
|
||||
if (!source) return null;
|
||||
|
||||
return <BaseMDXRemote {...source} components={MDXComponents} scope={scope} />;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {useRef, useState, FC, ReactNode} from "react";
|
||||
import {useRef, useState, FC, ReactNode, Key} from "react";
|
||||
import {
|
||||
link,
|
||||
Navbar as NextUINavbar,
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Chip,
|
||||
} from "@nextui-org/react";
|
||||
import {dataFocusVisibleClasses} from "@nextui-org/theme";
|
||||
import {ChevronDownIcon, LinkIcon} from "@nextui-org/shared-icons";
|
||||
import {isAppleDevice} from "@react-aria/utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
@@ -44,21 +46,22 @@ import {useCmdkStore} from "@/components/cmdk";
|
||||
|
||||
export interface NavbarProps {
|
||||
routes: Route[];
|
||||
mobileRoutes?: Route[];
|
||||
tag?: string;
|
||||
slug?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], slug, tag}) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState<boolean | undefined>(false);
|
||||
const [commandKey, setCommandKey] = useState<"ctrl" | "command">("command");
|
||||
|
||||
const ref = useRef(null);
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const cmkdStore = useCmdkStore();
|
||||
const cmdkStore = useCmdkStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isMenuOpen) {
|
||||
@@ -71,7 +74,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
}, []);
|
||||
|
||||
const {pressProps} = usePress({
|
||||
onPress: () => cmkdStore.onOpen(),
|
||||
onPress: () => cmdkStore.onOpen(),
|
||||
});
|
||||
const {focusProps, isFocusVisible} = useFocusRing();
|
||||
|
||||
@@ -97,12 +100,26 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
strokeWidth={2}
|
||||
/>
|
||||
}
|
||||
onPress={() => cmkdStore.onOpen()}
|
||||
onPress={() => cmdkStore.onOpen()}
|
||||
>
|
||||
Quick Search...
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (pathname.includes("/examples")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const navLinkClasses = clsx(link({color: "foreground"}), "data-[active=true]:text-primary");
|
||||
|
||||
const handleVersionChange = (key: Key) => {
|
||||
if (key === "v1") {
|
||||
const newWindow = window.open("https://v1.nextui.org", "_blank", "noopener,noreferrer");
|
||||
|
||||
if (newWindow) newWindow.opener = null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NextUINavbar
|
||||
ref={ref}
|
||||
@@ -111,12 +128,16 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
})}
|
||||
isMenuOpen={isMenuOpen}
|
||||
maxWidth="xl"
|
||||
position="fixed"
|
||||
position="sticky"
|
||||
onMenuOpenChange={setIsMenuOpen}
|
||||
>
|
||||
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
|
||||
<NavbarBrand as="li" className="gap-3 max-w-fit">
|
||||
<NextLink aria-label="Home" className="flex justify-start items-center gap-2" href="/">
|
||||
<NextLink
|
||||
aria-label="Home"
|
||||
className="flex justify-start items-center gap-2 tap-highlight-transparent transition-opacity active:opacity-50"
|
||||
href="/"
|
||||
>
|
||||
<SmallLogo className="w-6 h-6 md:hidden" />
|
||||
<LargeLogo className="h-5 md:h-6" />
|
||||
</NextLink>
|
||||
@@ -127,10 +148,10 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
<motion.div animate={{opacity: 1}} exit={{opacity: 0}} initial={{opacity: 0}}>
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
className="hidden min-w-fit max-w-[64px] sm:flex gap-0.5 bg-default-400/20 dark:bg-default-500/20"
|
||||
endContent={<ChevronDownIcon className="text-xs" />}
|
||||
className="hidden text-xs h-6 w-[74px] py-1 min-w-fit sm:flex gap-0.5 bg-default-400/20 dark:bg-default-500/20"
|
||||
endContent={<ChevronDownIcon className="text-tiny" />}
|
||||
radius="full"
|
||||
size="xs"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
v2.0.0
|
||||
@@ -143,6 +164,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
aria-label="NextUI versions"
|
||||
defaultSelectedKeys={["v2"]}
|
||||
selectionMode="single"
|
||||
onAction={handleVersionChange}
|
||||
>
|
||||
<DropdownItem key="v2">v2.0.0</DropdownItem>
|
||||
<DropdownItem key="v1" endContent={<LinkIcon />}>
|
||||
@@ -151,16 +173,13 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div className="w-[64px]" />
|
||||
<div className="w-[74px]" />
|
||||
)}
|
||||
</NavbarBrand>
|
||||
<ul className="hidden lg:flex gap-4 justify-start">
|
||||
<ul className="hidden lg:flex gap-4 justify-start items-center">
|
||||
<NavbarItem>
|
||||
<NextLink
|
||||
className={clsx(
|
||||
link({color: "foreground"}),
|
||||
"data-[active=true]:text-primary data-[active=true]:font-medium",
|
||||
)}
|
||||
className={navLinkClasses}
|
||||
color="foreground"
|
||||
data-active={includes(docsPaths, pathname)}
|
||||
href="/docs/guide/introduction"
|
||||
@@ -170,10 +189,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NextLink
|
||||
className={clsx(
|
||||
link({color: "foreground"}),
|
||||
"data-[active=true]:text-primary data-[active=true]:font-medium",
|
||||
)}
|
||||
className={navLinkClasses}
|
||||
color="foreground"
|
||||
data-active={includes(pathname, "components")}
|
||||
href="/docs/components/avatar"
|
||||
@@ -183,17 +199,35 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NextLink
|
||||
className={clsx(
|
||||
link({color: "foreground"}),
|
||||
"data-[active=true]:text-primary data-[active=true]:font-medium",
|
||||
)}
|
||||
className={navLinkClasses}
|
||||
color="foreground"
|
||||
data-active={pathname === "/figma"}
|
||||
data-active={includes(pathname, "blog")}
|
||||
href="/blog"
|
||||
>
|
||||
Blog
|
||||
</NextLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NextLink
|
||||
className={navLinkClasses}
|
||||
color="foreground"
|
||||
data-active={includes(pathname, "figma")}
|
||||
href="/figma"
|
||||
>
|
||||
Figma
|
||||
</NextLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Chip
|
||||
as={NextLink}
|
||||
className="hover:bg-default-100 border-default-200/80 dark:border-default-100/80 transition-colors cursor-pointer"
|
||||
color="secondary"
|
||||
href="/blog/nextui-v2"
|
||||
variant="dot"
|
||||
>
|
||||
Introducing NextUI v2.0
|
||||
</Chip>
|
||||
</NavbarItem>
|
||||
</ul>
|
||||
</NavbarContent>
|
||||
|
||||
@@ -216,13 +250,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
className={clsx(
|
||||
"transition-opacity p-1 hover:opacity-80 rounded-full cursor-pointer outline-none",
|
||||
// focus ring
|
||||
"data-[focus-visible=true]:z-10",
|
||||
"data-[focus-visible=true]:outline-none",
|
||||
"data-[focus-visible=true]:ring-2",
|
||||
"data-[focus-visible=true]:ring-primary",
|
||||
"data-[focus-visible=true]:ring-offset-2",
|
||||
"data-[focus-visible=true]:ring-offset-background",
|
||||
"data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
...dataFocusVisibleClasses,
|
||||
)}
|
||||
data-focus-visible={isFocusVisible}
|
||||
{...focusProps}
|
||||
@@ -231,7 +259,12 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
<SearchLinearIcon className="mt-px text-default-600 dark:text-default-500" size={20} />
|
||||
</button>
|
||||
</NavbarItem>
|
||||
<NavbarMenuToggle aria-label={isMenuOpen ? "Close menu" : "Open menu"} className="ml-2" />
|
||||
<NavbarItem className="w-10 h-full">
|
||||
<NavbarMenuToggle
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="w-full h-full pt-1"
|
||||
/>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex basis-1/5 sm:basis-full" justify="end">
|
||||
@@ -261,15 +294,15 @@ export const Navbar: FC<NavbarProps> = ({children, routes, slug, tag}) => {
|
||||
>
|
||||
Sponsor
|
||||
</Button>
|
||||
</NavbarItem>{" "}
|
||||
</NavbarItem>
|
||||
<NavbarMenuToggle
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="hidden sm:flex lg:hidden"
|
||||
className="hidden sm:flex lg:hidden ml-4"
|
||||
/>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarMenu>
|
||||
<DocsSidebar className="mt-4" routes={routes} slug={slug} tag={tag} />
|
||||
<DocsSidebar className="mt-4" routes={[...mobileRoutes, ...routes]} slug={slug} tag={tag} />
|
||||
{children}
|
||||
</NavbarMenu>
|
||||
</NextUINavbar>
|
||||
|
||||
@@ -24,11 +24,11 @@ export const BugReportButton = () => {
|
||||
isIconOnly
|
||||
as={Link}
|
||||
href={`${ISSUE_REPORT_URL}${componentTitle}`}
|
||||
size="xs"
|
||||
size="sm"
|
||||
title="Report a bug"
|
||||
variant="light"
|
||||
>
|
||||
<BugIcon className="text-zinc-500" height={16} width={16} />
|
||||
<BugIcon className="text-white dark:text-zinc-500" height={16} width={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -106,7 +106,7 @@ export const SandpackCodeViewer = React.forwardRef<any, CodeViewerProps>(
|
||||
<SandpackStack>
|
||||
{shouldShowTabs ? <FileTabs /> : null}
|
||||
<div
|
||||
className={clsx("sp-code-viewer", {
|
||||
className={clsx("sp-code-viewer max-h-[600px] overflow-y-scroll", {
|
||||
"is-expanded": isExpanded,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@ export const CodeSandboxButton = () => {
|
||||
placement="top"
|
||||
radius="md"
|
||||
>
|
||||
<Button isIconOnly as="span" size="xs" title="Open in CodeSandbox" variant="light">
|
||||
<Button isIconOnly as="span" size="sm" title="Open in CodeSandbox" variant="light">
|
||||
<UnstyledOpenInCodeSandboxButton
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -30,7 +30,7 @@ export const CodeSandboxButton = () => {
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<CodeSandboxIcon className="text-zinc-500" height={20} width={20} />
|
||||
<CodeSandboxIcon className="text-white dark:text-zinc-500" height={20} width={20} />
|
||||
</UnstyledOpenInCodeSandboxButton>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -23,8 +23,8 @@ export const CopyButton = () => {
|
||||
content={copied ? "Copied!" : "Copy"}
|
||||
radius="md"
|
||||
>
|
||||
<Button isIconOnly size="xs" title="Copy Code" variant="light" onClick={copyHandler}>
|
||||
<CopyLinearIcon className="text-zinc-500" height={16} width={16} />
|
||||
<Button isIconOnly size="sm" title="Copy Code" variant="light" onClick={copyHandler}>
|
||||
<CopyLinearIcon className="text-white dark:text-zinc-500" height={16} width={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const defaultEntry = `
|
||||
export const getFileEntry = (theme: string) => `
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { NextUIProvider } from "@nextui-org/react";
|
||||
@@ -8,24 +8,7 @@ import "./styles.css";
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<NextUIProvider>
|
||||
<div className="w-screen h-screen p-8 flex items-start justify-center">
|
||||
<App />
|
||||
</div>
|
||||
</NextUIProvider>
|
||||
</React.StrictMode>
|
||||
);`;
|
||||
|
||||
export const systemEntry = `
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { NextUIProvider } from "@nextui-org/system";
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<NextUIProvider>
|
||||
<div className="w-screen h-screen p-8 flex items-start justify-center">
|
||||
<div className="w-screen h-screen ${theme} text-foreground bg-background p-8 flex items-start justify-center">
|
||||
<App />
|
||||
</div>
|
||||
</NextUIProvider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, {useCallback} from "react";
|
||||
import {Tabs, Tab} from "@nextui-org/react";
|
||||
import {SandpackPredefinedTemplate} from "@codesandbox/sandpack-react";
|
||||
|
||||
@@ -12,11 +12,17 @@ interface Props {
|
||||
export type LanguageSelectorProps = Props;
|
||||
|
||||
export const LanguageSelector: React.FC<LanguageSelectorProps> = ({template, onChange}) => {
|
||||
const handleToggle = () => {
|
||||
const [selectedTemplate, setSelectedTemplate] = React.useState(template);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
const newTemplate = template === "vite-react" ? "vite-react-ts" : "vite-react";
|
||||
|
||||
onChange?.(newTemplate);
|
||||
};
|
||||
setSelectedTemplate(newTemplate);
|
||||
|
||||
setTimeout(() => {
|
||||
onChange?.(newTemplate);
|
||||
}, 250);
|
||||
}, [template, onChange]);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
@@ -27,21 +33,21 @@ export const LanguageSelector: React.FC<LanguageSelectorProps> = ({template, onC
|
||||
tabList:
|
||||
"bg-transparent relative before:bg-white/5 before:w-full before:rounded-lg before:h-full before:content-[''] before:block before:z-1 before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-100",
|
||||
}}
|
||||
radius="lg"
|
||||
selectedKey={template}
|
||||
size="xs"
|
||||
radius="md"
|
||||
selectedKey={selectedTemplate}
|
||||
size="sm"
|
||||
onSelectionChange={handleToggle}
|
||||
>
|
||||
<Tab
|
||||
key="vite-react-ts"
|
||||
title={
|
||||
<TypescriptIcon className="text-lg text-[#6b6b6b] group-data-[selected=true]:text-[#fafafa]" />
|
||||
<TypescriptIcon className="text-lg text-white dark:text-[#6b6b6b] group-data-[selected=true]:text-[#fafafa]" />
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
key="vite-react"
|
||||
title={
|
||||
<JavascriptIcon className="text-lg text-[#6b6b6b] group-data-[selected=true]:text-[#fafafa]" />
|
||||
<JavascriptIcon className="text-lg text-white dark:text-[#6b6b6b] group-data-[selected=true]:text-[#fafafa]" />
|
||||
}
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
@@ -37,18 +37,12 @@ export const Sandpack: FC<SandpackProps> = ({
|
||||
}) => {
|
||||
const editorContainerRef = useRef(null);
|
||||
|
||||
const {
|
||||
files,
|
||||
decorators,
|
||||
customSetup,
|
||||
sandpackTemplate,
|
||||
hasTypescript,
|
||||
setCurrentTemplate,
|
||||
} = useSandpack({
|
||||
files: filesProp,
|
||||
template,
|
||||
highlightedLines,
|
||||
});
|
||||
const {files, decorators, customSetup, sandpackTemplate, hasTypescript, setCurrentTemplate} =
|
||||
useSandpack({
|
||||
files: filesProp,
|
||||
template,
|
||||
highlightedLines,
|
||||
});
|
||||
|
||||
return (
|
||||
<SandpackProvider
|
||||
|
||||
@@ -30,8 +30,7 @@ export const nextuiTheme: SandpackTheme = {
|
||||
},
|
||||
font: {
|
||||
body: "Inter var",
|
||||
mono:
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
size: "14px",
|
||||
lineHeight: "1.5rem",
|
||||
},
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {useMemo} from "react";
|
||||
import {
|
||||
SandpackFiles,
|
||||
SandpackPredefinedTemplate,
|
||||
SandpackSetup,
|
||||
} from "@codesandbox/sandpack-react";
|
||||
import {SandpackFiles, SandpackPredefinedTemplate} from "@codesandbox/sandpack-react";
|
||||
import {useTheme} from "next-themes";
|
||||
|
||||
import {HighlightedLines} from "./types";
|
||||
import {getHighlightedLines, getFileName} from "./utils";
|
||||
import {defaultEntry, stylesConfig, postcssConfig, tailwindConfig} from "./entries";
|
||||
import {getFileEntry, stylesConfig, postcssConfig, tailwindConfig} from "./entries";
|
||||
|
||||
import {useLocalStorage} from "@/hooks/use-local-storage";
|
||||
|
||||
@@ -17,6 +14,9 @@ export interface UseSandpackProps {
|
||||
highlightedLines?: HighlightedLines;
|
||||
}
|
||||
|
||||
const importReact = 'import React from "react";';
|
||||
const importAllReact = 'import * as React from "react";';
|
||||
|
||||
export const useSandpack = ({
|
||||
files = {},
|
||||
template = "vite-react",
|
||||
@@ -31,6 +31,8 @@ export const useSandpack = ({
|
||||
(file) => file.includes(".ts") || file.includes(".tsx"),
|
||||
);
|
||||
|
||||
const {theme} = useTheme();
|
||||
|
||||
const decorators = getHighlightedLines(highlightedLines, currentTemplate);
|
||||
|
||||
const sandpackTemplate = useMemo<SandpackPredefinedTemplate>(
|
||||
@@ -39,9 +41,10 @@ export const useSandpack = ({
|
||||
);
|
||||
|
||||
// map current template to its mime type
|
||||
const mimeType = useMemo(() => (sandpackTemplate === "vite-react-ts" ? ".tsx" : ".jsx"), [
|
||||
sandpackTemplate,
|
||||
]);
|
||||
const mimeType = useMemo(
|
||||
() => (sandpackTemplate === "vite-react-ts" ? ".tsx" : ".jsx"),
|
||||
[sandpackTemplate],
|
||||
);
|
||||
|
||||
// get entry file by current template
|
||||
const entryFile = useMemo(
|
||||
@@ -51,14 +54,20 @@ export const useSandpack = ({
|
||||
|
||||
// filter files by current template
|
||||
const filteredFiles = Object.keys(files).reduce((acc, key) => {
|
||||
if (key.includes(mimeType)) {
|
||||
// @ts-ignore
|
||||
acc[key] = files[key];
|
||||
if (key.includes("App") && !key.includes(mimeType)) {
|
||||
return acc;
|
||||
}
|
||||
// @ts-ignore
|
||||
acc[key] = files[key];
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
let dependencies = {
|
||||
"framer-motion": "10.12.16",
|
||||
"@nextui-org/react": "latest",
|
||||
};
|
||||
|
||||
// sort files by dependency
|
||||
const sortedFiles = Object.keys(filteredFiles)
|
||||
.sort((a: string, b: string) => {
|
||||
@@ -77,9 +86,30 @@ export const useSandpack = ({
|
||||
return 0;
|
||||
})
|
||||
.reduce((acc, key) => {
|
||||
let fileContent = files[key] as string;
|
||||
|
||||
// Check if the file content includes 'React' import statements, if not, add it
|
||||
if (!fileContent.includes(importReact) && !fileContent.includes(importAllReact)) {
|
||||
fileContent = `${importReact}\n${fileContent}\n`;
|
||||
}
|
||||
|
||||
// Check if file content includes any other dependencies, if yes, add it to dependencies
|
||||
const importRegex = /import .* from ["'](.*)["']/g;
|
||||
let match;
|
||||
|
||||
while ((match = importRegex.exec(fileContent)) !== null) {
|
||||
const dependencyName = match[1];
|
||||
|
||||
if (!dependencies.hasOwnProperty(dependencyName) && !dependencyName.includes("./")) {
|
||||
// add the dependency to the dependencies object with version 'latest'
|
||||
// @ts-ignore
|
||||
dependencies[dependencyName] = "latest";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[key]: files[key],
|
||||
[key]: fileContent,
|
||||
};
|
||||
}, {});
|
||||
|
||||
@@ -128,28 +158,22 @@ export const useSandpack = ({
|
||||
// [tailwindConfig, nextUIComponents],
|
||||
// );
|
||||
|
||||
const customSetup = useMemo<SandpackSetup>(
|
||||
() => ({
|
||||
entry: entryFile,
|
||||
dependencies: {
|
||||
"framer-motion": "10.12.16",
|
||||
"@nextui-org/react": "dev-v2",
|
||||
},
|
||||
devDependencies: {
|
||||
autoprefixer: "^10.4.14",
|
||||
postcss: "^8.4.21",
|
||||
tailwindcss: "^3.2.7",
|
||||
},
|
||||
}),
|
||||
[entryFile],
|
||||
);
|
||||
const customSetup = {
|
||||
dependencies,
|
||||
entry: entryFile,
|
||||
devDependencies: {
|
||||
autoprefixer: "^10.4.14",
|
||||
postcss: "^8.4.21",
|
||||
tailwindcss: "^3.2.7",
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
customSetup,
|
||||
files: {
|
||||
...sortedFiles,
|
||||
[entryFile]: {
|
||||
code: defaultEntry,
|
||||
code: getFileEntry(theme ?? "light"),
|
||||
hidden: true,
|
||||
},
|
||||
"tailwind.config.js": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user