Compare commits
444 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51095b1628 | |||
| a2b8421441 | |||
| eccc2f2f3d | |||
| 2bc1d1a58b | |||
| 183a4a6cfd | |||
| a644544dfb | |||
| 9e5dd8ce37 | |||
| 69f713cb05 | |||
| d820d9397a | |||
| f89356691c | |||
| 6b56e43a35 | |||
| 4ca6e2c1f6 | |||
| 2126ea1863 | |||
| cadbb30cfb | |||
| f728c0542c | |||
| 74eda31288 | |||
| 158c2aa004 | |||
| ccd8f730e1 | |||
| 098d7884cf | |||
| 60c61aaf0c | |||
| fdbfa1f299 | |||
| ed1dbbc172 | |||
| 9cdc262619 | |||
| c0d0a57338 | |||
| 651c8b55af | |||
| 3a22505185 | |||
| dc0bcf13a5 | |||
| aab1f19a96 | |||
| 9873c6fb83 | |||
| af877f7ddb | |||
| 25640e42ff | |||
| abf532b548 | |||
| e458432550 | |||
| c2ef4c0935 | |||
| a1c112431f | |||
| 4dd2605ac2 | |||
| dab0534095 | |||
| 12bf410807 | |||
| 09504cd702 | |||
| 86a78c9b91 | |||
| feab3e6c1d | |||
| eb11a7731a | |||
| 2829d4afae | |||
| ebab10804c | |||
| 6da144f459 | |||
| 6811ce52c3 | |||
| 2e49e08315 | |||
| a60c2d7b22 | |||
| c5049edfde | |||
| 410e30c720 | |||
| 82a9fd9bc4 | |||
| ef6ea6c1ff | |||
| c188e4d628 | |||
| a597c82a9c | |||
| 0359bf3084 | |||
| eb51bf2261 | |||
| b14067bb94 | |||
| 8761168d34 | |||
| cf0d4e471e | |||
| 2aa476455f | |||
| ee7a819147 | |||
| fb0625470f | |||
| 2b9f89023a | |||
| 26a3a7d2c6 | |||
| 888d861841 | |||
| 0973cd9bff | |||
| e67b3af223 | |||
| 52dafd08f1 | |||
| ceeb52363d | |||
| 0d9ea4925b | |||
| ea48ac2d08 | |||
| b0ee8e3fc2 | |||
| 5d84634c0a | |||
| 9496ab57c5 | |||
| 5ea479f6c4 | |||
| cda881799c | |||
| 707a61de4c | |||
| 5a8768a4b2 | |||
| a25292215a | |||
| a05aef0acb | |||
| 2894aecca1 | |||
| a702bb869d | |||
| 0443ec1c87 | |||
| 7263daca08 | |||
| e4b3c7d1a1 | |||
| a686262009 | |||
| f373400102 | |||
| 2c3be59dfb | |||
| 798e84e729 | |||
| 139b28437b | |||
| f2205642f4 | |||
| 0a10de8d08 | |||
| bae544d198 | |||
| feafb36fcc | |||
| 65f48135d6 | |||
| 1d32bf8f1e | |||
| 0a9982d3ef | |||
| f864dc3974 | |||
| 3a0f2c5610 | |||
| a030680e6a | |||
| e12f3faa81 | |||
| 2dc5adb6e0 | |||
| f63b20c59d | |||
| 0df5099a62 | |||
| cdc4981508 | |||
| acee3c2fda | |||
| fa72bb45cc | |||
| 265c121d65 | |||
| d6b42f757a | |||
| d8b0ef528b | |||
| e83595e06f | |||
| 141887d38c | |||
| a235e324b9 | |||
| 9b27da544e | |||
| 5528ccd042 | |||
| 4957f56e86 | |||
| fa27bccb37 | |||
| 0cc8c79c53 | |||
| 5be104f72d | |||
| dc289fb59b | |||
| 0ce10269c2 | |||
| cd3b80974c | |||
| dc91ab8bc9 | |||
| dec7d411b5 | |||
| 37bed2368e | |||
| 755a835fa7 | |||
| 6f27b048d0 | |||
| dc9362f959 | |||
| e6f36281cb | |||
| f75174d07e | |||
| ac0e564b34 | |||
| 60e453d265 | |||
| 6e5ba692db | |||
| e4ba43b4e3 | |||
| 995b37e2eb | |||
| 3be8c621a1 | |||
| 0b5cde7bc4 | |||
| a4cab30df3 | |||
| 9ff61b5d54 | |||
| 7a3015c497 | |||
| 2106d987f2 | |||
| 2934a90d41 | |||
| 39714775c7 | |||
| 07372eaa65 | |||
| dedd42c288 | |||
| f28e93d940 | |||
| 0ad6e033f8 | |||
| fb0707087c | |||
| d953a22b99 | |||
| 077c19506f | |||
| 6ca97c0bd6 | |||
| 5c8f21a106 | |||
| decf11d270 | |||
| fff248a30b | |||
| 4996d9fb36 | |||
| a5d7fcff0e | |||
| e8f5699fe2 | |||
| a343e8e2ce | |||
| a95e06acf2 | |||
| 22f2665b00 | |||
| 921746f284 | |||
| 132efbcb6d | |||
| bea1e1fde8 | |||
| 95ffa64d41 | |||
| e551226ff2 | |||
| f3d0d6dded | |||
| 1c77b33e82 | |||
| 0d7a19693b | |||
| 01aedcf09b | |||
| 436ba1cd84 | |||
| 0ba165f0fd | |||
| 088af8b6df | |||
| 6951e262e5 | |||
| 51c54f118f | |||
| 11fc733b36 | |||
| a8dc5b429a | |||
| 12cebcfdac | |||
| 0fb2160899 | |||
| 09380c5b06 | |||
| 0f5cce5d62 | |||
| 64c89bc57d | |||
| 467383b8a2 | |||
| aab1dbf77c | |||
| 36775dceee | |||
| 8628ba04a7 | |||
| a978687b07 | |||
| 57cafa06d0 | |||
| d2bd008ba3 | |||
| 2f87b8d644 | |||
| 6ecdc278ab | |||
| 9189b3fbf2 | |||
| 1149345cae | |||
| 135cc21e6f | |||
| 7f6218b978 | |||
| 0eb9f1e3fe | |||
| 11ef4365da | |||
| 85a820eeff | |||
| 6a6d426b10 | |||
| a3cbb5997b | |||
| a5cc42bf80 | |||
| e728a8967b | |||
| acba2cf8f5 | |||
| 049db57bb2 | |||
| 5296d9b84d | |||
| 1619a35cf9 | |||
| 8106629dab | |||
| d55f52cc35 | |||
| 78198c32be | |||
| 71807e8831 | |||
| 9af6174dcf | |||
| 1a129db6fe | |||
| 44ed1056e7 | |||
| db0e26a003 | |||
| 38af48faf5 | |||
| 5b60840751 | |||
| 4c56839db0 | |||
| ef61a3fe09 | |||
| bc4927c901 | |||
| 4191feab2a | |||
| fe8647bd1d | |||
| 23992572ea | |||
| e7a4711d9b | |||
| e84158db62 | |||
| f59e8f6bac | |||
| 73dfb7fc93 | |||
| df9f6f5f58 | |||
| 1fd5049f5a | |||
| a386e1cadd | |||
| 3172a803e3 | |||
| dfc8bdc7af | |||
| 67ce4beb28 | |||
| 5a4a60be65 | |||
| 25e86fb417 | |||
| a95bd6558d | |||
| 080718e265 | |||
| 8217639fd4 | |||
| 7abd68941a | |||
| c3f762563b | |||
| 8d2192183b | |||
| 8b86f83802 | |||
| f38059aa44 | |||
| 311ceb0c3a | |||
| 50a9bf54d4 | |||
| 99d4d296d7 | |||
| 9245384ac3 | |||
| 5af4b10114 | |||
| 8de357cfac | |||
| 655599982d | |||
| 3aac9bac26 | |||
| 95c8fdaa2e | |||
| c0706acbca | |||
| 9d4c7ed9cf | |||
| 9ebaae083d | |||
| c9c5564c58 | |||
| d79808dbe1 | |||
| 5a9b78d234 | |||
| 9f4bdf352d | |||
| d62feb08dc | |||
| 62d307117a | |||
| 01a27293eb | |||
| a53e274d5b | |||
| 425a034bca | |||
| a5a1ea5ade | |||
| f6531c5f60 | |||
| 37d5f12162 | |||
| dd97fc3c4f | |||
| ceddd0d1d9 | |||
| bb8ed58749 | |||
| bbbdcbdfdf | |||
| bc424948c7 | |||
| cdc30db14c | |||
| 3aa86423aa | |||
| 59ed34b3de | |||
| f58fc9bb69 | |||
| 9a478b6c1f | |||
| 7fd0327657 | |||
| 05c966e8a4 | |||
| 5b014b76ff | |||
| cf32b5ae8b | |||
| 4452ed66c0 | |||
| b1b30b7976 | |||
| 97ef2d7250 | |||
| 8b39989090 | |||
| 8363a3ecfc | |||
| 5c30e04811 | |||
| 8606fbe0b9 | |||
| 1612532eee | |||
| a3be419cb3 | |||
| 72a7f71377 | |||
| 7c8341035d | |||
| 6ca0ce309a | |||
| acc2ff3913 | |||
| 4db10a47e9 | |||
| a9c4fab011 | |||
| d61428d9e6 | |||
| 455b788924 | |||
| 79cfdbd8ff | |||
| 7d294e3a04 | |||
| 43906f639e | |||
| 8fcc8b3767 | |||
| 28477447d3 | |||
| fc3c6b9431 | |||
| 59fda3772f | |||
| d8d2b87cb8 | |||
| 64571e468c | |||
| faf26acd23 | |||
| cac56faf29 | |||
| 09fe1d3141 | |||
| a9e324b351 | |||
| 043b8420cf | |||
| 57909accde | |||
| 7a17256268 | |||
| bf2bc265ee | |||
| cf6bf749a1 | |||
| 641bf0885b | |||
| 40b42001f9 | |||
| 789a7209a2 | |||
| 021aa16d96 | |||
| eb8a1fcc01 | |||
| 9b42a2fca1 | |||
| 5702287e56 | |||
| 290cce15be | |||
| 27bde30839 | |||
| 6be3bc3516 | |||
| e4113b05ce | |||
| a3a95e7196 | |||
| 8731159702 | |||
| b6adb2ae8c | |||
| cd2d9b70a9 | |||
| a371e1a5fa | |||
| 5cdc4d1d87 | |||
| 22c6e070dd | |||
| 0714192838 | |||
| ae9f300d45 | |||
| a16dac04c8 | |||
| 94db001961 | |||
| d77cf7658b | |||
| 46c47f7e6e | |||
| 35b9741663 | |||
| 2b82ba86eb | |||
| 904f538779 | |||
| 609e9f4c11 | |||
| e3dabac9f5 | |||
| 4e94c11528 | |||
| a17b6c7502 | |||
| bd72301b01 | |||
| cc839cdd1f | |||
| c9e5c2ef7f | |||
| e8f5264cf8 | |||
| df7f773f3d | |||
| af925d56d1 | |||
| baec55029d | |||
| ea2868e58e | |||
| 691f380dff | |||
| ac8afec964 | |||
| 5fe5521a7c | |||
| 15403bc67e | |||
| c186f37c98 | |||
| 1bc2a50e2c | |||
| 453f792a7a | |||
| 3f6fd48eee | |||
| d7c6c79f38 | |||
| f068552774 | |||
| 6d7372ede1 | |||
| 1487219a1c | |||
| 6e7e834c2d | |||
| 80a8f7e94c | |||
| fae7467b64 | |||
| 4e7789c1f6 | |||
| 6694b934e6 | |||
| 4d341081e3 | |||
| df562449fb | |||
| 4063362699 | |||
| 94d65df533 | |||
| 8f66c6b564 | |||
| 4a19a3c906 | |||
| 0be398ade9 | |||
| 50d23e5378 | |||
| d0341020e6 | |||
| 4170ecdc76 | |||
| e2b0076b53 | |||
| e2b0497b07 | |||
| df3b309542 | |||
| 6c35a502fe | |||
| a629f8c088 | |||
| fd4e01dcfa | |||
| e9c08abd55 | |||
| 3e263aa882 | |||
| 9add9aba97 | |||
| 0311ae43f1 | |||
| 79c5fb9a94 | |||
| abe1ba58de | |||
| e6c3747153 | |||
| 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 |
@@ -10,6 +10,9 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"onlyUpdatePeerDependentsWhenOutOfRange": true
|
||||
},
|
||||
"ignore": [
|
||||
"@nextui-org/docs",
|
||||
"@nextui-org/storybook"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
language: "en"
|
||||
early_access: false
|
||||
reviews:
|
||||
high_level_summary: true
|
||||
poem: false
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
ignore_title_keywords:
|
||||
- "WIP"
|
||||
- "DO NOT MERGE"
|
||||
- 'ci(changesets)'
|
||||
drafts: false
|
||||
base_branches:
|
||||
- "main"
|
||||
- "develop"
|
||||
- "fix/.*"
|
||||
- "chore/.*"
|
||||
- "feat/.*"
|
||||
chat:
|
||||
auto_reply: true
|
||||
+1
-1
@@ -3,7 +3,7 @@ const conventional = require("@commitlint/config-conventional");
|
||||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
plugins: ["commitlint-plugin-function-rules"],
|
||||
helpUrl: "https://github.com/nextui-org/nextui/blob/main/CONTRIBUTING.MD#commit-convention",
|
||||
helpUrl: "https://github.com/nextui-org/nextui/blob/main/CONTRIBUTING.md#commit-convention",
|
||||
rules: {
|
||||
...conventional.rules,
|
||||
"type-enum": [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.now/*
|
||||
.next/*
|
||||
*.css
|
||||
.changeset
|
||||
dist
|
||||
esm/*
|
||||
public/*
|
||||
|
||||
@@ -21,6 +21,16 @@ body:
|
||||
- NextUI's [Discussions tab](https://github.com/nextui-org/nextui/discussions)
|
||||
|
||||
The more information you fill in, the better the community can help you.
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: NextUI Version
|
||||
description: |
|
||||
Please provide the version of NextUI you are using.
|
||||
You can find the version number in the package.json file.
|
||||
placeholder: ex. 2.0.10
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
||||
@@ -33,6 +33,7 @@ jobs:
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
publish: pnpm release
|
||||
title: "ci(changesets): :package: version packages"
|
||||
commit: "ci(changesets): version packages"
|
||||
setupGitUser: false
|
||||
env:
|
||||
@@ -48,4 +49,4 @@ jobs:
|
||||
pnpm release:dev
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -20,6 +20,7 @@ coverage
|
||||
dist/
|
||||
storybook-static
|
||||
/packages/storybook/public/tailwind.css
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
+2
-2
@@ -13,12 +13,12 @@ const removeIgnoredFiles = async (files) => {
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
"**/*.{cjs,mjs,js,ts,jsx,tsx}": async (files) => {
|
||||
"**/*.{js,ts,jsx,tsx}": async (files) => {
|
||||
const filesToLint = await removeIgnoredFiles(files);
|
||||
|
||||
return [`eslint -c .eslintrc.json --max-warnings=0 --fix ${filesToLint}`];
|
||||
},
|
||||
"**/*.{css,json,md}": async (files) => {
|
||||
"**/*.css": async (files) => {
|
||||
const filesToLint = await removeIgnoredFiles(files);
|
||||
|
||||
return [`prettier --config .prettierrc.json --ignore-path --write ${filesToLint}`];
|
||||
|
||||
@@ -2,6 +2,7 @@ dist
|
||||
node_modules
|
||||
plop
|
||||
coverage
|
||||
.changeset
|
||||
.next
|
||||
build
|
||||
scripts
|
||||
|
||||
Vendored
+1
-1
@@ -2,6 +2,6 @@
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,12 +118,13 @@ We use [Turbo Repo](https://turborepo.org/) for the project management.
|
||||
|
||||
```bash
|
||||
## Start the dev babel server of NextUI core components
|
||||
pnpm dev:nextui
|
||||
pnpm dev
|
||||
|
||||
## optional
|
||||
pnpm dev:docs ## this will start the documentation next.js server and it will automatically detect the changes in the components.
|
||||
|
||||
pnpm start:sb ## this will start the storybook server for a faster development and testing.
|
||||
pnpm sb ## this will start the storybook server for a faster development and testing.
|
||||
|
||||
pnpm dev:docs ## this will start the documentation next.js server and it will automatically detect the changes in the components.
|
||||
```
|
||||
|
||||
- If you will be working just on the documentation source code / mdx, you can use the following commands to build
|
||||
@@ -131,7 +132,7 @@ pnpm start:sb ## this will start the storybook server for a faster development a
|
||||
|
||||
```bash
|
||||
## Build NextUI source components
|
||||
pnpm build:nextui
|
||||
pnpm build
|
||||
|
||||
## Start the next.js documentation dev server
|
||||
pnpm dev:docs
|
||||
@@ -141,10 +142,6 @@ pnpm dev:docs
|
||||
|
||||
```bash
|
||||
pnpm sb
|
||||
|
||||
#or
|
||||
|
||||
pnpm start:sb
|
||||
```
|
||||
|
||||
Remember that these commands must be executed in the root folder of the project.
|
||||
@@ -166,23 +163,40 @@ git checkout -b fix/something
|
||||
All commits that fix bugs or add features need a test.
|
||||
You can run the nest command for component specific tests.
|
||||
|
||||
```
|
||||
```bash
|
||||
# Test current code
|
||||
pnpm test:update # or npm run test:update
|
||||
|
||||
pnpm test
|
||||
|
||||
# or
|
||||
|
||||
npm run test
|
||||
```
|
||||
|
||||
```bash
|
||||
# Test isolated component code
|
||||
pnpm test:update src/button # or npm run test:update src/button
|
||||
|
||||
pnpm test button
|
||||
|
||||
# or
|
||||
|
||||
npm run test button
|
||||
|
||||
```
|
||||
|
||||
5. Be sure the package builds.
|
||||
|
||||
```
|
||||
```bash
|
||||
# Build current code
|
||||
pnpm build # or npm run build
|
||||
|
||||
pnpm build
|
||||
|
||||
# or
|
||||
|
||||
npm run build
|
||||
```
|
||||
|
||||
> Note: ensure your version of Node is 14 or higher to run scripts
|
||||
> Note: ensure your version of Node is 16 or higher to run scripts
|
||||
|
||||
6. Send your pull request:
|
||||
|
||||
@@ -12,4 +12,8 @@ IS_PREVIEW=true/false
|
||||
ANALYZE_BUNDLE=true/false
|
||||
|
||||
# Vercel preview env (is used for taking the docs directly from the project files)
|
||||
NEXT_PUBLIC_PREVIEW=true/false
|
||||
NEXT_PUBLIC_PREVIEW=true/false
|
||||
|
||||
## Featurebase
|
||||
NEXT_PUBLIC_FB_FEEDBACK_ORG=
|
||||
NEXT_PUBLIC_FB_FEEDBACK_URL=
|
||||
@@ -202,7 +202,7 @@
|
||||
"email": null,
|
||||
"twitter": null,
|
||||
"github": "https://github.com/HighError",
|
||||
"website": "https://higherror.github.io/"
|
||||
"website": null
|
||||
},
|
||||
{
|
||||
"MemberId": 409558,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {format, parseISO} from "date-fns";
|
||||
import NextLink from "next/link";
|
||||
import {Balancer} from "react-wrap-balancer";
|
||||
|
||||
import {__DEV__, __PREVIEW__} from "@/utils";
|
||||
import {MDXContent} from "@/components/mdx-content";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {Route} from "@/libs/docs/page";
|
||||
@@ -18,6 +19,8 @@ interface BlogPostProps {
|
||||
};
|
||||
}
|
||||
|
||||
const isDraftVisible = __DEV__ || __PREVIEW__;
|
||||
|
||||
async function getBlogPostFromParams({params}: BlogPostProps) {
|
||||
const slug = params.slug || "";
|
||||
const post = allBlogPosts.find((post) => post.slugAsParams === slug);
|
||||
@@ -78,7 +81,7 @@ export async function generateStaticParams(): Promise<BlogPostProps["params"][]>
|
||||
export default async function DocPage({params}: BlogPostProps) {
|
||||
const {post} = await getBlogPostFromParams({params});
|
||||
|
||||
if (!post) {
|
||||
if (!post || (post.draft && !isDraftVisible)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -96,6 +99,7 @@ export default async function DocPage({params}: BlogPostProps) {
|
||||
<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>
|
||||
@@ -119,6 +123,7 @@ export default async function DocPage({params}: BlogPostProps) {
|
||||
</div>
|
||||
<h1 className="mb-2 font-bold text-4xl">
|
||||
<Balancer>{post.title}</Balancer>
|
||||
<strong className="text-default-300">{post?.draft && " (Draft)"}</strong>
|
||||
</h1>
|
||||
<MDXContent code={post.body.code} />
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,20 @@ import {allBlogPosts} from "contentlayer/generated";
|
||||
import {compareDesc} from "date-fns";
|
||||
|
||||
import {BlogPostList} from "@/components/blog-post";
|
||||
import {__DEV__, __PREVIEW__} from "@/utils";
|
||||
|
||||
const isDraftVisible = __DEV__ || __PREVIEW__;
|
||||
|
||||
export default function Blog() {
|
||||
const posts = allBlogPosts.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)));
|
||||
const posts = allBlogPosts
|
||||
.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)))
|
||||
?.filter((post) => {
|
||||
if (post.draft && !isDraftVisible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full lg:px-16 mt-12">
|
||||
|
||||
@@ -83,7 +83,7 @@ export default async function DocPage({params}: DocPageProps) {
|
||||
if (!doc) {
|
||||
notFound();
|
||||
}
|
||||
const editUrl = `${GITHUB_URL}/${REPO_NAME}/edit/${TAG}/${CONTENT_PATH}${currentRoute?.path}`;
|
||||
const editUrl = `${GITHUB_URL}/${REPO_NAME}/edit/${TAG}${CONTENT_PATH}${currentRoute?.path}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function DocsLayout({children}: DocsLayoutProps) {
|
||||
<>
|
||||
<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">
|
||||
<div className="hidden overflow-visible relative z-10 lg:block lg:col-span-2 mt-8 pr-4">
|
||||
<DocsSidebar routes={manifest.routes} />
|
||||
</div>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable no-console */
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, filterText}) {
|
||||
let res = await fetch(`https://swapi.py4e.com/api/people/?search=${filterText}`, {signal});
|
||||
let json = await res.json();
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
inputValue={list.filterText}
|
||||
isLoading={list.isLoading}
|
||||
items={list.items}
|
||||
label="Select a character"
|
||||
placeholder="Type to search..."
|
||||
variant="bordered"
|
||||
onInputChange={list.setFilterText}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.name} className="capitalize">
|
||||
{item.name}
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/* eslint-disable no-console */
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";
|
||||
|
||||
type Pokemon = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type UsePokemonListProps = {
|
||||
/** Delay to wait before fetching more items */
|
||||
fetchDelay?: number;
|
||||
};
|
||||
|
||||
function usePokemonList({fetchDelay = 0}: UsePokemonListProps = {}) {
|
||||
const [items, setItems] = React.useState<Pokemon[]>([]);
|
||||
const [hasMore, setHasMore] = React.useState(true);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const limit = 10; // Number of items per page, adjust as necessary
|
||||
|
||||
const loadPokemon = async (currentOffset: number) => {
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (offset > 0) {
|
||||
// Delay to simulate network latency
|
||||
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
|
||||
}
|
||||
|
||||
let res = await fetch(
|
||||
`https://pokeapi.co/api/v2/pokemon?offset=${currentOffset}&limit=${limit}`,
|
||||
{signal},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
let json = await res.json();
|
||||
|
||||
setHasMore(json.next !== null);
|
||||
// Append new results to existing ones
|
||||
setItems((prevItems) => [...prevItems, ...json.results]);
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Fetch aborted");
|
||||
} else {
|
||||
console.error("There was an error with the fetch operation:", error);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
loadPokemon(offset);
|
||||
}, []);
|
||||
|
||||
const onLoadMore = () => {
|
||||
const newOffset = offset + limit;
|
||||
|
||||
setOffset(newOffset);
|
||||
loadPokemon(newOffset);
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
hasMore,
|
||||
isLoading,
|
||||
onLoadMore,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500});
|
||||
|
||||
const [, scrollerRef] = useInfiniteScroll({
|
||||
hasMore,
|
||||
isEnabled: isOpen,
|
||||
shouldUseLoader: false, // We don't want to show the loader at the bottom of the list
|
||||
onLoadMore,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
defaultItems={items}
|
||||
isLoading={isLoading}
|
||||
label="Pick a Pokemon"
|
||||
placeholder="Select a Pokemon"
|
||||
scrollRef={scrollerRef}
|
||||
variant="bordered"
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.name} className="capitalize">
|
||||
{item.name}
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {Autocomplete, AutocompleteItem, MenuTriggerAction} from "@nextui-org/react";
|
||||
import {useFilter} from "@react-aria/i18n";
|
||||
|
||||
const animals = [
|
||||
{label: "Cat", value: "cat", description: "The second most popular pet in the world"},
|
||||
{label: "Dog", value: "dog", description: "The most popular pet in the world"},
|
||||
{label: "Elephant", value: "elephant", description: "The largest land animal"},
|
||||
{label: "Lion", value: "lion", description: "The king of the jungle"},
|
||||
{label: "Tiger", value: "tiger", description: "The largest cat species"},
|
||||
{label: "Giraffe", value: "giraffe", description: "The tallest land animal"},
|
||||
{
|
||||
label: "Dolphin",
|
||||
value: "dolphin",
|
||||
description: "A widely distributed and diverse group of aquatic mammals",
|
||||
},
|
||||
{label: "Penguin", value: "penguin", description: "A group of aquatic flightless birds"},
|
||||
{label: "Zebra", value: "zebra", description: "A several species of African equids"},
|
||||
{
|
||||
label: "Shark",
|
||||
value: "shark",
|
||||
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
|
||||
},
|
||||
{
|
||||
label: "Whale",
|
||||
value: "whale",
|
||||
description: "Diverse group of fully aquatic placental marine mammals",
|
||||
},
|
||||
{label: "Otter", value: "otter", description: "A carnivorous mammal in the subfamily Lutrinae"},
|
||||
{label: "Crocodile", value: "crocodile", description: "A large semiaquatic reptile"},
|
||||
];
|
||||
|
||||
type FieldState = {
|
||||
selectedKey: React.Key | null;
|
||||
inputValue: string;
|
||||
items: typeof animals;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
// Store ComboBox input value, selected option, open state, and items
|
||||
// in a state tracker
|
||||
const [fieldState, setFieldState] = React.useState<FieldState>({
|
||||
selectedKey: "",
|
||||
inputValue: "",
|
||||
items: animals,
|
||||
});
|
||||
|
||||
// Implement custom filtering logic and control what items are
|
||||
// available to the Autocomplete.
|
||||
const {startsWith} = useFilter({sensitivity: "base"});
|
||||
|
||||
// Specify how each of the Autocomplete values should change when an
|
||||
// option is selected from the list box
|
||||
const onSelectionChange = (key: React.Key) => {
|
||||
setFieldState((prevState) => {
|
||||
let selectedItem = prevState.items.find((option) => option.value === key);
|
||||
|
||||
return {
|
||||
inputValue: selectedItem?.label || "",
|
||||
selectedKey: key,
|
||||
items: animals.filter((item) => startsWith(item.label, selectedItem?.label || "")),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Specify how each of the Autocomplete values should change when the input
|
||||
// field is altered by the user
|
||||
const onInputChange = (value: string) => {
|
||||
setFieldState((prevState) => ({
|
||||
inputValue: value,
|
||||
selectedKey: value === "" ? null : prevState.selectedKey,
|
||||
items: animals.filter((item) => startsWith(item.label, value)),
|
||||
}));
|
||||
};
|
||||
|
||||
// Show entire list if user opens the menu manually
|
||||
const onOpenChange = (isOpen: boolean, menuTrigger: MenuTriggerAction) => {
|
||||
if (menuTrigger === "manual" && isOpen) {
|
||||
setFieldState((prevState) => ({
|
||||
inputValue: prevState.inputValue,
|
||||
selectedKey: prevState.selectedKey,
|
||||
items: animals,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
inputValue={fieldState.inputValue}
|
||||
items={fieldState.items}
|
||||
label="Favorite Animal"
|
||||
placeholder="Select an animal"
|
||||
selectedKey={fieldState.selectedKey}
|
||||
variant="bordered"
|
||||
onInputChange={onInputChange}
|
||||
onOpenChange={onOpenChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -44,12 +44,12 @@ export default function Page() {
|
||||
];
|
||||
|
||||
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">
|
||||
<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>
|
||||
@@ -57,17 +57,13 @@ export default function Page() {
|
||||
</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="#">
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
@@ -77,25 +73,23 @@ export default function Page() {
|
||||
</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">
|
||||
<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 ? "warning" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
|
||||
@@ -28,6 +28,8 @@ const AcmeLogo = () => (
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
|
||||
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
@@ -42,12 +44,12 @@ export default function Page() {
|
||||
];
|
||||
|
||||
return (
|
||||
<Navbar disableAnimation isBordered>
|
||||
<NavbarContent className="sm:hidden" justify="start">
|
||||
<NavbarMenuToggle />
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="sm:hidden pr-3" justify="center">
|
||||
<Navbar disableAnimation onMenuOpenChange={setIsMenuOpen}>
|
||||
<NavbarContent>
|
||||
<NavbarMenuToggle
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="sm:hidden"
|
||||
/>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
@@ -55,17 +57,13 @@ export default function Page() {
|
||||
</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="#">
|
||||
<Link aria-current="page" href="#">
|
||||
Customers
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
@@ -75,25 +73,23 @@ export default function Page() {
|
||||
</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">
|
||||
<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 ? "warning" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
|
||||
@@ -61,7 +61,8 @@ export default function Page() {
|
||||
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",
|
||||
inputWrapper:
|
||||
"h-full font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20",
|
||||
}}
|
||||
placeholder="Type to search..."
|
||||
startContent={<SearchLinearIcon size={18} />}
|
||||
|
||||
@@ -9,10 +9,17 @@ import {
|
||||
Textarea,
|
||||
Input,
|
||||
Tab,
|
||||
Avatar,
|
||||
Select,
|
||||
SelectItem,
|
||||
AccordionItem,
|
||||
Pagination,
|
||||
extendVariants,
|
||||
PaginationItem,
|
||||
} from "@nextui-org/react";
|
||||
import {useState} from "react";
|
||||
import {useFilter} from "@react-aria/i18n";
|
||||
import {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {useSearchParams} from "next/navigation";
|
||||
|
||||
import {SearchLinearIcon} from "@/components/icons";
|
||||
|
||||
@@ -117,15 +124,15 @@ const MyInput = extendVariants(Input, {
|
||||
},
|
||||
size: {
|
||||
xs: {
|
||||
inputWrapper: "h-unit-6 min-h-unit-6 px-1",
|
||||
inputWrapper: "h-6 min-h-6 px-1",
|
||||
input: "text-tiny",
|
||||
},
|
||||
md: {
|
||||
inputWrapper: "h-unit-10 min-h-unit-10",
|
||||
inputWrapper: "h-10 min-h-10",
|
||||
input: "text-small",
|
||||
},
|
||||
xl: {
|
||||
inputWrapper: "h-unit-14 min-h-unit-14",
|
||||
inputWrapper: "h-14 min-h-14",
|
||||
input: "text-medium",
|
||||
},
|
||||
},
|
||||
@@ -175,16 +182,302 @@ const MyButton2 = extendVariants(Button, {
|
||||
xl: "px-12 py-6 text-xl",
|
||||
},
|
||||
},
|
||||
defaultVariants: {},
|
||||
defaultVariants: {
|
||||
color: "foreground",
|
||||
},
|
||||
});
|
||||
|
||||
const usersData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Tony Reichert",
|
||||
role: "CEO",
|
||||
team: "Management",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/1.png",
|
||||
email: "tony.reichert@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Zoey Lang",
|
||||
role: "Tech Lead",
|
||||
team: "Development",
|
||||
status: "paused",
|
||||
age: "25",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/1.png",
|
||||
email: "zoey.lang@example.com",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Jane Fisher",
|
||||
role: "Sr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/2.png",
|
||||
email: "jane.fisher@example.com",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "William Howard",
|
||||
role: "C.M.",
|
||||
team: "Marketing",
|
||||
status: "vacation",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/2.png",
|
||||
email: "william.howard@example.com",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kristen Copper",
|
||||
role: "S. Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "24",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/3.png",
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/3.png",
|
||||
email: "brian.kim@example.com",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Michael Hunt",
|
||||
role: "Designer",
|
||||
team: "Design",
|
||||
status: "paused",
|
||||
age: "27",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/4.png",
|
||||
email: "michael.hunt@example.com",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Samantha Brooks",
|
||||
role: "HR Manager",
|
||||
team: "HR",
|
||||
status: "active",
|
||||
age: "31",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/4.png",
|
||||
email: "samantha.brooks@example.com",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Frank Harrison",
|
||||
role: "F. Manager",
|
||||
team: "Finance",
|
||||
status: "vacation",
|
||||
age: "33",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/5.png",
|
||||
email: "frank.harrison@example.com",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Emma Adams",
|
||||
role: "Ops Manager",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "35",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/5.png",
|
||||
email: "emma.adams@example.com",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Brandon Stevens",
|
||||
role: "Jr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/7.png",
|
||||
email: "brandon.stevens@example.com",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Megan Richards",
|
||||
role: "P. Manager",
|
||||
team: "Product",
|
||||
status: "paused",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/7.png",
|
||||
email: "megan.richards@example.com",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Oliver Scott",
|
||||
role: "S. Manager",
|
||||
team: "Security",
|
||||
status: "active",
|
||||
age: "37",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/8.png",
|
||||
email: "oliver.scott@example.com",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Grace Allen",
|
||||
role: "M. Specialist",
|
||||
team: "Marketing",
|
||||
status: "active",
|
||||
age: "30",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/8.png",
|
||||
email: "grace.allen@example.com",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Noah Carter",
|
||||
role: "IT Specialist",
|
||||
team: "I. Technology",
|
||||
status: "paused",
|
||||
age: "31",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/9.png",
|
||||
email: "noah.carter@example.com",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Ava Perez",
|
||||
role: "Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/9.png",
|
||||
email: "ava.perez@example.com",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Liam Johnson",
|
||||
role: "Data Analyst",
|
||||
team: "Analysis",
|
||||
status: "active",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/11.png",
|
||||
email: "liam.johnson@example.com",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Sophia Taylor",
|
||||
role: "QA Analyst",
|
||||
team: "Testing",
|
||||
status: "active",
|
||||
age: "27",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/11.png",
|
||||
email: "sophia.taylor@example.com",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Lucas Harris",
|
||||
role: "Administrator",
|
||||
team: "Information Technology",
|
||||
status: "paused",
|
||||
age: "32",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/12.png",
|
||||
email: "lucas.harris@example.com",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Mia Robinson",
|
||||
role: "Coordinator",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "26",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/12.png",
|
||||
email: "mia.robinson@example.com",
|
||||
},
|
||||
];
|
||||
|
||||
export default function NextUIPerf() {
|
||||
const [textA, setTextA] = useState<string | undefined>("");
|
||||
const [textB, setTextB] = useState<string | undefined>("");
|
||||
const [textC, setTextC] = useState<string | undefined>("");
|
||||
const [textA, setTextA] = useState<string>("");
|
||||
const [textB, setTextB] = useState<string>("");
|
||||
const [textC, setTextC] = useState<string>("");
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [inputValue, setInputValue] = useState<string>();
|
||||
const [selectedKey, setSelectedKey] = useState<string>("");
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const page = Number(searchParams.get("page"));
|
||||
|
||||
let {startsWith} = useFilter({sensitivity: "base"});
|
||||
|
||||
const filteredItems = inputValue
|
||||
? usersData.filter((item) => startsWith(item.name, inputValue))
|
||||
: usersData;
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
isOpen && inputRef?.current?.focus();
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSelectionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedKey(e.target.value);
|
||||
};
|
||||
|
||||
const topContent = useMemo(() => {
|
||||
return (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
isClearable
|
||||
aria-activedescendant={selectedKey}
|
||||
aria-expanded={isOpen}
|
||||
aria-label="Search user"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="z-10 sticky top-1"
|
||||
placeholder="Search..."
|
||||
spellCheck={false}
|
||||
startContent={<SearchLinearIcon className="text-default-400" size={18} strokeWidth="2" />}
|
||||
type="text"
|
||||
onValueChange={setInputValue}
|
||||
/>
|
||||
);
|
||||
}, [inputRef, selectedKey, isOpen]);
|
||||
|
||||
return (
|
||||
<div className="w-full p-24 gap-4 flex flex-col">
|
||||
<Select
|
||||
classNames={{
|
||||
base: "max-w-xs",
|
||||
listboxWrapper: "scroll-pb-6 scroll-pt-28",
|
||||
}}
|
||||
items={filteredItems}
|
||||
label="Assigned to"
|
||||
labelPlacement="outside"
|
||||
listboxProps={{
|
||||
topContent,
|
||||
variant: "flat",
|
||||
classNames: {
|
||||
base: [
|
||||
"before:content-[''] before:rounded-t-medium before:fixed before:w-full before:h-14 before:z-10",
|
||||
"before:top-0 before:left-0 before:bg-gradient-to-b before:from-default-50",
|
||||
],
|
||||
},
|
||||
}}
|
||||
placeholder="Select a user"
|
||||
selectedKeys={[selectedKey]}
|
||||
showScrollIndicators={false}
|
||||
variant="flat"
|
||||
onChange={handleSelectionChange}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{(item) => (
|
||||
<SelectItem key={item.id} textValue={item.name}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar alt={item.name} className="flex-shrink-0" size="sm" src={item.avatar} />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-small">{item.name}</span>
|
||||
<span className="text-tiny text-default-400">{item.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
|
||||
<Accordion>
|
||||
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
|
||||
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
|
||||
@@ -253,7 +546,16 @@ export default function NextUIPerf() {
|
||||
|
||||
<Button>Click Me!</Button>
|
||||
|
||||
<MyButton2 color="foreground">Press Me!</MyButton2>
|
||||
<MyButton2 color="primary">Press Me!</MyButton2>
|
||||
|
||||
<Pagination
|
||||
showControls
|
||||
initialPage={page ?? 1}
|
||||
renderItem={({page, ...itemProps}) => {
|
||||
return <PaginationItem href={`/examples/perf?page=${page}`} {...itemProps} />;
|
||||
}}
|
||||
total={10}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/* eslint-disable no-console */
|
||||
"use client";
|
||||
|
||||
import {Select, SelectItem} from "@nextui-org/react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";
|
||||
|
||||
type Pokemon = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type UsePokemonListProps = {
|
||||
/** Delay to wait before fetching more items */
|
||||
fetchDelay?: number;
|
||||
};
|
||||
|
||||
function usePokemonList({fetchDelay = 0}: UsePokemonListProps = {}) {
|
||||
const [items, setItems] = useState<Pokemon[]>([]);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const limit = 10; // Number of items per page, adjust as necessary
|
||||
|
||||
const loadPokemon = async (currentOffset: number) => {
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (offset > 0) {
|
||||
// Delay to simulate network latency
|
||||
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
|
||||
}
|
||||
|
||||
let res = await fetch(
|
||||
`https://pokeapi.co/api/v2/pokemon?offset=${currentOffset}&limit=${limit}`,
|
||||
{signal},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
let json = await res.json();
|
||||
|
||||
setHasMore(json.next !== null);
|
||||
// Append new results to existing ones
|
||||
setItems((prevItems) => [...prevItems, ...json.results]);
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Fetch aborted");
|
||||
} else {
|
||||
console.error("There was an error with the fetch operation:", error);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadPokemon(offset);
|
||||
}, []);
|
||||
|
||||
const onLoadMore = () => {
|
||||
const newOffset = offset + limit;
|
||||
|
||||
setOffset(newOffset);
|
||||
loadPokemon(newOffset);
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
hasMore,
|
||||
isLoading,
|
||||
onLoadMore,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500});
|
||||
|
||||
const [, scrollerRef] = useInfiniteScroll({
|
||||
hasMore,
|
||||
isEnabled: isOpen,
|
||||
shouldUseLoader: false, // We don't want to show the loader at the bottom of the list
|
||||
onLoadMore,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Select
|
||||
className="max-w-xs"
|
||||
isLoading={isLoading}
|
||||
items={items}
|
||||
label="Pick a Pokemon"
|
||||
placeholder="Select a Pokemon"
|
||||
scrollRef={scrollerRef}
|
||||
selectionMode="single"
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{(item) => (
|
||||
<SelectItem key={item.name} className="capitalize">
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
Spinner,
|
||||
Pagination,
|
||||
} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useCallback, useMemo, useState} from "react";
|
||||
import {useMemo, useState} from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
@@ -21,50 +21,23 @@ type SWCharacter = {
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
const fetcher = (...args: Parameters<typeof fetch>) => fetch(...args).then((res) => res.json());
|
||||
|
||||
export default function Page() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const {data, isLoading} = useSWR<{
|
||||
count: number;
|
||||
results: SWCharacter[];
|
||||
}>(`https://swapi.py4e.com/api/people?page=${page}`, fetcher, {
|
||||
keepPreviousData: 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],
|
||||
);
|
||||
const pages = useMemo(() => {
|
||||
return data?.count ? Math.ceil(data.count / rowsPerPage) : 0;
|
||||
}, [data?.count, rowsPerPage]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -80,7 +53,7 @@ export default function Page() {
|
||||
color="primary"
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={onPaginationChange}
|
||||
onChange={(page) => setPage(page)}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
@@ -96,8 +69,8 @@ export default function Page() {
|
||||
<TableColumn key="birth_year">Birth year</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={isLoading && !items.length}
|
||||
items={items}
|
||||
isLoading={isLoading || data?.results.length === 0}
|
||||
items={data?.results ?? []}
|
||||
loadingContent={<Spinner />}
|
||||
>
|
||||
{(item) => (
|
||||
|
||||
@@ -105,7 +105,7 @@ const users = [
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
id: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
@@ -256,7 +256,7 @@ const users = [
|
||||
},
|
||||
];
|
||||
|
||||
type User = typeof users[0];
|
||||
type User = (typeof users)[number];
|
||||
|
||||
export default function Page() {
|
||||
const [filterValue, setFilterValue] = useState("");
|
||||
|
||||
@@ -105,7 +105,7 @@ const users = [
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
id: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
@@ -256,7 +256,7 @@ const users = [
|
||||
},
|
||||
];
|
||||
|
||||
type User = typeof users[0];
|
||||
type User = (typeof users)[number];
|
||||
|
||||
export default function Page() {
|
||||
const [filterValue, setFilterValue] = useState("");
|
||||
@@ -271,8 +271,6 @@ export default function Page() {
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const pages = Math.ceil(users.length / rowsPerPage);
|
||||
|
||||
const hasSearchFilter = Boolean(filterValue);
|
||||
|
||||
const headerColumns = useMemo(() => {
|
||||
@@ -298,6 +296,8 @@ export default function Page() {
|
||||
return filteredUsers;
|
||||
}, [users, filterValue, statusFilter]);
|
||||
|
||||
const pages = Math.ceil(filteredItems.length / rowsPerPage);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const start = (page - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
@@ -390,6 +390,11 @@ export default function Page() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
setFilterValue("");
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const topContent = useMemo(() => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -400,7 +405,7 @@ export default function Page() {
|
||||
placeholder="Search by name..."
|
||||
startContent={<SearchIcon />}
|
||||
value={filterValue}
|
||||
onClear={() => setFilterValue("")}
|
||||
onClear={() => onClear()}
|
||||
onValueChange={onSearchChange}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
@@ -483,23 +488,22 @@ export default function Page() {
|
||||
<span className="w-[30%] text-small text-default-400">
|
||||
{selectedKeys === "all"
|
||||
? "All items selected"
|
||||
: `${selectedKeys.size} of ${items.length} selected`}
|
||||
: `${selectedKeys.size} of ${filteredItems.length} selected`}
|
||||
</span>
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color="primary"
|
||||
isDisabled={hasSearchFilter}
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={setPage}
|
||||
/>
|
||||
<div className="hidden sm:flex w-[30%] justify-end gap-2">
|
||||
<Button isDisabled={hasSearchFilter} size="sm" variant="flat" onPress={onPreviousPage}>
|
||||
<Button isDisabled={pages === 1} size="sm" variant="flat" onPress={onPreviousPage}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button isDisabled={hasSearchFilter} size="sm" variant="flat" onPress={onNextPage}>
|
||||
<Button isDisabled={pages === 1} size="sm" variant="flat" onPress={onNextPage}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,8 @@ export async function GET() {
|
||||
url: `${siteConfig.siteUrl}/blog/${post.slug}`,
|
||||
guid: `${siteConfig.siteUrl}/blog/${post.slug}`,
|
||||
date: post.date,
|
||||
author: `${author} <${siteConfig.email}>`,
|
||||
// @ts-ignore - name does exist
|
||||
author: `${author.name} <${siteConfig.email}>`,
|
||||
categories: post.tags ?? [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {Button, Image, Link} from "@nextui-org/react";
|
||||
import {Image} from "@nextui-org/react";
|
||||
|
||||
import {Blockquote} from "@/components/docs/components/blockquote";
|
||||
import {FigmaButton} from "@/components/figma-button";
|
||||
|
||||
export default function FigmaPage() {
|
||||
return (
|
||||
@@ -23,16 +24,7 @@ export default function FigmaPage() {
|
||||
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>
|
||||
<FigmaButton />
|
||||
<Blockquote color="warning">
|
||||
This file is still in development and will be continuously updated.
|
||||
</Blockquote>
|
||||
|
||||
@@ -2,7 +2,6 @@ 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";
|
||||
|
||||
@@ -12,7 +11,8 @@ import {siteConfig} from "@/config/site";
|
||||
import {fontSans} from "@/config/fonts";
|
||||
import {Navbar} from "@/components/navbar";
|
||||
import {Footer} from "@/components/footer";
|
||||
import {__PROD__} from "@/utils";
|
||||
import {ProBanner} from "@/components/pro-banner";
|
||||
import {ScriptProviders} from "@/components/scripts/script-providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
@@ -53,6 +53,12 @@ export const metadata: Metadata = {
|
||||
},
|
||||
],
|
||||
creator: "jrgarciadev",
|
||||
alternates: {
|
||||
canonical: "https://nextui.org",
|
||||
types: {
|
||||
"application/rss+xml": [{url: "https://nextui.org/feed.xml", title: "NextUI RSS Feed"}],
|
||||
},
|
||||
},
|
||||
viewport:
|
||||
"viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0",
|
||||
};
|
||||
@@ -64,13 +70,14 @@ export default function RootLayout({children}: {children: React.ReactNode}) {
|
||||
<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">
|
||||
<ProBanner />
|
||||
<Navbar mobileRoutes={manifest.mobileRoutes} routes={manifest.routes} />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
<Cmdk />
|
||||
</Providers>
|
||||
{__PROD__ && <Analytics />}
|
||||
<ScriptProviders />
|
||||
</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";
|
||||
@@ -12,6 +12,7 @@ import {Community} from "@/components/marketing/community";
|
||||
import {Support} from "@/components/marketing/support";
|
||||
import landingContent from "@/content/landing";
|
||||
import {getAllSponsors} from "@/utils/get-all-sponsors";
|
||||
import {Sponsors} from "@/components/marketing/sponsors";
|
||||
|
||||
async function getData() {
|
||||
try {
|
||||
@@ -33,6 +34,7 @@ export default async function Home() {
|
||||
<section className="flex flex-col items-center justify-center">
|
||||
<Hero />
|
||||
<FeaturesGrid features={landingContent.topFeatures} />
|
||||
<Sponsors />
|
||||
<CustomThemes />
|
||||
<A11yOtb />
|
||||
<DarkMode />
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as React from "react";
|
||||
import {NextUIProvider} from "@nextui-org/react";
|
||||
import {ThemeProvider as NextThemesProvider} from "next-themes";
|
||||
import {ThemeProviderProps} from "next-themes/dist/types";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
@@ -11,8 +12,10 @@ export interface ProvidersProps {
|
||||
}
|
||||
|
||||
export function Providers({children, themeProps}: ProvidersProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<NextUIProvider>
|
||||
<NextUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</NextUIProvider>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import {BlogPost} from "contentlayer/generated";
|
||||
import {Card, CardFooter, CardBody, CardHeader, Link, Avatar} from "@nextui-org/react";
|
||||
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";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
const BlogPostCard = (post: BlogPost) => {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const handlePress = () => {
|
||||
trackEvent("BlogPostCard - Selection", {
|
||||
name: post.title,
|
||||
action: "click",
|
||||
category: "blog",
|
||||
data: post.url ?? "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isMounted && (
|
||||
<motion.div
|
||||
<motion.article
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: 5}}
|
||||
initial={{opacity: 0, y: 5}}
|
||||
@@ -24,9 +34,10 @@ const BlogPostCard = (post: BlogPost) => {
|
||||
<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]"
|
||||
className="p-2 h-full 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}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<CardHeader>
|
||||
<Link
|
||||
@@ -35,23 +46,23 @@ const BlogPostCard = (post: BlogPost) => {
|
||||
href={post.url}
|
||||
size="lg"
|
||||
underline="hover"
|
||||
onPress={handlePress}
|
||||
>
|
||||
<Balancer>{post.title}</Balancer>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardBody className="px-3 pt-0 pb-1">
|
||||
<p className="font-normal text-default-600">
|
||||
<Balancer>{post.description}</Balancer>
|
||||
</p>
|
||||
<CardBody className="pt-0 px-2 pb-1">
|
||||
<Image className="mb-4" src={post.image} />
|
||||
<p className="font-normal w-full 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")}
|
||||
{format(parseISO(post.date), "LLLL d, yyyy")} {post?.draft && " (Draft)"}
|
||||
</time>
|
||||
<Avatar size="sm" src={post.author?.avatar} />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.article>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
@@ -59,7 +70,7 @@ const BlogPostCard = (post: BlogPost) => {
|
||||
|
||||
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">
|
||||
<div className="mt-10 grid gap-4 grid-cols-[repeat(auto-fill,minmax(300px,1fr))]">
|
||||
{posts.map((post, idx) => (
|
||||
<BlogPostCard key={idx} {...post} />
|
||||
))}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
|
||||
import searchData from "@/config/search-meta.json";
|
||||
import {useUpdateEffect} from "@/hooks/use-update-effect";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
const hideOnPaths = ["examples"];
|
||||
|
||||
@@ -175,6 +176,13 @@ export const Cmdk: FC<{}> = () => {
|
||||
|
||||
const matches = intersectionBy(...matchesForEachWord, "objectID").slice(0, MAX_RESULTS);
|
||||
|
||||
trackEvent("Cmdk - Search", {
|
||||
name: "cmdk - search",
|
||||
action: "search",
|
||||
category: "cmdk",
|
||||
data: {query, words, matches: matches?.map((match) => match.url).join(", ")},
|
||||
});
|
||||
|
||||
return matches;
|
||||
},
|
||||
[query],
|
||||
@@ -190,10 +198,21 @@ export const Cmdk: FC<{}> = () => {
|
||||
if (e?.key?.toLowerCase() === "k" && e[hotkey]) {
|
||||
e.preventDefault();
|
||||
isOpen ? onClose() : onOpen();
|
||||
|
||||
trackEvent("Cmdk - Open/Close", {
|
||||
name: "cmdk - open/close",
|
||||
action: "keydown",
|
||||
category: "cmdk",
|
||||
data: isOpen ? "close" : "open",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const onItemSelect = useCallback(
|
||||
@@ -201,6 +220,13 @@ export const Cmdk: FC<{}> = () => {
|
||||
onClose();
|
||||
router.push(item.url);
|
||||
addToRecentSearches(item);
|
||||
|
||||
trackEvent("Cmdk - ItemSelect", {
|
||||
name: item.content,
|
||||
action: "click",
|
||||
category: "cmdk",
|
||||
data: item.url,
|
||||
});
|
||||
},
|
||||
[router, recentSearches],
|
||||
);
|
||||
|
||||
@@ -108,9 +108,9 @@ function CodeTypewriter({value, className, css, ...props}: any) {
|
||||
return (
|
||||
<Pre className={className} css={css} {...props}>
|
||||
<code
|
||||
dangerouslySetInnerHTML={{__html: value}}
|
||||
ref={wrapperRef}
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{__html: value}}
|
||||
style={{opacity: 0}}
|
||||
/>
|
||||
</Pre>
|
||||
@@ -155,7 +155,7 @@ const CodeBlock = React.forwardRef<HTMLPreElement, CodeBlockProps>((_props, forw
|
||||
{...props}
|
||||
>
|
||||
{showWindowIcons && <WindowActions title={title} />}
|
||||
<code className={clsx(classes, codeClasses)} dangerouslySetInnerHTML={{__html: result}} />
|
||||
<code dangerouslySetInnerHTML={{__html: result}} className={clsx(classes, codeClasses)} />
|
||||
</Pre>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ export const DemoCodeModal: FC<DemoCodeModalProps> = ({isOpen, code, title, subt
|
||||
<>
|
||||
This is an example of how to use the {lowerTitle} component, for more information
|
||||
please visit the
|
||||
<NextUILink as={Link} href={`/docs/${lowerTitle}`}>
|
||||
<NextUILink as={Link} href={`/docs/components/${lowerTitle}`}>
|
||||
{lowerTitle}
|
||||
</NextUILink>
|
||||
docs.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import {useRef} from "react";
|
||||
import {Button} from "@nextui-org/react";
|
||||
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
export const CustomButton = () => {
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
@@ -26,6 +28,11 @@ export const CustomButton = () => {
|
||||
x: targetCenterX / clientWidth,
|
||||
},
|
||||
});
|
||||
|
||||
trackEvent("LandingPage - Confetti Button", {
|
||||
action: "press",
|
||||
category: "landing-page",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardBody, Button, Image, Progress, CardProps} from "@nextui-org/react";
|
||||
import {Card, CardBody, Button, Image, Slider, CardProps} from "@nextui-org/react";
|
||||
import {useState, FC} from "react";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import NextImage from "next/image";
|
||||
@@ -63,15 +63,15 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-3 gap-1">
|
||||
<Progress
|
||||
<Slider
|
||||
aria-label="Music progress"
|
||||
classNames={{
|
||||
indicator: "bg-default-800 dark:bg-white",
|
||||
track: "bg-default-500/30",
|
||||
thumb: "w-2 h-2 after:w-2 after:h-2 after:bg-foreground",
|
||||
}}
|
||||
color="default"
|
||||
color="foreground"
|
||||
defaultValue={33}
|
||||
size="sm"
|
||||
value={33}
|
||||
/>
|
||||
<div className="flex justify-between">
|
||||
<p className="text-sm">1:23</p>
|
||||
|
||||
@@ -31,11 +31,12 @@ interface CodeDemoProps extends UseCodeDemoProps, WindowResizerProps {
|
||||
showSandpackPreview?: boolean;
|
||||
initialEditorOpen?: boolean;
|
||||
enableResize?: boolean;
|
||||
showTabs?: boolean;
|
||||
showPreview?: boolean;
|
||||
hideWindowActions?: boolean;
|
||||
showOpenInCodeSandbox?: boolean;
|
||||
isPreviewCentered?: boolean;
|
||||
resizeEnabled?: boolean;
|
||||
typescriptStrict?: boolean;
|
||||
displayMode?: "always" | "visible";
|
||||
isGradientBox?: boolean;
|
||||
gradientColor?: GradientBoxProps["color"];
|
||||
@@ -51,16 +52,19 @@ export const CodeDemo: React.FC<CodeDemoProps> = ({
|
||||
showEditor = true,
|
||||
showPreview = true,
|
||||
asIframe = false,
|
||||
showTabs = true,
|
||||
resizeEnabled = true,
|
||||
hideWindowActions = false,
|
||||
showSandpackPreview = false,
|
||||
isPreviewCentered = false,
|
||||
// when false .js files will be used
|
||||
typescriptStrict = false,
|
||||
showOpenInCodeSandbox,
|
||||
isGradientBox = false,
|
||||
defaultExpanded = false,
|
||||
previewHeight = "auto",
|
||||
overflow = "visible",
|
||||
displayMode = "always",
|
||||
showTabs = true,
|
||||
gradientColor,
|
||||
highlightedLines,
|
||||
iframeInitialWidth,
|
||||
@@ -96,6 +100,7 @@ export const CodeDemo: React.FC<CodeDemoProps> = ({
|
||||
|
||||
const content = asIframe ? (
|
||||
<WindowResizer
|
||||
hideWindowActions={hideWindowActions}
|
||||
iframeHeight={previewHeight}
|
||||
iframeInitialWidth={iframeInitialWidth}
|
||||
iframeSrc={iframeSrc}
|
||||
@@ -121,6 +126,7 @@ export const CodeDemo: React.FC<CodeDemoProps> = ({
|
||||
isGradientBox,
|
||||
gradientColor,
|
||||
previewHeight,
|
||||
hideWindowActions,
|
||||
asIframe,
|
||||
showPreview,
|
||||
isInView,
|
||||
@@ -138,6 +144,7 @@ export const CodeDemo: React.FC<CodeDemoProps> = ({
|
||||
showEditor={showEditor}
|
||||
showOpenInCodeSandbox={showOpenInCodeSandbox || showPreview}
|
||||
showPreview={showSandpackPreview}
|
||||
typescriptStrict={typescriptStrict}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ 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 * as intlDateUtils from "@internationalized/date";
|
||||
import * as reactAriaI18n from "@react-aria/i18n";
|
||||
|
||||
import {BgGridContainer} from "@/components/bg-grid-container";
|
||||
import {GradientBox, GradientBoxProps} from "@/components/gradient-box";
|
||||
@@ -19,6 +21,8 @@ export interface ReactLiveDemoProps {
|
||||
|
||||
export const scope = {
|
||||
...Components,
|
||||
...intlDateUtils,
|
||||
...reactAriaI18n,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
export const ReactLiveDemo: React.FC<ReactLiveDemoProps> = ({
|
||||
|
||||
@@ -49,6 +49,7 @@ const resizer = tv({
|
||||
|
||||
export interface WindowResizerProps {
|
||||
resizeEnabled?: boolean;
|
||||
hideWindowActions?: boolean;
|
||||
iframeHeight?: string | number;
|
||||
iframeMinWidth?: number;
|
||||
iframeSrc?: string;
|
||||
@@ -70,6 +71,7 @@ const WindowResizer: React.FC<WindowResizerProps> = (props) => {
|
||||
iframeSrc,
|
||||
iframeTitle,
|
||||
resizeEnabled,
|
||||
hideWindowActions = false,
|
||||
iframeHeight: height = "420px",
|
||||
iframeInitialWidth,
|
||||
iframeMinWidth: minWidth = MIN_WIDTH,
|
||||
@@ -120,7 +122,7 @@ const WindowResizer: React.FC<WindowResizerProps> = (props) => {
|
||||
width: isMobile ? "100%" : browserWidth,
|
||||
}}
|
||||
>
|
||||
<WindowActions className="bg-default-100 dark:bg-default-50" />
|
||||
{!hideWindowActions && <WindowActions className="bg-default-100 dark:bg-default-50" />}
|
||||
<motion.iframe ref={iframeRef} className={iframe()} src={iframeSrc} title={iframeTitle} />
|
||||
</motion.div>
|
||||
{resizeEnabled && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {forwardRef} from "react";
|
||||
import {clsx, getUniqueID} from "@nextui-org/shared-utils";
|
||||
import React, {forwardRef, useEffect} from "react";
|
||||
import {clsx, dataAttr, getUniqueID} from "@nextui-org/shared-utils";
|
||||
import BaseHighlight, {Language, PrismTheme, defaultProps} from "prism-react-renderer";
|
||||
import {debounce, omit} from "lodash";
|
||||
|
||||
import defaultTheme from "@/libs/prism-theme";
|
||||
|
||||
@@ -9,13 +10,26 @@ interface CodeblockProps {
|
||||
codeString: string;
|
||||
metastring?: string;
|
||||
theme?: PrismTheme;
|
||||
children?: React.ReactNode;
|
||||
showLines?: boolean;
|
||||
removeIndent?: boolean;
|
||||
hideScrollBar?: boolean;
|
||||
className?: string;
|
||||
children?: (props: any) => React.ReactNode;
|
||||
}
|
||||
|
||||
type HighlightStyle = "inserted" | "deleted" | undefined;
|
||||
|
||||
const nextuiCliCommand = [
|
||||
/^init$/,
|
||||
/^add$/,
|
||||
/^upgrade$/,
|
||||
/^remove$/,
|
||||
/^list$/,
|
||||
/^env$/,
|
||||
/^doctor$/,
|
||||
];
|
||||
|
||||
const highlightStyleToken = ["bun", /nextui\s\w+(?=\s?)/, /^nextui$/, "Usage", ...nextuiCliCommand];
|
||||
const RE = /{([\d,-]+)}/;
|
||||
|
||||
const calculateLinesToHighlight = (meta?: string) => {
|
||||
@@ -60,11 +74,70 @@ const Codeblock = forwardRef<HTMLPreElement, CodeblockProps>(
|
||||
const shouldHighlightLine = calculateLinesToHighlight(metastring);
|
||||
const isMultiLine = codeString.split("\n").length > 2;
|
||||
|
||||
const lastSelectionText = React.useRef<string | null>(null);
|
||||
|
||||
const isDiff = language.includes("diff");
|
||||
|
||||
const codeLang = isDiff ? (language.split("-")[1] as Language) : language;
|
||||
|
||||
let highlightStyle: HighlightStyle[] = [];
|
||||
|
||||
if (isDiff) {
|
||||
let code: string[] = [];
|
||||
|
||||
highlightStyle = codeString.split?.("\n").map((line) => {
|
||||
if (line.startsWith("+")) {
|
||||
code.push(line.substr(1));
|
||||
|
||||
return "inserted";
|
||||
}
|
||||
if (line.startsWith("-")) {
|
||||
code.push(line.substr(1));
|
||||
|
||||
return "deleted";
|
||||
}
|
||||
code.push(line);
|
||||
});
|
||||
|
||||
codeString = code.join("\n");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleSelectionChange = () => {
|
||||
if (!window.getSelection) return;
|
||||
|
||||
const el = window.getSelection()?.anchorNode?.parentNode;
|
||||
|
||||
if (!el) return;
|
||||
|
||||
const selectionText = window.getSelection()?.toString();
|
||||
|
||||
if (!selectionText) return;
|
||||
|
||||
if (
|
||||
!selectionText ||
|
||||
selectionText === lastSelectionText.current ||
|
||||
!codeString.includes(selectionText)
|
||||
)
|
||||
return;
|
||||
|
||||
lastSelectionText.current = selectionText;
|
||||
};
|
||||
|
||||
const debouncedHandleSelectionChange = debounce(handleSelectionChange, 1000);
|
||||
|
||||
document.addEventListener("selectionchange", debouncedHandleSelectionChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("selectionchange", debouncedHandleSelectionChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BaseHighlight
|
||||
{...defaultProps}
|
||||
code={codeString}
|
||||
language={language}
|
||||
language={codeLang}
|
||||
theme={theme}
|
||||
{...props}
|
||||
>
|
||||
@@ -83,8 +156,8 @@ const Codeblock = forwardRef<HTMLPreElement, CodeblockProps>(
|
||||
|
||||
return (
|
||||
<div
|
||||
{...omit(lineProps, ["key"])}
|
||||
key={`${i}-${getUniqueID("line-wrapper")}`}
|
||||
{...lineProps}
|
||||
className={clsx(
|
||||
lineProps.className,
|
||||
removeIndent ? "pr-4" : "px-4",
|
||||
@@ -97,17 +170,34 @@ const Codeblock = forwardRef<HTMLPreElement, CodeblockProps>(
|
||||
shouldHighlightLine(i),
|
||||
},
|
||||
)}
|
||||
data-deleted={dataAttr(highlightStyle?.[i] === "deleted")}
|
||||
data-inserted={dataAttr(highlightStyle?.[i] === "inserted")}
|
||||
>
|
||||
{showLines && (
|
||||
<span className="select-none text-xs mr-6 opacity-30">{i + 1}</span>
|
||||
)}
|
||||
{line.map((token, key) => (
|
||||
<span
|
||||
key={`${key}-${getUniqueID("line")}`}
|
||||
{...getTokenProps({token, key})}
|
||||
className={className}
|
||||
/>
|
||||
))}
|
||||
{line.map((token, key) => {
|
||||
// Bun has no color style by default in the code block, so hack add in here
|
||||
const props = getTokenProps({token, key}) || {};
|
||||
|
||||
return (
|
||||
<span
|
||||
{...omit(props, ["key"])}
|
||||
key={`${key}-${getUniqueID("line")}`}
|
||||
className={className}
|
||||
style={{
|
||||
...props.style,
|
||||
...(highlightStyleToken.some((t) => {
|
||||
const regex = t instanceof RegExp ? t : new RegExp(t);
|
||||
|
||||
return regex.test(token.content.trim());
|
||||
})
|
||||
? {color: "rgb(var(--code-function))"}
|
||||
: {}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import {Button, ButtonProps, 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";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
export interface ComponentLinksProps {
|
||||
component: string;
|
||||
npm?: string;
|
||||
source?: string;
|
||||
styles?: string;
|
||||
storybook?: string;
|
||||
rscCompatible?: boolean;
|
||||
reactAriaHook?: string;
|
||||
}
|
||||
|
||||
@@ -14,27 +20,53 @@ const ButtonLink = ({
|
||||
children,
|
||||
href,
|
||||
startContent,
|
||||
tooltip,
|
||||
...props
|
||||
}: ButtonProps & {
|
||||
href: string;
|
||||
}) => (
|
||||
<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>
|
||||
);
|
||||
tooltip?: string | ReactNode;
|
||||
}) => {
|
||||
const handlePress = () => {
|
||||
if (!href) return;
|
||||
|
||||
trackEvent("ComponentLinks - Click", {
|
||||
category: "docs",
|
||||
action: "click",
|
||||
data: href || "",
|
||||
});
|
||||
};
|
||||
|
||||
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}
|
||||
onPress={handlePress}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<Tooltip className="max-w-[230px]" content={tooltip}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
};
|
||||
|
||||
export const ComponentLinks = ({
|
||||
component,
|
||||
npm,
|
||||
source,
|
||||
storybook,
|
||||
styles,
|
||||
rscCompatible,
|
||||
reactAriaHook,
|
||||
}: ComponentLinksProps) => {
|
||||
if (!component) {
|
||||
@@ -44,16 +76,18 @@ export const ComponentLinks = ({
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3 mt-6">
|
||||
<ButtonLink
|
||||
href={`https://storybook.nextui.org/?path=/story/components-${storybook || component}`}
|
||||
href={`https://storybook.nextui.org/?path=/story/components-${
|
||||
storybook || component
|
||||
}--default`}
|
||||
startContent={<StorybookIcon className="text-lg text-[#ff4785]" />}
|
||||
>
|
||||
Storybook
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
href={`https://www.npmjs.com/package/@nextui-org/${component}`}
|
||||
href={`https://www.npmjs.com/package/@nextui-org/${npm || component}`}
|
||||
startContent={<NpmIcon className="text-2xl text-[#E53E3E]" />}
|
||||
>
|
||||
{`@nextui-org/${component}`}
|
||||
{`@nextui-org/${npm || component}`}
|
||||
</ButtonLink>
|
||||
{reactAriaHook && (
|
||||
<ButtonLink
|
||||
@@ -63,7 +97,30 @@ export const ComponentLinks = ({
|
||||
React Aria
|
||||
</ButtonLink>
|
||||
)}
|
||||
<ButtonLink href={`${COMPONENT_PATH}/${component}`} startContent={<GithubIcon size={20} />}>
|
||||
{rscCompatible && (
|
||||
<ButtonLink
|
||||
href="https://nextjs.org/docs/app/building-your-application/rendering/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}/${source || component}`}
|
||||
startContent={<GithubIcon size={20} />}
|
||||
>
|
||||
Source
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import {Tabs, Tab, Snippet} from "@nextui-org/react";
|
||||
import {Key, useState} from "react";
|
||||
|
||||
import Codeblock from "./codeblock";
|
||||
|
||||
import {YarnIcon, NpmSmallIcon, PnpmIcon} from "@/components/icons";
|
||||
import {YarnIcon, NpmSmallIcon, PnpmIcon, BunIcon, CLIBoldIcon} from "@/components/icons";
|
||||
|
||||
type PackageManagerName = "npm" | "yarn" | "pnpm";
|
||||
type PackageManagerName = "cli" | "npm" | "yarn" | "pnpm" | "bun";
|
||||
|
||||
type PackageManager = {
|
||||
icon: JSX.Element;
|
||||
label?: string;
|
||||
name: PackageManagerName;
|
||||
};
|
||||
|
||||
const packageManagers: PackageManager[] = [
|
||||
{
|
||||
name: "cli",
|
||||
label: "CLI",
|
||||
icon: <CLIBoldIcon className="text-lg text-default-600 dark:text-default-400" />,
|
||||
},
|
||||
{
|
||||
name: "npm",
|
||||
icon: <NpmSmallIcon className="text-[#E53E3E]" />,
|
||||
@@ -24,13 +31,25 @@ const packageManagers: PackageManager[] = [
|
||||
name: "pnpm",
|
||||
icon: <PnpmIcon className="text-[#F69220]" />,
|
||||
},
|
||||
{
|
||||
name: "bun",
|
||||
icon: <BunIcon className="text-lg text-[#FBF0DF]" />,
|
||||
},
|
||||
];
|
||||
|
||||
export interface PackageManagersProps {
|
||||
commands: Partial<Record<PackageManagerName, string>>;
|
||||
commands: Partial<Record<PackageManagerName, React.Key>>;
|
||||
}
|
||||
|
||||
export const PackageManagers = ({commands}: PackageManagersProps) => {
|
||||
const [selectedManager, setSelectedManager] = useState<PackageManagerName>(
|
||||
commands.cli ? "cli" : "npm",
|
||||
);
|
||||
|
||||
const handleSelectionChange = (tabKey: Key) => {
|
||||
setSelectedManager(tabKey as PackageManagerName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
aria-label="NextUI installation commands"
|
||||
@@ -38,9 +57,11 @@ export const PackageManagers = ({commands}: PackageManagersProps) => {
|
||||
base: "group mt-4",
|
||||
tabList: "h-10",
|
||||
}}
|
||||
selectedKey={selectedManager}
|
||||
variant="underlined"
|
||||
onSelectionChange={handleSelectionChange}
|
||||
>
|
||||
{packageManagers.map(({name, icon}) => {
|
||||
{packageManagers.map(({name, label, icon}) => {
|
||||
if (!commands[name]) return null;
|
||||
|
||||
return (
|
||||
@@ -49,7 +70,7 @@ export const PackageManagers = ({commands}: PackageManagersProps) => {
|
||||
title={
|
||||
<div className="flex items-center space-x-2">
|
||||
{icon}
|
||||
<span>{name}</span>
|
||||
<span>{label || name}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ type SwatchColors = {
|
||||
|
||||
type SwatchSetProps = {
|
||||
colors: SwatchColors[];
|
||||
isSematic?: boolean;
|
||||
isSemantic?: boolean;
|
||||
};
|
||||
|
||||
const scaleIndexMap: Record<number, string> = {
|
||||
@@ -90,7 +90,7 @@ const Swatch = ({color, scale}: {color: string; scale?: string}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SematicSwatch = ({
|
||||
const SemanticSwatch = ({
|
||||
color,
|
||||
className,
|
||||
textClassName,
|
||||
@@ -139,15 +139,15 @@ const SematicSwatch = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SwatchSet = ({colors, isSematic = false}: SwatchSetProps) => (
|
||||
const SwatchSet = ({colors, isSemantic = 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
|
||||
isSemantic ? (
|
||||
<SemanticSwatch
|
||||
key={`${c.color}-${index}`}
|
||||
className={c.className}
|
||||
color={c.color}
|
||||
@@ -218,7 +218,7 @@ export const CommonColors = () => {
|
||||
export const SemanticColors = () => {
|
||||
return (
|
||||
<SwatchSet
|
||||
isSematic
|
||||
isSemantic
|
||||
colors={[
|
||||
{
|
||||
title: "Layout",
|
||||
|
||||
@@ -1,31 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import NextLink from "next/link";
|
||||
import {Link} from "@nextui-org/react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {ChevronIcon} from "@nextui-org/shared-icons";
|
||||
|
||||
import manifest from "@/config/routes.json";
|
||||
import {removeFromLast} from "@/utils";
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {useDocsRoute} from "@/hooks/use-docs-route";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
export interface FooterNavProps {
|
||||
currentRoute?: Route;
|
||||
}
|
||||
|
||||
export const DocsPager: React.FC<FooterNavProps> = ({currentRoute}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const {prevRoute, nextRoute} = useDocsRoute(manifest.routes, currentRoute);
|
||||
|
||||
const handlePress = (path: string) => {
|
||||
trackEvent("DocsPager - Click", {
|
||||
category: "docs",
|
||||
action: "click",
|
||||
data: path || "",
|
||||
});
|
||||
|
||||
router.push(removeFromLast(path || "", "."));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-between py-20">
|
||||
{prevRoute ? (
|
||||
<Link
|
||||
isBlock
|
||||
as={NextLink}
|
||||
className="flex gap-2"
|
||||
className="cursor-pointer flex gap-2"
|
||||
color="foreground"
|
||||
href={removeFromLast(prevRoute.path || "", ".")}
|
||||
onPress={() => handlePress(prevRoute.path || "")}
|
||||
>
|
||||
<ChevronIcon className="text-primary" height={20} width={20} />
|
||||
{prevRoute.title}
|
||||
@@ -36,10 +48,9 @@ export const DocsPager: React.FC<FooterNavProps> = ({currentRoute}) => {
|
||||
{nextRoute && (
|
||||
<Link
|
||||
isBlock
|
||||
as={NextLink}
|
||||
className="flex gap-1 items-center"
|
||||
className="cursor-pointer flex gap-1 items-center"
|
||||
color="foreground"
|
||||
href={removeFromLast(nextRoute.path || "", ".")}
|
||||
onPress={() => handlePress(nextRoute.path || "")}
|
||||
>
|
||||
{nextRoute.title}
|
||||
<ChevronIcon className="rotate-180 text-primary" height={20} width={20} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {FC} from "react";
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import {ChevronIcon} from "@nextui-org/shared-icons";
|
||||
import {CollectionBase, Expandable, MultipleSelection, Node, ItemProps} from "@react-types/shared";
|
||||
import {BaseItem} from "@nextui-org/aria-utils";
|
||||
@@ -21,11 +21,18 @@ import Link from "next/link";
|
||||
import {isEmpty} from "lodash";
|
||||
import {usePathname, useRouter} from "next/navigation";
|
||||
|
||||
import {ScrollArea} from "../scroll-area";
|
||||
|
||||
import {getRoutePaths} from "./utils";
|
||||
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {TreeKeyboardDelegate} from "@/utils/tree-keyboard-delegate";
|
||||
import {useScrollPosition} from "@/hooks/use-scroll-position";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
import {FbFeedbackButton} from "@/components/featurebase/fb-feedback-button";
|
||||
import {FbChangelogButton} from "@/components/featurebase/fb-changelog-button";
|
||||
import {FbRoadmapLink} from "@/components/featurebase/fb-roadmap-link";
|
||||
import {openFeedbackWidget} from "@/utils/featurebase";
|
||||
import emitter from "@/libs/emitter";
|
||||
|
||||
export interface Props<T> extends Omit<ItemProps<T>, "title">, Route {
|
||||
slug?: string;
|
||||
@@ -68,6 +75,7 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
};
|
||||
|
||||
const isNew = item.props?.newPost;
|
||||
const isUpdated = item.props?.updated;
|
||||
|
||||
const isExpanded = state.expandedKeys.has(key);
|
||||
const isSelected =
|
||||
@@ -82,18 +90,109 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
|
||||
const Component = hasChildNodes ? "ul" : "li";
|
||||
|
||||
const cn = clsx(
|
||||
"w-full",
|
||||
"font-normal",
|
||||
"before:mr-4",
|
||||
"before:content-['']",
|
||||
"before:block",
|
||||
"before:bg-default-300",
|
||||
"before:w-1",
|
||||
"before:h-1",
|
||||
"before:rounded-full",
|
||||
);
|
||||
|
||||
const {pressProps} = usePress({
|
||||
onPress: () => {
|
||||
if (hasChildNodes) {
|
||||
state.toggleKey(item.key);
|
||||
} else {
|
||||
router.push(paths.pathname);
|
||||
|
||||
trackEvent("SidebarDocs", {
|
||||
category: "docs",
|
||||
action: "click",
|
||||
data: paths.pathname || "",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const {focusProps, isFocused, isFocusVisible} = useFocusRing();
|
||||
|
||||
const renderFeaturebaseComponent = (key: string) => {
|
||||
if (key === "roadmap")
|
||||
return <FbRoadmapLink className={cn} innerClassName="opacity-80 dark:opacity-60" />;
|
||||
if (key === "changelog")
|
||||
return (
|
||||
<NextUILink as={Link} className={cn} color="foreground" href="#">
|
||||
<FbChangelogButton />
|
||||
</NextUILink>
|
||||
);
|
||||
|
||||
return (
|
||||
<NextUILink as={Link} className={cn} color="foreground" href="#" onClick={openFeedbackWidget}>
|
||||
<FbFeedbackButton />
|
||||
</NextUILink>
|
||||
);
|
||||
};
|
||||
|
||||
const renderComponent = () => {
|
||||
if (hasChildNodes) {
|
||||
return (
|
||||
<span className="flex items-center gap-3">
|
||||
<span>{rendered}</span>
|
||||
<ChevronIcon
|
||||
className={clsx("transition-transform", {
|
||||
"-rotate-90": isExpanded,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof key === "string" && ["changelog", "feedback", "roadmap"].includes(key)) {
|
||||
return renderFeaturebaseComponent(key);
|
||||
}
|
||||
|
||||
return (
|
||||
<NextUILink as={Link} className={clsx(cn)} color="foreground" href={paths.pathname}>
|
||||
<span
|
||||
className={clsx(
|
||||
isSelected
|
||||
? "text-primary font-medium dark:text-foreground"
|
||||
: "opacity-80 dark:opacity-60",
|
||||
{
|
||||
"pointer-events-none": item.props?.comingSoon,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{rendered}
|
||||
</span>
|
||||
{isUpdated && (
|
||||
<Chip
|
||||
className="ml-1 py-1 text-tiny text-default-400 bg-default-100/50"
|
||||
color="default"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
Updated
|
||||
</Chip>
|
||||
)}
|
||||
{isNew && (
|
||||
<Chip className="ml-1 py-1 text-tiny" color="primary" size="sm" variant="flat">
|
||||
New
|
||||
</Chip>
|
||||
)}
|
||||
{item.props?.comingSoon && (
|
||||
<Chip className="ml-1 py-1 text-tiny" color="default" size="sm" variant="flat">
|
||||
Coming soon
|
||||
</Chip>
|
||||
)}
|
||||
</NextUILink>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...focusProps}
|
||||
@@ -101,7 +200,9 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
aria-expanded={dataAttr(hasChildNodes ? isExpanded : undefined)}
|
||||
aria-selected={dataAttr(isSelected)}
|
||||
className={clsx(
|
||||
"flex flex-col gap-3 outline-none w-full tap-highlight-transparent",
|
||||
"flex flex-col gap-3outline-none w-full tap-highlight-transparent",
|
||||
|
||||
hasChildNodes ? "mb-4" : "first:mt-4",
|
||||
// focus ring
|
||||
...dataFocusVisibleClasses,
|
||||
)}
|
||||
@@ -111,56 +212,9 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
>
|
||||
<div className="flex items-center gap-3 cursor-pointer" {...pressProps}>
|
||||
<Spacer x={spaceLeft} />
|
||||
{hasChildNodes ? (
|
||||
<span className="flex items-center gap-3">
|
||||
<span>{rendered}</span>
|
||||
<ChevronIcon
|
||||
className={clsx("transition-transform", {
|
||||
"-rotate-90": isExpanded,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<NextUILink
|
||||
as={Link}
|
||||
className={clsx(
|
||||
"w-full",
|
||||
"font-normal",
|
||||
"before:mr-4",
|
||||
"before:content-['']",
|
||||
"before:block",
|
||||
"before:bg-default-300",
|
||||
"before:w-1",
|
||||
"before:h-1",
|
||||
"before:rounded-full",
|
||||
)}
|
||||
color="foreground"
|
||||
href={paths.pathname}
|
||||
>
|
||||
<span
|
||||
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="sm" variant="flat">
|
||||
New
|
||||
</Chip>
|
||||
)}
|
||||
{item.props?.comingSoon && (
|
||||
<Chip className="ml-2" color="default" size="sm" variant="flat">
|
||||
Coming soon
|
||||
</Chip>
|
||||
)}
|
||||
</NextUILink>
|
||||
)}
|
||||
{renderComponent()}
|
||||
{/* Workaround to avoid scrollbar overlapping */}
|
||||
<Spacer x={4} />
|
||||
</div>
|
||||
{isExpanded && hasChildNodes && (
|
||||
<div className="flex flex-col gap-3 items-start" role="group">
|
||||
@@ -189,9 +243,7 @@ function TreeHeading({item}: {item: any}) {
|
||||
function Tree<T extends object>(props: CollectionBase<T> & Expandable & MultipleSelection) {
|
||||
let state = useTreeState(props);
|
||||
|
||||
let ref = useRef<HTMLUListElement>(null);
|
||||
|
||||
const scrollPosition = useScrollPosition(ref);
|
||||
let ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
let keyboardDelegate = useMemo(
|
||||
// @ts-expect-error
|
||||
@@ -206,16 +258,11 @@ function Tree<T extends object>(props: CollectionBase<T> & Expandable & Multiple
|
||||
});
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...collectionProps}
|
||||
<ScrollArea
|
||||
ref={ref}
|
||||
className="flex flex-col gap-4 scrollbar-hide lg:overflow-y-scroll lg:max-h-[calc(100vh_-_64px)] pb-28"
|
||||
className="h-full lg:max-h-[calc(100vh_-_64px)]"
|
||||
role="tree"
|
||||
style={{
|
||||
WebkitMaskImage: `linear-gradient(to top, transparent 0%, #000 100px, #000 ${
|
||||
scrollPosition > 30 ? "90%" : "100%"
|
||||
}, transparent 100%)`,
|
||||
}}
|
||||
{...collectionProps}
|
||||
>
|
||||
{[...state.collection].map((item) => {
|
||||
if (item.type === "section") {
|
||||
@@ -224,7 +271,7 @@ function Tree<T extends object>(props: CollectionBase<T> & Expandable & Multiple
|
||||
|
||||
return <TreeItem key={item.key} item={item} state={state} />;
|
||||
})}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -236,6 +283,8 @@ export interface DocsSidebarProps {
|
||||
}
|
||||
|
||||
export const DocsSidebar: FC<DocsSidebarProps> = ({routes, slug, tag, className}) => {
|
||||
const [isProBannerVisible, setIsProBannerVisible] = useState(true);
|
||||
|
||||
const expandedKeys = routes?.reduce((keys, route) => {
|
||||
if (route.defaultOpen) {
|
||||
keys.push(route.key as string);
|
||||
@@ -244,8 +293,18 @@ export const DocsSidebar: FC<DocsSidebarProps> = ({routes, slug, tag, className}
|
||||
return keys;
|
||||
}, [] as string[]);
|
||||
|
||||
return (
|
||||
<div className={clsx("lg:fixed lg:top-20 mt-2 z-0 lg:h-[calc(100vh-121px)]", className)}>
|
||||
useEffect(() => {
|
||||
emitter.on("proBannerVisibilityChange", (value) => {
|
||||
setIsProBannerVisible(value === "visible");
|
||||
});
|
||||
|
||||
return () => {
|
||||
emitter.off("proBannerVisibilityChange");
|
||||
};
|
||||
}, []);
|
||||
|
||||
const treeContent = useMemo(() => {
|
||||
return (
|
||||
<Tree defaultExpandedKeys={expandedKeys} items={routes || []}>
|
||||
{(route) => (
|
||||
<Item
|
||||
@@ -259,6 +318,18 @@ export const DocsSidebar: FC<DocsSidebarProps> = ({routes, slug, tag, className}
|
||||
</Item>
|
||||
)}
|
||||
</Tree>
|
||||
);
|
||||
}, [routes]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"lg:fixed mt-2 z-0 lg:h-[calc(100vh-121px)]",
|
||||
isProBannerVisible ? "lg:top-32" : "lg:top-20",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{treeContent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {FC, useRef, useEffect} from "react";
|
||||
import {FC, useRef, useEffect, useState} from "react";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {Divider, Spacer} from "@nextui-org/react";
|
||||
import {ChevronCircleTopLinearIcon} from "@nextui-org/shared-icons";
|
||||
@@ -9,6 +9,7 @@ import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import {Heading} from "@/libs/docs/utils";
|
||||
import {useScrollSpy} from "@/hooks/use-scroll-spy";
|
||||
import {useScrollPosition} from "@/hooks/use-scroll-position";
|
||||
import emitter from "@/libs/emitter";
|
||||
|
||||
export interface DocsTocProps {
|
||||
headings: Heading[];
|
||||
@@ -22,6 +23,8 @@ const paddingLeftByLevel: Record<number, string> = {
|
||||
};
|
||||
|
||||
export const DocsToc: FC<DocsTocProps> = ({headings}) => {
|
||||
const [isProBannerVisible, setIsProBannerVisible] = useState(true);
|
||||
|
||||
const tocRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollPosition = useScrollPosition(tocRef);
|
||||
@@ -51,10 +54,23 @@ export const DocsToc: FC<DocsTocProps> = ({headings}) => {
|
||||
}
|
||||
}, [activeId, activeIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
emitter.on("proBannerVisibilityChange", (value) => {
|
||||
setIsProBannerVisible(value === "visible");
|
||||
});
|
||||
|
||||
return () => {
|
||||
emitter.off("proBannerVisibilityChange");
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={tocRef}
|
||||
className="fixed w-full max-w-[12rem] flex flex-col gap-4 text-left top-20 pb-20 h-[calc(100vh-121px)] scrollbar-hide overflow-y-scroll"
|
||||
className={clsx(
|
||||
"fixed w-full max-w-[12rem] flex flex-col gap-4 text-left pb-28 h-[calc(100vh-121px)] scrollbar-hide overflow-y-scroll",
|
||||
isProBannerVisible ? "top-32" : "top-20",
|
||||
)}
|
||||
style={{
|
||||
WebkitMaskImage: `linear-gradient(to top, transparent 0%, #000 100px, #000 ${
|
||||
scrollPosition > 30 ? "90%" : "100%"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import {useEffect} from "react";
|
||||
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// ref: https://developers.featurebase.app/install/changelog-widget/install
|
||||
export const FbChangelogButton = ({className}: Props) => {
|
||||
useEffect(() => {
|
||||
const win = window as any;
|
||||
|
||||
if (typeof win.Featurebase !== "function") {
|
||||
win.Featurebase = function () {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
(win.Featurebase.q = win.Featurebase.q || []).push(arguments);
|
||||
};
|
||||
}
|
||||
win.Featurebase("initialize_changelog_widget", {
|
||||
organization: process.env.NEXT_PUBLIC_FB_FEEDBACK_ORG,
|
||||
theme: "dark",
|
||||
usersName: "",
|
||||
fullscreenPopup: true,
|
||||
alwaysShow: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fbButtonOnClick = () => {
|
||||
(window as any).Featurebase("manually_open_changelog_popup");
|
||||
|
||||
trackEvent("Featurebase - Changelog", {
|
||||
name: "featurebase-changelog",
|
||||
action: "press",
|
||||
category: "featurebase",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={className} onClick={fbButtonOnClick}>
|
||||
Changelog <span id="fb-update-badge" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import {useEffect} from "react";
|
||||
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// ref: https://developers.featurebase.app/install/feedback-widget/setup
|
||||
export const FbFeedbackButton = ({className}: Props) => {
|
||||
useEffect(() => {
|
||||
const win = window as any;
|
||||
|
||||
if (typeof win.Featurebase !== "function") {
|
||||
win.Featurebase = function () {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
(win.Featurebase.q = win.Featurebase.q || []).push(arguments);
|
||||
};
|
||||
}
|
||||
win.Featurebase("initialize_feedback_widget", {
|
||||
organization: process.env.NEXT_PUBLIC_FB_FEEDBACK_ORG,
|
||||
theme: "dark",
|
||||
email: "",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fbButtonOnClick = () => {
|
||||
trackEvent("Featurebase - Feedback", {
|
||||
name: "featurebase-feedback",
|
||||
action: "press",
|
||||
category: "featurebase",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button data-featurebase-feedback className={className} onClick={fbButtonOnClick}>
|
||||
Feedback
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import NextLink from "next/link";
|
||||
import arrowRightUpIcon from "@iconify/icons-solar/arrow-right-up-linear";
|
||||
import {Icon} from "@iconify/react/dist/offline";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
innerClassName?: string;
|
||||
};
|
||||
|
||||
export const FbRoadmapLink = ({className, innerClassName}: Props) => {
|
||||
const fbLinkOnClick = () => {
|
||||
trackEvent("Featurebase - Roadmap", {
|
||||
name: "featurebase-roadmap",
|
||||
action: "press",
|
||||
category: "featurebase",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<NextLink
|
||||
className={clsx("inline-flex items-center", className)}
|
||||
color="foreground"
|
||||
href={`${process.env.NEXT_PUBLIC_FB_FEEDBACK_URL}/roadmap`}
|
||||
target="_blank"
|
||||
onClick={fbLinkOnClick}
|
||||
>
|
||||
<div className={clsx("relative", innerClassName)}>
|
||||
Roadmap
|
||||
<Icon
|
||||
className="absolute right-[-10px] top-0 outline-none transition-transform group-data-[hover=true]:translate-y-0.5 [&>path]:stroke-[2.5px]"
|
||||
icon={arrowRightUpIcon}
|
||||
width={10}
|
||||
/>
|
||||
</div>
|
||||
</NextLink>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import {Button, Link} from "@nextui-org/react";
|
||||
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
export const FigmaButton = () => (
|
||||
<Button
|
||||
isExternal
|
||||
showAnchorIcon
|
||||
as={Link}
|
||||
className="max-w-fit text-current"
|
||||
color="default"
|
||||
href="https://www.figma.com/community/file/1267584376234254760"
|
||||
variant="bordered"
|
||||
onPress={() => {
|
||||
trackEvent("FigmaPage - Open Figma Link", {
|
||||
action: "click",
|
||||
category: "figma",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Open in Figma
|
||||
</Button>
|
||||
);
|
||||
@@ -3,9 +3,8 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import {usePathname} from "next/navigation";
|
||||
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {getCurrentYear} from "@/utils/time";
|
||||
|
||||
const Link = dynamic(() => import("@nextui-org/react").then((mod) => mod.Link));
|
||||
const VercelCallout = dynamic(() => import("./vercel-callout").then((mod) => mod.VercelCallout));
|
||||
|
||||
export const Footer = () => {
|
||||
@@ -18,12 +17,7 @@ export const Footer = () => {
|
||||
return (
|
||||
<footer className="container mx-auto max-w-7xl pb-12 px-12">
|
||||
<div className="flex flex-col justify-center items-center gap-1">
|
||||
<p className="text-sm text-default-400">
|
||||
Created by
|
||||
<Link isExternal className="text-sm" href={siteConfig.links.portfolio}>
|
||||
Junior Garcia
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-sm text-default-400">© {getCurrentYear()} NextUI Inc.</p>
|
||||
<VercelCallout />
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const CLIBoldIcon = ({...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M3.464 3.464C2 4.93 2 7.286 2 12c0 4.714 0 7.071 1.464 8.535C4.93 22 7.286 22 12 22c4.714 0 7.071 0 8.535-1.465C22 19.072 22 16.714 22 12s0-7.071-1.465-8.536C19.072 2 16.714 2 12 2S4.929 2 3.464 3.464m2.96 6.056a.75.75 0 0 1 1.056-.096l.277.23c.605.504 1.12.933 1.476 1.328c.379.42.674.901.674 1.518s-.295 1.099-.674 1.518c-.356.395-.871.824-1.476 1.328l-.277.23a.75.75 0 1 1-.96-1.152l.234-.195c.659-.55 1.09-.91 1.366-1.216c.262-.29.287-.427.287-.513c0-.086-.025-.222-.287-.513c-.277-.306-.707-.667-1.366-1.216l-.234-.195a.75.75 0 0 1-.096-1.056M17.75 15a.75.75 0 0 1-.75.75h-5a.75.75 0 0 1 0-1.5h5a.75.75 0 0 1 .75.75"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -16,3 +16,4 @@ export * from "./hash";
|
||||
export * from "./more-square";
|
||||
export * from "./play";
|
||||
export * from "./pause";
|
||||
export * from "./cli";
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const CLILinearIcon = ({...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M2 12c0-4.714 0-7.071 1.464-8.536C4.93 2 7.286 2 12 2c4.714 0 7.071 0 8.535 1.464C22 4.93 22 7.286 22 12c0 4.714 0 7.071-1.465 8.535C19.072 22 16.714 22 12 22s-7.071 0-8.536-1.465C2 19.072 2 16.714 2 12Z" />
|
||||
<path
|
||||
d="M17 15h-5m-5-5l.234.195c1.282 1.068 1.923 1.602 1.923 2.305c0 .703-.64 1.237-1.923 2.305L7 15"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -20,3 +20,4 @@ export * from "./chevron-right";
|
||||
export * from "./search";
|
||||
export * from "./simple-grid";
|
||||
export * from "./rotate-left";
|
||||
export * from "./cli";
|
||||
|
||||
@@ -223,6 +223,44 @@ const AstroIcon: React.FC<IconSvgProps> = ({width = "1em", height = "1em", ...pr
|
||||
);
|
||||
};
|
||||
|
||||
const BunIcon: React.FC<IconSvgProps> = ({width = "1em", height = "1em", ...props}) => {
|
||||
return (
|
||||
<svg aria-hidden="true" height={height} viewBox="0 0 32 32" width={width} {...props}>
|
||||
<path
|
||||
d="M29 17c0 5.65-5.82 10.23-13 10.23S3 22.61 3 17c0-3.5 2.24-6.6 5.66-8.44S14.21 4.81 16 4.81s3.32 1.54 7.34 3.71C26.76 10.36 29 13.46 29 17"
|
||||
fill="#fbf0df"
|
||||
/>
|
||||
<path
|
||||
d="M16 27.65c7.32 0 13.46-4.65 13.46-10.65c0-3.72-2.37-7-5.89-8.85c-1.39-.75-2.46-1.41-3.37-2l-1.13-.69A6.14 6.14 0 0 0 16 4.35a6.88 6.88 0 0 0-3.3 1.23c-.42.24-.86.51-1.32.8c-.87.54-1.83 1.13-3 1.73C4.91 10 2.54 13.24 2.54 17c0 6 6.14 10.65 13.46 10.65Z"
|
||||
fill="none"
|
||||
stroke="#000"
|
||||
/>
|
||||
<ellipse cx="21.65" cy="18.62" fill="#febbd0" rx="2.17" ry="1.28" />
|
||||
<ellipse cx="10.41" cy="18.62" fill="#febbd0" rx="2.17" ry="1.28" />
|
||||
<path
|
||||
d="M11.43 18.11a2 2 0 1 0-2-2.05a2.05 2.05 0 0 0 2 2.05m9.2 0a2 2 0 1 0-2-2.05a2 2 0 0 0 2 2.05"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M10.79 16.19a.77.77 0 1 0-.76-.77a.76.76 0 0 0 .76.77m9.2 0a.77.77 0 1 0 0-1.53a.77.77 0 0 0 0 1.53"
|
||||
fill="#fff"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M18.62 19.67a3.3 3.3 0 0 1-1.09 1.75a2.48 2.48 0 0 1-1.5.69a2.53 2.53 0 0 1-1.5-.69a3.28 3.28 0 0 1-1.08-1.75a.26.26 0 0 1 .29-.3h4.58a.27.27 0 0 1 .3.3Z"
|
||||
fill="#b71422"
|
||||
stroke="#000"
|
||||
strokeWidth=".75"
|
||||
/>
|
||||
<path
|
||||
d="M14.93 5.75a6.09 6.09 0 0 1-2.09 4.62c-.1.09 0 .27.11.22c1.25-.49 2.94-1.94 2.23-4.88c-.03-.15-.25-.11-.25.04m.85 0a6 6 0 0 1 .57 5c0 .13.12.24.21.13c.83-1 1.54-3.11-.59-5.31c-.1-.11-.27.04-.19.17Zm1-.06a6.09 6.09 0 0 1 2.53 4.38c0 .14.21.17.24 0c.34-1.3.15-3.51-2.66-4.66c-.12-.02-.21.18-.09.27ZM9.94 9.55a6.27 6.27 0 0 0 3.89-3.33c.07-.13.28-.08.25.07c-.64 3-2.79 3.59-4.13 3.51c-.14-.01-.14-.21-.01-.25"
|
||||
fill="#ccbea7"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const NewNextJSIcon: React.FC<IconSvgProps> = ({width = "1em", height = "1em", ...props}) => {
|
||||
return (
|
||||
<svg aria-hidden="true" fill="none" focusable="false" height={height} width={width} {...props}>
|
||||
@@ -452,4 +490,5 @@ export {
|
||||
CodeSandboxIcon,
|
||||
JavascriptIcon,
|
||||
TypescriptIcon,
|
||||
BunIcon,
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,6 +6,8 @@ import {Card, CardHeader, CardBody, LinkProps, SlotsToClasses} from "@nextui-org
|
||||
import {useRouter} from "next/navigation";
|
||||
import {LinkIcon} from "@nextui-org/shared-icons";
|
||||
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
const styles = tv({
|
||||
slots: {
|
||||
base: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4",
|
||||
@@ -38,6 +40,13 @@ export const FeaturesGrid: React.FC<FeaturesGridProps> = ({features, classNames,
|
||||
const slots = styles();
|
||||
|
||||
const handleClick = (feat: Feature) => {
|
||||
trackEvent("FeaturesGrid - Click", {
|
||||
name: feat.title,
|
||||
action: "click",
|
||||
category: "docs",
|
||||
data: feat.href ?? "",
|
||||
});
|
||||
|
||||
if (!feat.href) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,12 +105,14 @@ export const FloatingComponents: React.FC<{}> = () => {
|
||||
|
||||
{isMounted && (
|
||||
<Tooltip
|
||||
showArrow
|
||||
className="text-sm animate-[levitate_14s_ease_infinite]"
|
||||
color="secondary"
|
||||
content="Developers love Next.js"
|
||||
isOpen={!isTablet}
|
||||
placement="top"
|
||||
style={{
|
||||
zIndex: 39,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className="absolute left-[200px] top-[160px] max-w-fit animate-[levitate_14s_ease_infinite_0.5s]"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import NextLink from "next/link";
|
||||
import {Button, Link} from "@nextui-org/react";
|
||||
import {Button, Link, Chip} from "@nextui-org/react";
|
||||
import {ArrowRightIcon} from "@nextui-org/shared-icons";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
@@ -9,15 +9,40 @@ import {FloatingComponents} from "./floating-components";
|
||||
|
||||
import {title, subtitle} from "@/components/primitives";
|
||||
import {GithubIcon} from "@/components/icons";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
const BgLooper = dynamic(() => import("./bg-looper").then((mod) => mod.BgLooper), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const Hero = () => {
|
||||
const handlePressAnnouncement = (name: string, url: string) => {
|
||||
trackEvent("NavbarItem", {
|
||||
name,
|
||||
action: "press",
|
||||
category: "home - gero",
|
||||
data: url,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="flex relative overflow-hidden lg:overflow-visible w-full flex-nowrap justify-between items-center h-[calc(100vh_-_64px)] 2xl:h-[calc(84vh_-_64px)]">
|
||||
<div className="flex relative z-20 flex-col gap-6 w-full lg:w-1/2 xl:mt-10">
|
||||
<div className="w-full flex justify-center md:hidden">
|
||||
<Chip
|
||||
as={NextLink}
|
||||
className="bg-default-100/50 hover:bg-default-100 border-default-200/80 dark:border-default-100/80 transition-colors cursor-pointer"
|
||||
color="default"
|
||||
href="/blog/v2.3.0"
|
||||
variant="dot"
|
||||
onClick={() => handlePressAnnouncement("Introducing v2.3.0", "/blog/v2.3.0")}
|
||||
>
|
||||
Introducing v2.3.0
|
||||
<span aria-label="tada emoji" role="img">
|
||||
🎉
|
||||
</span>
|
||||
</Chip>
|
||||
</div>
|
||||
<div className="text-center leading-8 md:leading-10 md:text-left">
|
||||
<div className="inline-block">
|
||||
<h1 className={title()}>Make </h1>
|
||||
@@ -42,6 +67,14 @@ export const Hero = () => {
|
||||
href="/docs/guide/introduction"
|
||||
radius="full"
|
||||
size="lg"
|
||||
onPress={() => {
|
||||
trackEvent("Hero - Get Started", {
|
||||
name: "Get Started",
|
||||
action: "click",
|
||||
category: "landing-page",
|
||||
data: "/docs/guide/introduction",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
@@ -56,6 +89,14 @@ export const Hero = () => {
|
||||
size="lg"
|
||||
startContent={<GithubIcon />}
|
||||
variant="bordered"
|
||||
onPress={() => {
|
||||
trackEvent("Hero - Github", {
|
||||
name: "Github",
|
||||
action: "click",
|
||||
category: "landing-page",
|
||||
data: "https://github.com/nextui-org/nextui",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Github
|
||||
</Button>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {FeaturesGrid} from "./features-grid";
|
||||
import {sectionWrapper, subtitle, title} from "@/components/primitives";
|
||||
import {GithubIcon, NoteLinearIcon, NextJsIcon} from "@/components/icons";
|
||||
import {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
const bannerSuggestions = [
|
||||
{
|
||||
@@ -69,6 +70,13 @@ export const InstallBanner = () => {
|
||||
href="/docs/guide/installation"
|
||||
radius="full"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
trackEvent("InstallBanner - Get Started", {
|
||||
action: "press",
|
||||
category: "landing-page",
|
||||
data: "/docs/guide/installation",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
@@ -81,6 +89,13 @@ export const InstallBanner = () => {
|
||||
size="md"
|
||||
startContent={<GithubIcon />}
|
||||
variant="bordered"
|
||||
onClick={() => {
|
||||
trackEvent("InstallBanner - Github", {
|
||||
action: "press",
|
||||
category: "landing-page",
|
||||
data: "https://github.com/nextui-org/nextui",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Github
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import {Link} from "@nextui-org/react";
|
||||
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
export type Sponsor = {
|
||||
name: string;
|
||||
href: string;
|
||||
logo: React.ReactNode;
|
||||
};
|
||||
|
||||
export const SponsorItem = ({name, href, logo}: Sponsor) => {
|
||||
return (
|
||||
<Link
|
||||
isExternal
|
||||
className="flex flex-col items-center justify-center"
|
||||
href={href}
|
||||
onClick={() => {
|
||||
trackEvent("Hero - Sponsors", {
|
||||
name,
|
||||
action: "click",
|
||||
category: "hero",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{logo}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import {Button, Link} from "@nextui-org/react";
|
||||
|
||||
import {sectionWrapper} from "@/components/primitives";
|
||||
import {RelumeLogo, Story2DesignLogo} from "@/components/icons/sponsors";
|
||||
import {HeartFilledIcon} from "@/components/icons";
|
||||
import {VercelIcon} from "@/components/icons/social";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {Sponsor, SponsorItem} from "@/components/marketing/sponsor-item";
|
||||
|
||||
const sponsors: Sponsor[] = [
|
||||
{
|
||||
name: "Relume",
|
||||
href: "https://library.relume.io/ai-site-builder?via=nextui",
|
||||
logo: <RelumeLogo className="text-black dark:text-white" />,
|
||||
},
|
||||
{
|
||||
name: "story.to.design",
|
||||
href: "https://story.to.design?utm_source=nextui&utm_marketing=partnership",
|
||||
logo: <Story2DesignLogo className="pt-1" />,
|
||||
},
|
||||
{
|
||||
name: "Vercel",
|
||||
href: "https://www.vercel.com?utm_source=nextui&utm_marketing=oss",
|
||||
logo: <VercelIcon className="text-black dark:text-white" height={24} />,
|
||||
},
|
||||
];
|
||||
|
||||
export const Sponsors = () => {
|
||||
return (
|
||||
<section className={sectionWrapper({class: "text-center mt-24 lg:mt-32"})}>
|
||||
<h3 className="text-large text-default-500">Supported and backed by</h3>
|
||||
<div className="w-full flex flex-wrap gap-x-5 gap-y-3 justify-center items-center">
|
||||
{sponsors.map((sponsor) => (
|
||||
<SponsorItem key={sponsor.name} {...sponsor} />
|
||||
))}
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="group text-sm border-dashed font-normal text-default-600 data-[hover=true]:bg-default-100/50"
|
||||
href={siteConfig.links.sponsor}
|
||||
startContent={
|
||||
<HeartFilledIcon className="text-danger group-data-[hover=true]:animate-heartbeat" />
|
||||
}
|
||||
variant="bordered"
|
||||
>
|
||||
Your Company
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import {OpenCollectiveIcon, PatreonIcon, HeartBoldIcon, PlusLinearIcon} from "@/
|
||||
import {Sponsor, SPONSOR_TIERS, SPONSOR_COLORS, getTier} from "@/libs/docs/sponsors";
|
||||
import {SonarPulse} from "@/components/sonar-pulse";
|
||||
import {useIsMobile} from "@/hooks/use-media-query";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
export interface SupportProps {
|
||||
sponsors: Sponsor[];
|
||||
@@ -100,6 +101,14 @@ export const Support: FC<SupportProps> = ({sponsors = []}) => {
|
||||
window.open(href, "_blank");
|
||||
};
|
||||
|
||||
const handleBecomeSponsor = () => {
|
||||
trackEvent("Support - Become a sponsor", {
|
||||
action: "click",
|
||||
category: "landing-page",
|
||||
});
|
||||
|
||||
handleExternalLinkClick(supportAccounts[0].href);
|
||||
};
|
||||
const renderSponsors = useMemo(() => {
|
||||
if (!sponsors.length) return null;
|
||||
|
||||
@@ -181,7 +190,7 @@ export const Support: FC<SupportProps> = ({sponsors = []}) => {
|
||||
aria-label="Become a sponsor"
|
||||
className="z-50 w-auto h-auto bg-gradient-to-b from-[#FF1CF7] to-[#7928CA]"
|
||||
radius="full"
|
||||
onPress={() => handleExternalLinkClick(supportAccounts[0].href)}
|
||||
onPress={handleBecomeSponsor}
|
||||
>
|
||||
<PlusLinearIcon
|
||||
className="flex items-center justify-center rounded-full text-white"
|
||||
|
||||
@@ -11,7 +11,8 @@ 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";
|
||||
import {VirtualAnchor, virtualAnchorEncode} from "@/components/virtual-anchor";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
const Table: React.FC<{children?: React.ReactNode}> = ({children}) => {
|
||||
return (
|
||||
@@ -104,7 +105,7 @@ 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">
|
||||
<Components.Code className="font-normal text-default-700 bg-default-200/50 dark:bg-default-100/60 px-2 py-0.5">
|
||||
{children}
|
||||
</Components.Code>
|
||||
);
|
||||
@@ -144,6 +145,12 @@ const Code = ({
|
||||
copyButton: "text-lg text-zinc-500 mr-2",
|
||||
}}
|
||||
codeString={codeString}
|
||||
onCopy={() => {
|
||||
trackEvent("MDXComponents - Copy", {
|
||||
category: "docs",
|
||||
action: "copyCode",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Codeblock codeString={codeString} language={language} metastring={meta} />
|
||||
</Components.Snippet>
|
||||
@@ -153,8 +160,21 @@ const Code = ({
|
||||
const Link = ({href, children}: {href?: string; children?: React.ReactNode}) => {
|
||||
const isExternal = href?.startsWith("http");
|
||||
|
||||
const handlePress = () => {
|
||||
trackEvent("MDXComponents - Click", {
|
||||
category: "docs",
|
||||
action: "click",
|
||||
data: href || "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Components.Link href={href} isExternal={isExternal} showAnchorIcon={isExternal}>
|
||||
<Components.Link
|
||||
href={href}
|
||||
isExternal={isExternal}
|
||||
showAnchorIcon={isExternal}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{children}
|
||||
</Components.Link>
|
||||
);
|
||||
@@ -208,7 +228,7 @@ export const MDXComponents = {
|
||||
),
|
||||
Steps: ({...props}) => (
|
||||
<div
|
||||
className="[&>h3]:step [&>h3>a]:pt-0.5 mb-12 ml-4 relative border-l border-default-100 pl-[1.625rem] [counter-reset:step]"
|
||||
className="[&>h3]:step [&>h3>a]:pt-0.5 [&>h4]:step [&>h4>a]:pt-0.5 mb-12 ml-4 relative border-l border-default-100 pl-[1.625rem] [counter-reset:step]"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -30,19 +30,16 @@ import {useEffect} from "react";
|
||||
import {usePress} from "@react-aria/interactions";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
|
||||
import {currentVersion} from "@/utils/version";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {LargeLogo, SmallLogo, ThemeSwitch} from "@/components";
|
||||
import {
|
||||
TwitterIcon,
|
||||
GithubIcon,
|
||||
DiscordIcon,
|
||||
HeartFilledIcon,
|
||||
SearchLinearIcon,
|
||||
} from "@/components/icons";
|
||||
import {TwitterIcon, GithubIcon, DiscordIcon, SearchLinearIcon} from "@/components/icons";
|
||||
import {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
import {DocsSidebar} from "@/components/docs/sidebar";
|
||||
import {useCmdkStore} from "@/components/cmdk";
|
||||
import {FbRoadmapLink} from "@/components/featurebase/fb-roadmap-link";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
export interface NavbarProps {
|
||||
routes: Route[];
|
||||
@@ -73,8 +70,17 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
setCommandKey(isAppleDevice() ? "command" : "ctrl");
|
||||
}, []);
|
||||
|
||||
const handleOpenCmdk = () => {
|
||||
cmdkStore.onOpen();
|
||||
trackEvent("Navbar - Search", {
|
||||
name: "navbar - search",
|
||||
action: "press",
|
||||
category: "cmdk",
|
||||
});
|
||||
};
|
||||
|
||||
const {pressProps} = usePress({
|
||||
onPress: () => cmdkStore.onOpen(),
|
||||
onPress: handleOpenCmdk,
|
||||
});
|
||||
const {focusProps, isFocusVisible} = useFocusRing();
|
||||
|
||||
@@ -100,7 +106,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
strokeWidth={2}
|
||||
/>
|
||||
}
|
||||
onPress={() => cmdkStore.onOpen()}
|
||||
onPress={handleOpenCmdk}
|
||||
>
|
||||
Quick Search...
|
||||
</Button>
|
||||
@@ -120,6 +126,15 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
}
|
||||
};
|
||||
|
||||
const handlePressNavbarItem = (name: string, url: string) => {
|
||||
trackEvent("NavbarItem", {
|
||||
name,
|
||||
action: "press",
|
||||
category: "navbar",
|
||||
data: url,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<NextUINavbar
|
||||
ref={ref}
|
||||
@@ -137,6 +152,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
aria-label="Home"
|
||||
className="flex justify-start items-center gap-2 tap-highlight-transparent transition-opacity active:opacity-50"
|
||||
href="/"
|
||||
onClick={() => handlePressNavbarItem("Home", "/")}
|
||||
>
|
||||
<SmallLogo className="w-6 h-6 md:hidden" />
|
||||
<LargeLogo className="h-5 md:h-6" />
|
||||
@@ -154,7 +170,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
v2.0.0
|
||||
v{currentVersion}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
</motion.div>
|
||||
@@ -166,7 +182,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
selectionMode="single"
|
||||
onAction={handleVersionChange}
|
||||
>
|
||||
<DropdownItem key="v2">v2.0.0</DropdownItem>
|
||||
<DropdownItem key="v2">v{currentVersion}</DropdownItem>
|
||||
<DropdownItem key="v1" endContent={<LinkIcon />}>
|
||||
v1.0.0
|
||||
</DropdownItem>
|
||||
@@ -183,6 +199,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
color="foreground"
|
||||
data-active={includes(docsPaths, pathname)}
|
||||
href="/docs/guide/introduction"
|
||||
onClick={() => handlePressNavbarItem("Docs", "/docs/guide/introduction")}
|
||||
>
|
||||
Docs
|
||||
</NextLink>
|
||||
@@ -193,6 +210,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
color="foreground"
|
||||
data-active={includes(pathname, "components")}
|
||||
href="/docs/components/avatar"
|
||||
onClick={() => handlePressNavbarItem("Components", "/docs/components/avatar")}
|
||||
>
|
||||
Components
|
||||
</NextLink>
|
||||
@@ -203,6 +221,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
color="foreground"
|
||||
data-active={includes(pathname, "blog")}
|
||||
href="/blog"
|
||||
onClick={() => handlePressNavbarItem("Blog", "/blog")}
|
||||
>
|
||||
Blog
|
||||
</NextLink>
|
||||
@@ -213,21 +232,40 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
color="foreground"
|
||||
data-active={includes(pathname, "figma")}
|
||||
href="/figma"
|
||||
onClick={() => handlePressNavbarItem("Figma", "/figma")}
|
||||
>
|
||||
Figma
|
||||
</NextLink>
|
||||
</NavbarItem>
|
||||
{/* hide feedback and changelog at this moment */}
|
||||
{/* <NavbarItem>
|
||||
<NextLink className={navLinkClasses} color="foreground" href="#">
|
||||
<FbChangelogButton key="changelog" userName="" />
|
||||
</NextLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NextLink className={navLinkClasses} color="foreground" href="#">
|
||||
<FbFeedbackButton key="feedback" userEmail="" />
|
||||
</NextLink>
|
||||
</NavbarItem> */}
|
||||
<NavbarItem>
|
||||
<FbRoadmapLink className={navLinkClasses} />
|
||||
</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"
|
||||
href="/blog/v2.2.0"
|
||||
variant="dot"
|
||||
onClick={() => handlePressNavbarItem("Introducing v2.2.0", "/blog/v2.2.0")}
|
||||
>
|
||||
Introducing NextUI v2.0
|
||||
Introducing v2.2.0
|
||||
<span aria-label="rocket emoji" role="img">
|
||||
🚀
|
||||
</span>
|
||||
</Chip>
|
||||
</NavbarItem>
|
||||
</NavbarItem> */}
|
||||
</ul>
|
||||
</NavbarContent>
|
||||
|
||||
@@ -238,6 +276,7 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
aria-label="Github"
|
||||
className="p-1"
|
||||
href="https://github.com/nextui-org/nextui"
|
||||
onClick={() => handlePressNavbarItem("Github", "https://github.com/nextui-org/nextui")}
|
||||
>
|
||||
<GithubIcon className="text-default-600 dark:text-default-500" />
|
||||
</Link>
|
||||
@@ -269,19 +308,52 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
|
||||
<NavbarContent className="hidden sm:flex basis-1/5 sm:basis-full" justify="end">
|
||||
<NavbarItem className="hidden sm:flex">
|
||||
<Link isExternal aria-label="Twitter" className="p-1" href={siteConfig.links.twitter}>
|
||||
<Chip
|
||||
as={NextLink}
|
||||
className="bg-default-100/50 hover:bg-default-100 border-default-200/80 dark:border-default-100/80 transition-colors cursor-pointer"
|
||||
color="default"
|
||||
href="/blog/v2.3.0"
|
||||
variant="dot"
|
||||
onClick={() => handlePressNavbarItem("Introducing v2.3.0", "/blog/v2.3.0")}
|
||||
>
|
||||
Introducing v2.3.0
|
||||
<span aria-label="tada emoji" role="img">
|
||||
🎉
|
||||
</span>
|
||||
</Chip>
|
||||
</NavbarItem>
|
||||
<NavbarItem className="hidden sm:flex">
|
||||
<Link
|
||||
isExternal
|
||||
aria-label="Twitter"
|
||||
className="p-1"
|
||||
href={siteConfig.links.twitter}
|
||||
onPress={() => handlePressNavbarItem("Twitter", siteConfig.links.twitter)}
|
||||
>
|
||||
<TwitterIcon className="text-default-600 dark:text-default-500" />
|
||||
</Link>
|
||||
<Link isExternal aria-label="Discord" className="p-1" href={siteConfig.links.discord}>
|
||||
<Link
|
||||
isExternal
|
||||
aria-label="Discord"
|
||||
className="p-1"
|
||||
href={siteConfig.links.discord}
|
||||
onPress={() => handlePressNavbarItem("Discord", siteConfig.links.discord)}
|
||||
>
|
||||
<DiscordIcon className="text-default-600 dark:text-default-500" />
|
||||
</Link>
|
||||
<Link isExternal aria-label="Github" className="p-1" href={siteConfig.links.github}>
|
||||
<Link
|
||||
isExternal
|
||||
aria-label="Github"
|
||||
className="p-1"
|
||||
href={siteConfig.links.github}
|
||||
onPress={() => handlePressNavbarItem("Github", siteConfig.links.github)}
|
||||
>
|
||||
<GithubIcon className="text-default-600 dark:text-default-500" />
|
||||
</Link>
|
||||
<ThemeSwitch />
|
||||
</NavbarItem>
|
||||
<NavbarItem className="hidden lg:flex">{searchButton}</NavbarItem>
|
||||
<NavbarItem className="hidden md:flex">
|
||||
{/* <NavbarItem className="hidden md:flex">
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
@@ -291,10 +363,11 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
<HeartFilledIcon className="text-danger group-data-[hover=true]:animate-heartbeat" />
|
||||
}
|
||||
variant="flat"
|
||||
onPress={() => handlePressNavbarItem("Sponsor", siteConfig.links.sponsor)}
|
||||
>
|
||||
Sponsor
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarItem> */}
|
||||
<NavbarMenuToggle
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="hidden sm:flex lg:hidden ml-4"
|
||||
@@ -302,7 +375,12 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarMenu>
|
||||
<DocsSidebar className="mt-4" routes={[...mobileRoutes, ...routes]} slug={slug} tag={tag} />
|
||||
<DocsSidebar
|
||||
className="mt-4 pt-8"
|
||||
routes={[...mobileRoutes, ...routes]}
|
||||
slug={slug}
|
||||
tag={tag}
|
||||
/>
|
||||
{children}
|
||||
</NavbarMenu>
|
||||
</NextUINavbar>
|
||||
|
||||
@@ -20,6 +20,7 @@ export const title = tv({
|
||||
sm: "text-3xl lg:text-4xl",
|
||||
md: "text-[2.5rem] lg:text-5xl",
|
||||
lg: "text-4xl lg:text-6xl",
|
||||
xl: "text-5xl md:text-6xl lg:text-7xl",
|
||||
},
|
||||
fullWidth: {
|
||||
true: "w-full block",
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import {Icon} from "@iconify/react/dist/offline";
|
||||
import arrowRightIcon from "@iconify/icons-solar/arrow-right-linear";
|
||||
import {usePathname} from "next/navigation";
|
||||
import {useEffect} from "react";
|
||||
|
||||
import {trackEvent} from "@/utils/va";
|
||||
import emitter from "@/libs/emitter";
|
||||
|
||||
const hideOnPaths = ["examples"];
|
||||
|
||||
export const ProBanner = () => {
|
||||
const handleClick = () => {
|
||||
trackEvent("NextUI Pro Banner", {
|
||||
action: "click",
|
||||
category: "landing-page",
|
||||
});
|
||||
};
|
||||
|
||||
const pathname = usePathname();
|
||||
const shouldBeVisible = !hideOnPaths.some((path) => pathname.includes(path));
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldBeVisible) return;
|
||||
|
||||
// listen to scroll event, dispatch an event when scroll is at the top < 48 px
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY < 48) {
|
||||
emitter.emit("proBannerVisibilityChange", "visible");
|
||||
} else {
|
||||
emitter.emit("proBannerVisibilityChange", "hidden");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [shouldBeVisible]);
|
||||
|
||||
if (!shouldBeVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="relative z-50 isolate flex items-center gap-x-6 overflow-hidden bg-background border-b-1 border-divider px-6 py-2 sm:px-3.5 sm:before:flex-1">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute left-[max(-7rem,calc(50%-52rem))] top-1/2 -z-10 -translate-y-1/2 transform-gpu blur-2xl"
|
||||
>
|
||||
<div
|
||||
className="aspect-[577/310] w-[36.0625rem] bg-gradient-to-r from-[#ff80b5] to-[#9089fc] dark:from-[#F54180] dark:to-[#338EF7] opacity-20 dark:opacity-10"
|
||||
style={{
|
||||
clipPath:
|
||||
"polygon(74.8% 41.9%, 97.2% 73.2%, 100% 34.9%, 92.5% 0.4%, 87.5% 0%, 75% 28.6%, 58.5% 54.6%, 50.1% 56.8%, 46.9% 44%, 48.3% 17.4%, 24.7% 53.9%, 0% 27.9%, 11.9% 74.2%, 24.9% 54.1%, 68.6% 100%, 74.8% 41.9%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute left-[max(45rem,calc(50%+8rem))] top-1/2 -z-10 -translate-y-1/2 transform-gpu blur-2xl"
|
||||
>
|
||||
<div
|
||||
className="aspect-[577/310] w-[36.0625rem] bg-gradient-to-r from-[#ff80b5] to-[#9089fc] dark:from-[#F54180] dark:to-[#338EF7] opacity-30 dark:opacity-20"
|
||||
style={{
|
||||
clipPath:
|
||||
"polygon(74.8% 41.9%, 97.2% 73.2%, 100% 34.9%, 92.5% 0.4%, 87.5% 0%, 75% 28.6%, 58.5% 54.6%, 50.1% 56.8%, 46.9% 44%, 48.3% 17.4%, 24.7% 53.9%, 0% 27.9%, 11.9% 74.2%, 24.9% 54.1%, 68.6% 100%, 74.8% 41.9%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between md:justify-center gap-x-3">
|
||||
<a
|
||||
className="text-small flex items-end sm:text-[0.93rem] text-foreground hover:opacity-80 transition-opacity"
|
||||
href="https://nextui.pro?utm_source=nextui.org&utm_medium=top-banner"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span aria-label="rocket" className="hidden md:block" role="img">
|
||||
🚀
|
||||
</span>
|
||||
<span
|
||||
className="inline-flex md:ml-1 animate-text-gradient font-medium bg-clip-text text-transparent bg-[linear-gradient(90deg,#D6009A_0%,#8a56cc_50%,#D6009A_100%)] dark:bg-[linear-gradient(90deg,#FFEBF9_0%,#8a56cc_50%,#FFEBF9_100%)]"
|
||||
style={{
|
||||
fontSize: "inherit",
|
||||
backgroundSize: "200%",
|
||||
backgroundClip: "text",
|
||||
WebkitBackgroundClip: "text",
|
||||
color: "transparent",
|
||||
}}
|
||||
>
|
||||
Ship faster with beautiful components
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
className="flex group min-w-[120px] items-center font-semibold text-foreground shadow-sm gap-1.5 relative overflow-hidden rounded-full p-[1px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
||||
href="https://nextui.pro?utm_source=nextui.org&utm_medium=top-banner"
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="absolute inset-[-1000%] animate-[spin_3s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#F54180_0%,#338EF7_50%,#F54180_100%)]" />
|
||||
<div className="inline-flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-background group-hover:bg-background/70 transition-background px-3 py-1 text-sm font-medium text-foreground backdrop-blur-3xl">
|
||||
NextUI Pro
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
className="outline-none transition-transform group-hover:translate-x-0.5 [&>path]:stroke-[2px]"
|
||||
icon={arrowRightIcon}
|
||||
width={16}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import {usePathname} from "next/navigation";
|
||||
import {Tooltip, Link, Button} from "@nextui-org/react";
|
||||
import {Tooltip, Button} from "@nextui-org/react";
|
||||
import {capitalize, last} from "lodash";
|
||||
|
||||
import {BugIcon} from "@/components/icons";
|
||||
@@ -11,6 +11,10 @@ export const BugReportButton = () => {
|
||||
|
||||
const componentTitle = capitalize(last(pathname?.split("/")));
|
||||
|
||||
const handlePress = () => {
|
||||
window.open(`${ISSUE_REPORT_URL}${componentTitle}`, "_blank");
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
className="text-xs px-2"
|
||||
@@ -19,15 +23,7 @@ export const BugReportButton = () => {
|
||||
placement="top"
|
||||
radius="md"
|
||||
>
|
||||
<Button
|
||||
isExternal
|
||||
isIconOnly
|
||||
as={Link}
|
||||
href={`${ISSUE_REPORT_URL}${componentTitle}`}
|
||||
size="sm"
|
||||
title="Report a bug"
|
||||
variant="light"
|
||||
>
|
||||
<Button isIconOnly size="sm" title="Report a bug" variant="light" onPress={handlePress}>
|
||||
<BugIcon className="text-white dark:text-zinc-500" height={16} width={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const getFileEntry = (theme: string) => `
|
||||
export const rootFile = `
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { NextUIProvider } from "@nextui-org/react";
|
||||
@@ -8,13 +8,26 @@ import "./styles.css";
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<NextUIProvider>
|
||||
<div className="w-screen h-screen ${theme} text-foreground bg-background p-8 flex items-start justify-center">
|
||||
<div className="w-screen h-screen p-8 flex items-start justify-center">
|
||||
<App />
|
||||
</div>
|
||||
</NextUIProvider>
|
||||
</React.StrictMode>
|
||||
);`;
|
||||
|
||||
export const getHtmlFile = (theme: string, entryFile: string) => `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body class="${theme} text-foreground bg-background">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/${entryFile}"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
export const tailwindConfig = `const { nextui } = require("@nextui-org/react");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
@@ -26,6 +26,7 @@ export const Sandpack: FC<SandpackProps> = ({
|
||||
files: filesProp,
|
||||
template,
|
||||
highlightedLines,
|
||||
typescriptStrict = false,
|
||||
showPreview = false,
|
||||
showEditor = true,
|
||||
defaultExpanded = false,
|
||||
@@ -41,6 +42,7 @@ export const Sandpack: FC<SandpackProps> = ({
|
||||
useSandpack({
|
||||
files: filesProp,
|
||||
template,
|
||||
typescriptStrict,
|
||||
highlightedLines,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,24 +1,24 @@
|
||||
import {useMemo} from "react";
|
||||
import {SandpackFiles, SandpackPredefinedTemplate} from "@codesandbox/sandpack-react";
|
||||
import {useTheme} from "next-themes";
|
||||
import {useLocalStorage} from "usehooks-ts";
|
||||
|
||||
import {HighlightedLines} from "./types";
|
||||
import {getHighlightedLines, getFileName} from "./utils";
|
||||
import {getFileEntry, stylesConfig, postcssConfig, tailwindConfig} from "./entries";
|
||||
|
||||
import {useLocalStorage} from "@/hooks/use-local-storage";
|
||||
import {stylesConfig, postcssConfig, tailwindConfig, getHtmlFile, rootFile} from "./entries";
|
||||
|
||||
export interface UseSandpackProps {
|
||||
files?: SandpackFiles;
|
||||
typescriptStrict?: boolean;
|
||||
template?: SandpackPredefinedTemplate;
|
||||
highlightedLines?: HighlightedLines;
|
||||
}
|
||||
|
||||
const importReact = 'import React from "react";';
|
||||
const importAllReact = 'import * as React from "react";';
|
||||
|
||||
export const useSandpack = ({
|
||||
files = {},
|
||||
typescriptStrict = false,
|
||||
template = "vite-react",
|
||||
highlightedLines,
|
||||
}: UseSandpackProps) => {
|
||||
@@ -57,6 +57,12 @@ export const useSandpack = ({
|
||||
if (key.includes("App") && !key.includes(mimeType)) {
|
||||
return acc;
|
||||
}
|
||||
if (typescriptStrict && currentTemplate === "vite-react-ts" && key.includes(".js")) {
|
||||
return acc;
|
||||
}
|
||||
if (currentTemplate === "vite-react" && key.includes(".ts")) {
|
||||
return acc;
|
||||
}
|
||||
// @ts-ignore
|
||||
acc[key] = files[key];
|
||||
|
||||
@@ -64,7 +70,7 @@ export const useSandpack = ({
|
||||
}, {});
|
||||
|
||||
let dependencies = {
|
||||
"framer-motion": "10.12.16",
|
||||
"framer-motion": "11.0.22",
|
||||
"@nextui-org/react": "latest",
|
||||
};
|
||||
|
||||
@@ -76,6 +82,11 @@ export const useSandpack = ({
|
||||
const aName = getFileName(a);
|
||||
const bName = getFileName(b);
|
||||
|
||||
// if bName includes "App" should be first
|
||||
if (bName.includes("App")) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (aFile?.includes(bName)) {
|
||||
return -1;
|
||||
}
|
||||
@@ -89,7 +100,7 @@ export const useSandpack = ({
|
||||
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)) {
|
||||
if (!fileContent.includes("from 'react'") && !fileContent.includes('from "react"')) {
|
||||
fileContent = `${importReact}\n${fileContent}\n`;
|
||||
}
|
||||
|
||||
@@ -128,7 +139,7 @@ export const useSandpack = ({
|
||||
|
||||
// const dependencies = useMemo(() => {
|
||||
// let deps = {
|
||||
// "framer-motion": "10.12.16",
|
||||
// "framer-motion": "11.0.22",
|
||||
// };
|
||||
|
||||
// if (hasComponents) {
|
||||
@@ -173,7 +184,11 @@ export const useSandpack = ({
|
||||
files: {
|
||||
...sortedFiles,
|
||||
[entryFile]: {
|
||||
code: getFileEntry(theme ?? "light"),
|
||||
code: rootFile,
|
||||
hidden: true,
|
||||
},
|
||||
"index.html": {
|
||||
code: getHtmlFile(theme ?? "light", entryFile),
|
||||
hidden: true,
|
||||
},
|
||||
"tailwind.config.js": {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
import {Analytics} from "@vercel/analytics/react";
|
||||
import Script from "next/script";
|
||||
|
||||
import {__PROD__} from "@/utils";
|
||||
|
||||
export function ScriptProviders() {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
id="featurebase-sdk"
|
||||
src="https://do.featurebase.app/js/sdk.js"
|
||||
strategy={"beforeInteractive"}
|
||||
/>
|
||||
<Script
|
||||
defer
|
||||
data-modal-disclaimer="This is a custom LLM for NextUI with access to all developer docs (nextui.org/docs) and GitHub Issues and PRs (github.com/nextui-org/nextui)."
|
||||
data-modal-example-questions="How do I install for Next.js?,How do I customize primary color?"
|
||||
data-project-color="#000000"
|
||||
data-project-logo="https://avatars.githubusercontent.com/u/86160567?s=280&v=4"
|
||||
data-project-name="NextUI"
|
||||
data-website-id="e733a73f-980e-4f7d-9e8b-91867453f899"
|
||||
src="https://widget.kapa.ai/kapa-widget.bundle.js"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
{__PROD__ && <Analytics />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import {cn} from "@nextui-org/react";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({className, children, ...props}, ref) => {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] pb-28">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
});
|
||||
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({className, orientation = "vertical", ...props}, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" && "h-2.5 border-t border-t-transparent p-[1px]",
|
||||
className,
|
||||
)}
|
||||
orientation={orientation}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
className={cn(
|
||||
"relative rounded-full bg-default-400/50",
|
||||
orientation === "vertical" && "flex-1",
|
||||
)}
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export {ScrollArea, ScrollBar};
|
||||
@@ -8,6 +8,7 @@ import {clsx} from "@nextui-org/shared-utils";
|
||||
import {useIsSSR} from "@react-aria/ssr";
|
||||
|
||||
import {SunFilledIcon, MoonFilledIcon} from "@/components/icons";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
export interface ThemeSwitchProps {
|
||||
className?: string;
|
||||
@@ -20,6 +21,12 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({className, classNames}) => {
|
||||
|
||||
const onChange = () => {
|
||||
theme === "light" ? setTheme("dark") : setTheme("light");
|
||||
|
||||
trackEvent("ThemeChange", {
|
||||
action: "click",
|
||||
category: "theme",
|
||||
data: theme === "light" ? "dark" : "light",
|
||||
});
|
||||
};
|
||||
|
||||
const {Component, slots, isSelected, getBaseProps, getInputProps, getWrapperProps} = useSwitch({
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {Link} from "@nextui-org/react";
|
||||
|
||||
import {VercelIcon} from "@/components/icons";
|
||||
import {trackEvent} from "@/utils/va";
|
||||
|
||||
export const VercelCallout: React.FC<unknown> = () => {
|
||||
return (
|
||||
@@ -9,6 +12,13 @@ export const VercelCallout: React.FC<unknown> = () => {
|
||||
isExternal
|
||||
className="flex justify-end items-center gap-2 text-foreground"
|
||||
href="https://www.vercel.com?utm_source=nextui&utm_marketing=oss"
|
||||
onClick={() => {
|
||||
trackEvent("VercelCallout", {
|
||||
name: "vercel callout",
|
||||
action: "click",
|
||||
category: "footer",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<p className="font-normal">Deployed on</p>
|
||||
<VercelIcon className="text-black dark:text-white" height={18} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {DM_Sans} from "next/font/google";
|
||||
import {Inter} from "next/font/google";
|
||||
|
||||
export const fontSans = DM_Sans({
|
||||
export const fontSans = Inter({
|
||||
variable: "--font-sans",
|
||||
adjustFontFallback: true,
|
||||
display: "optional",
|
||||
|
||||
+134
-21
@@ -18,12 +18,25 @@
|
||||
"keywords": "getting started, installation, start, nextui",
|
||||
"path": "/docs/guide/installation.mdx"
|
||||
},
|
||||
{
|
||||
"key": "cli",
|
||||
"title": "CLI",
|
||||
"keywords": "cli, command line interface",
|
||||
"path": "/docs/guide/cli.mdx",
|
||||
"newPost": true
|
||||
},
|
||||
{
|
||||
"key": "design-principles",
|
||||
"title": "Design Principles",
|
||||
"keywords": "design principles, nextui",
|
||||
"path": "/docs/guide/design-principles.mdx"
|
||||
},
|
||||
{
|
||||
"key": "routing",
|
||||
"title": "Routing",
|
||||
"keywords": "client side routing, routing, browser routing, nextui, next.js router, react router, remix router",
|
||||
"path": "/docs/guide/routing.mdx"
|
||||
},
|
||||
{
|
||||
"key": "upgrade-to-v2",
|
||||
"title": "Upgrade to v2",
|
||||
@@ -78,7 +91,8 @@
|
||||
{
|
||||
"key": "layout",
|
||||
"title": "Layout",
|
||||
"path": "/docs/customization/layout.mdx"
|
||||
"path": "/docs/customization/layout.mdx",
|
||||
"updated": true
|
||||
},
|
||||
{
|
||||
"key": "colors",
|
||||
@@ -130,6 +144,12 @@
|
||||
"keywords": "accordion, collapse, expandable sections, content hiding",
|
||||
"path": "/docs/components/accordion.mdx"
|
||||
},
|
||||
{
|
||||
"key": "autocomplete",
|
||||
"title": "Autocomplete",
|
||||
"keywords": "autocomplete, auto suggest, search, typeahead",
|
||||
"path": "/docs/components/autocomplete.mdx"
|
||||
},
|
||||
{
|
||||
"key": "badge",
|
||||
"title": "Badge",
|
||||
@@ -142,6 +162,12 @@
|
||||
"keywords": "button, interactive, action trigger, click events",
|
||||
"path": "/docs/components/button.mdx"
|
||||
},
|
||||
{
|
||||
"key": "breadcrumbs",
|
||||
"title": "Breadcrumbs",
|
||||
"keywords": "breadcrumbs, navigation, path, trail, location",
|
||||
"path": "/docs/components/breadcrumbs.mdx"
|
||||
},
|
||||
{
|
||||
"key": "card",
|
||||
"title": "Card",
|
||||
@@ -164,7 +190,32 @@
|
||||
"key": "chip",
|
||||
"title": "Chip",
|
||||
"keywords": "chip, tag, label, small actionable entity",
|
||||
"path": "/docs/components/chip.mdx",
|
||||
"path": "/docs/components/chip.mdx"
|
||||
},
|
||||
{
|
||||
"key": "code",
|
||||
"title": "Code",
|
||||
"keywords": "code, code snippet, inline code, coding",
|
||||
"path": "/docs/components/code.mdx"
|
||||
},
|
||||
{
|
||||
"key": "input",
|
||||
"title": "Input",
|
||||
"keywords": "input, text box, form field, data entry",
|
||||
"path": "/docs/components/input.mdx"
|
||||
},
|
||||
{
|
||||
"key": "date-input",
|
||||
"title": "Date Input",
|
||||
"keywords": "date-input, time, input, timezone",
|
||||
"path": "/docs/components/date-input.mdx",
|
||||
"newPost": true
|
||||
},
|
||||
{
|
||||
"key": "time-input",
|
||||
"title": "Time Input",
|
||||
"keywords": "timeinput, time, input, timezone",
|
||||
"path": "/docs/components/time-input.mdx",
|
||||
"newPost": true
|
||||
},
|
||||
{
|
||||
@@ -174,17 +225,38 @@
|
||||
"path": "/docs/components/circular-progress.mdx"
|
||||
},
|
||||
{
|
||||
"key": "code",
|
||||
"title": "Code",
|
||||
"keywords": "code, code snippet, inline code, coding",
|
||||
"path": "/docs/components/code.mdx"
|
||||
"key": "calendar",
|
||||
"title": "Calendar",
|
||||
"keywords": "calendar, date picker, month picker, year picker",
|
||||
"path": "/docs/components/calendar.mdx",
|
||||
"newPost": true
|
||||
},
|
||||
{
|
||||
"key": "range-calendar",
|
||||
"title": "Range Calendar",
|
||||
"keywords": "range calendar, date picker, month picker, year picker",
|
||||
"path": "/docs/components/range-calendar.mdx",
|
||||
"newPost": true
|
||||
},
|
||||
{
|
||||
"key": "date-picker",
|
||||
"title": "Date Picker",
|
||||
"keywords": "date-picker, time, input, timezone",
|
||||
"path": "/docs/components/date-picker.mdx",
|
||||
"newPost": true
|
||||
},
|
||||
{
|
||||
"key": "date-range-picker",
|
||||
"title": "Date Range Picker",
|
||||
"keywords": "date-range-picker, date-picker, time, input, timezone",
|
||||
"path": "/docs/components/date-range-picker.mdx",
|
||||
"newPost": true
|
||||
},
|
||||
{
|
||||
"key": "divider",
|
||||
"title": "Divider",
|
||||
"keywords": "divider, boundary, separator, section divider",
|
||||
"path": "/docs/components/divider.mdx",
|
||||
"newPost": true
|
||||
"path": "/docs/components/divider.mdx"
|
||||
},
|
||||
{
|
||||
"key": "dropdown",
|
||||
@@ -198,18 +270,11 @@
|
||||
"keywords": "image, media, picture, photo, graphic display",
|
||||
"path": "/docs/components/image.mdx"
|
||||
},
|
||||
{
|
||||
"key": "input",
|
||||
"title": "Input",
|
||||
"keywords": "input, text box, form field, data entry",
|
||||
"path": "/docs/components/input.mdx"
|
||||
},
|
||||
{
|
||||
"key": "kbd",
|
||||
"title": "Kbd",
|
||||
"keywords": "keyboard input, shortcut, keys, user input display",
|
||||
"path": "/docs/components/kbd.mdx",
|
||||
"newPost": true
|
||||
"path": "/docs/components/kbd.mdx"
|
||||
},
|
||||
{
|
||||
"key": "link",
|
||||
@@ -217,6 +282,12 @@
|
||||
"keywords": "link, navigation, href, web page connection",
|
||||
"path": "/docs/components/link.mdx"
|
||||
},
|
||||
{
|
||||
"key": "listbox",
|
||||
"title": "Listbox",
|
||||
"keywords": "listbox, selection, option list, multiple choice",
|
||||
"path": "/docs/components/listbox.mdx"
|
||||
},
|
||||
{
|
||||
"key": "modal",
|
||||
"title": "Modal",
|
||||
@@ -253,19 +324,29 @@
|
||||
"keywords": "radio group, selection set, option selection, exclusive choices",
|
||||
"path": "/docs/components/radio-group.mdx"
|
||||
},
|
||||
{
|
||||
"key": "select",
|
||||
"title": "Select",
|
||||
"keywords": "select, selection, option list, multiple choice",
|
||||
"path": "/docs/components/select.mdx"
|
||||
},
|
||||
{
|
||||
"key": "skeleton",
|
||||
"title": "Skeleton",
|
||||
"keywords": "skeleton, loading state, placeholder, content preview",
|
||||
"path": "/docs/components/skeleton.mdx",
|
||||
"newPost": true
|
||||
"path": "/docs/components/skeleton.mdx"
|
||||
},
|
||||
{
|
||||
"key": "snippet",
|
||||
"title": "Snippet",
|
||||
"keywords": "snippet, code block, programming, code example",
|
||||
"path": "/docs/components/snippet.mdx",
|
||||
"newPost": true
|
||||
"path": "/docs/components/snippet.mdx"
|
||||
},
|
||||
{
|
||||
"key": "scroll-shadow",
|
||||
"title": "Scroll Shadow",
|
||||
"keywords": "scroll shadow, scroll indicator, scroll bar, scroll position",
|
||||
"path": "/docs/components/scroll-shadow.mdx"
|
||||
},
|
||||
{
|
||||
"key": "spacer",
|
||||
@@ -285,6 +366,12 @@
|
||||
"keywords": "switch, toggle, binary input, on/off control",
|
||||
"path": "/docs/components/switch.mdx"
|
||||
},
|
||||
{
|
||||
"key": "slider",
|
||||
"title": "Slider",
|
||||
"keywords": "slider, range input, value selector, sliding control",
|
||||
"path": "/docs/components/slider.mdx"
|
||||
},
|
||||
{
|
||||
"key": "table",
|
||||
"title": "Table",
|
||||
@@ -296,7 +383,7 @@
|
||||
"title": "Tabs",
|
||||
"keywords": "tabs, section navigation, categorized content, tabbed interface",
|
||||
"path": "/docs/components/tabs.mdx",
|
||||
"newPost": true
|
||||
"updated": true
|
||||
},
|
||||
{
|
||||
"key": "textarea",
|
||||
@@ -317,6 +404,26 @@
|
||||
"path": "/docs/components/user.mdx"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "api-references",
|
||||
"title": "API References",
|
||||
"defaultOpen": true,
|
||||
"keywords": "api references, nextui, api",
|
||||
"routes": [
|
||||
{
|
||||
"key": "cli-api",
|
||||
"title": "NextUI CLI",
|
||||
"keywords": "api references, nextui, api, cli",
|
||||
"path": "/docs/api-references/cli-api.mdx"
|
||||
},
|
||||
{
|
||||
"key": "nextui-provider",
|
||||
"title": "NextUI Provider",
|
||||
"keywords": "api references, nextui, api, nextui provider",
|
||||
"path": "/docs/api-references/nextui-provider.mdx"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"mobileRoutes": [
|
||||
@@ -349,6 +456,12 @@
|
||||
"title": "Figma",
|
||||
"keywords": "figma, nextui, design, ui kit",
|
||||
"path": "/figma"
|
||||
},
|
||||
{
|
||||
"key": "roadmap",
|
||||
"title": "Roadmap",
|
||||
"keywords": "roadmap, nextui",
|
||||
"path": "#"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+5366
-1256
File diff suppressed because it is too large
Load Diff
@@ -175,7 +175,7 @@ We've added a new function, `extendVariants`, that allows you to customize any N
|
||||
height={350}
|
||||
/>
|
||||
|
||||
You can create or override the component `variants`, `defaultVariants` and `componentVariants`.
|
||||
You can create or override the component `variants`, `defaultVariants` and `compoundVariants`.
|
||||
|
||||
> Check out the [Custom Variants](/docs/customization/custom-variants) documentation for more information.
|
||||
|
||||
@@ -310,3 +310,16 @@ file helps streamline the process of transforming design ideas into functioning
|
||||
To start using **NextUI v2.0**, head over to our [installation guide](/docs/guide/installation).
|
||||
|
||||
We can't wait to see the amazing things you'll build with **NextUI v2.0**!
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
We're excited to see the community adopt NextUI, raise issues, and provide feedback.
|
||||
Whether it's a feature request, bug report, or a project to showcase, please get involved!
|
||||
|
||||
<Community />
|
||||
|
||||
## Contributing
|
||||
|
||||
PR's on NextUI are always welcome, please see our [contribution guidelines](https://github.com/nextui-org/nextui/blob/main/CONTRIBUTING.md) to learn how you can contribute to this project.
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
title: "New components v2.1.0 🎉"
|
||||
description: "NextUI v2.1.0 is here with new components, Select, Listbox and ScrollShadow."
|
||||
date: "2023-08-21"
|
||||
image: "/blog/v2.1.0.jpg"
|
||||
tags: ["nextui", "select", "listbox", "scroll-shadow", "multi-select"]
|
||||
author:
|
||||
name: "Junior Garcia"
|
||||
username: "@jrgarciadev"
|
||||
link: "https://twitter.com/jrgarciadev"
|
||||
avatar: "/avatars/junior-garcia.jpeg"
|
||||
---
|
||||
|
||||
import {selectContent} from "@/content/components/select";
|
||||
import {listboxContent} from "@/content/components/listbox";
|
||||
import {scrollShadowContent} from "@/content/components/scroll-shadow";
|
||||
|
||||
|
||||
<img
|
||||
src="/blog/v2.1.0.jpg"
|
||||
width={700}
|
||||
height={350}
|
||||
alt="NextUI v2"
|
||||
className="w-full border border-transparent dark:border-default-200/50 object-fit rounded-xl shadow-lg"
|
||||
/>
|
||||
|
||||
We are thrilled to announce the latest update to NextUI, version **2.1.0**! This release introduces some game-changing
|
||||
additions that many of you have eagerly been waiting for.
|
||||
|
||||
First on the list is the highly-anticipated **Select** component. Fully customizable and beautifully designed, supports both single and
|
||||
multi-select modes and is accessible out of the box.
|
||||
|
||||
But that's not all. We're also rolling out two more incredible components **Listbox** and **ScrollShadow**. The new
|
||||
**Listbox** allows you to make list manipulations more efficient and visually appealing. Meanwhile, the
|
||||
**ScrollShadow** component adds an elegant shadow effect to scrollable areas, enhancing the UI aesthetics while
|
||||
also improving usability.
|
||||
|
||||
## Select
|
||||
|
||||
Creating a select component that is both accessible and customizable is a challenging task. We've spent a lot of time
|
||||
researching and testing different approaches to come up with a solution that works for everyone. The result is a
|
||||
component that is easy to use, fully accessible, and highly customizable.
|
||||
|
||||
The new **Select** component includes:
|
||||
|
||||
- Support for selecting a single option.
|
||||
- Support for selecting multiple options.
|
||||
- Support for disabled options.
|
||||
- Support for sections.
|
||||
- Labeling support for accessibility.
|
||||
- Exposed to assistive technology as a button with a listbox popup using ARIA (combined with [Listbox](/docs/components/listbox)).
|
||||
- Support for description and error message help text linked to the input via ARIA.
|
||||
- Support for mouse, touch, and keyboard interactions.
|
||||
- Tab stop focus management.
|
||||
- Asynchronous options loading.
|
||||
- Keyboard support for opening the listbox using the arrow keys, including automatically focusing the first or last item accordingly.
|
||||
- Typeahead to allow selecting options by typing text, even without opening the listbox.
|
||||
- Browser autofill integration via a hidden native `<select>` element.
|
||||
- Support for mobile form navigation via software keyboard.
|
||||
- Mobile screen reader listbox dismissal support.
|
||||
- And much more...
|
||||
|
||||
### Single Select
|
||||
|
||||
The single select component is used to select a single option from a list of options. It is a combination of a button
|
||||
and a listbox. The button displays the currently selected option and the listbox displays the available options.
|
||||
|
||||
<CodeDemo title="Usage" files={selectContent.usage} />
|
||||
|
||||
### Multiple Select
|
||||
|
||||
The multiple select component can be used to select multiple options from a list of options.
|
||||
|
||||
You only need to pass the `selectionMode="multiple"` prop to the `Select` component.
|
||||
|
||||
<CodeDemo title="Multiple Selection" files={selectContent.multiple} />
|
||||
|
||||
### Multiple Variants
|
||||
|
||||
The select component comes with multiple variants.
|
||||
|
||||
<CodeDemo title="Variants" files={selectContent.variants} />
|
||||
|
||||
|
||||
### Chips Support
|
||||
|
||||
The select component is flexible and allows you to render any component as an option and as a selected option.
|
||||
|
||||
<CodeDemo title="Multiple Selection with Chips" files={selectContent.multipleWithChips} />
|
||||
|
||||
|
||||
### Customizable
|
||||
|
||||
The select component is highly customizable, you can customize the selected option, the options, the listbox,
|
||||
the popover and the scrollable area.
|
||||
|
||||
<CodeDemo title="Custom Styles" files={selectContent.customStyles} />
|
||||
|
||||
|
||||
Go to the [Select](/docs/components/select) component page to learn more about sizes, colors, and more.
|
||||
|
||||
|
||||
## Listbox
|
||||
|
||||
The listbox component allows you to make list manipulations more efficient and visually appealing.
|
||||
|
||||
The new **Listbox** component includes:
|
||||
|
||||
- Support for single, multiple, or no selection.
|
||||
- Exposed to assistive technology as a `listbox` using ARIA.
|
||||
- Support for disabled items.
|
||||
- Support for sections.
|
||||
- Labeling support for accessibility.
|
||||
- Support for mouse, touch, and keyboard interactions.
|
||||
- Tab stop focus management.
|
||||
- Keyboard navigation support including arrow keys, home/end, page up/down, select all, and clear.
|
||||
- Automatic scrolling support during keyboard navigation.
|
||||
- Typeahead to allow focusing options by typing text.
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeDemo title="Usage" files={listboxContent.usage} />
|
||||
|
||||
### Custom Styles
|
||||
|
||||
The Listbox components offers multiple customization options.
|
||||
|
||||
<CodeDemo title="Custom Styles" files={listboxContent.customStyles} />
|
||||
|
||||
> **Note**: In the above example, we've utilized the [Boxicons](https://boxicons.com/) icons collection.
|
||||
|
||||
Go to the [Listbox](/docs/components/listbox) component page to learn more about it.
|
||||
|
||||
|
||||
## ScrollShadow
|
||||
|
||||
The ScrollShadow component gives a nice shadow effect to scrollable areas. These shadows are handled by using
|
||||
the CSS `mask-image` property, which makes the shadows adapt to the background color.
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeDemo title="Usage" files={scrollShadowContent.usage} />
|
||||
|
||||
You can hide the scrollbars, customize the shadows size, change the orientation, and more.
|
||||
|
||||
Go to the [ScrollShadow](/docs/components/scroll-shadow) component page to learn more about it.
|
||||
|
||||
|
||||
<Spacer y={6}/>
|
||||
|
||||
We hope you enjoy these new components and the new features. We're excited to see what you build with them!
|
||||
|
||||
Thanks for reading and happy coding! 🚀
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
We're excited to see the community adopt NextUI, raise issues, and provide feedback.
|
||||
Whether it's a feature request, bug report, or a project to showcase, please get involved!
|
||||
|
||||
<Community />
|
||||
|
||||
## Contributing
|
||||
|
||||
PR's on NextUI are always welcome, please see our [contribution guidelines](https://github.com/nextui-org/nextui/blob/main/CONTRIBUTING.md) to learn how you can contribute to this project.
|
||||
@@ -0,0 +1,443 @@
|
||||
---
|
||||
title: "Introducing v2.2.0 🚀"
|
||||
description: "NextUI v2.2.0 is here! Dive into client-side router support, discover 3 new components including the Autocomplete, and more."
|
||||
date: "2023-11-03"
|
||||
image: "/blog/v2.2.0.jpg"
|
||||
tags: ["nextui", "autocomplete", "breadcrumbs", "client side router", "slider"]
|
||||
author:
|
||||
name: "Junior Garcia"
|
||||
username: "@jrgarciadev"
|
||||
link: "https://twitter.com/jrgarciadev"
|
||||
avatar: "/avatars/junior-garcia.jpeg"
|
||||
---
|
||||
|
||||
import {autocompleteContent} from "@/content/components/autocomplete";
|
||||
import {sliderContent} from "@/content/components/slider";
|
||||
import {breadcrumbsContent} from "@/content/components/breadcrumbs";
|
||||
|
||||
<img
|
||||
src="/blog/v2.2.0_2x.jpg"
|
||||
width={700}
|
||||
height={350}
|
||||
alt="NextUI v2.2.0"
|
||||
className="w-full border border-transparent dark:border-default-200/50 object-fit rounded-xl shadow-lg"
|
||||
/>
|
||||
|
||||
We are excited to announce the latest update to NextUI, version **2.2.0**! This release introduces 3 new components,
|
||||
support for client-side routing, and more.
|
||||
|
||||
## What's New in v2.2.0?
|
||||
|
||||
- [Client Side Routing](#client-side-routing) - Allows you to seamlessly incorporate client-side routers.
|
||||
- [Autocomplete](/docs/components/autocomplete) - Combines a text input with a listbox, allowing users to filter a list of options.
|
||||
- [Slider](/docs/components/slider) - Allows users to make selections from a range of values.
|
||||
- [Breadcrumbs](/docs/components/breadcrumbs) - Displays a hierarchy of links to the current page or resource in an application.
|
||||
- [Other Changes](#other-changes) - Includes styling improvements, accessibility and usability enhancements.
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
## Client Side Routing
|
||||
|
||||
By default, links perform native browser navigation when they are interacted with. However, many apps and
|
||||
frameworks use client side routers to avoid a full page reload when navigating between pages.
|
||||
|
||||
NextUI now natively supports client-side routing in components such as [Link](/docs/components/link), [Tabs](/docs/components/tabs),
|
||||
[Breadcrumbs](/docs/components/breadcrumbs), [Listbox](/docs/components/listbox), [Dropdown](/docs/components/dropdown) and many others offering
|
||||
the flexibility to be rendered as HTML links, allowing you to seamlessly incorporate client-side routers. See the [Routing](/docs/guide/routing) guide to
|
||||
learn how it set it up in your app.
|
||||
|
||||
The `NextUIProvider` component configures all NextUI components within it to navigate using the
|
||||
client side router you provide.
|
||||
|
||||
> **Note**: Client side routing is based on [React Aria Routing](https://react-spectrum.adobe.com/react-aria/routing.html).
|
||||
|
||||
### Next.js Example
|
||||
|
||||
- App Router
|
||||
|
||||
Go to your `app/providers.tsx` or `app/providers.jsx` (create it if it doesn't exist) and add the
|
||||
`useRouter` hook from `next/navigation`, it returns a router object that can be used to perform navigation.
|
||||
|
||||
```tsx
|
||||
// app/providers.tsx
|
||||
"use client";
|
||||
|
||||
import {NextUIProvider} from "@nextui-org/react";
|
||||
|
||||
export function Providers({children}: {children: React.ReactNode}) {
|
||||
const router = useRouter();
|
||||
|
||||
return <NextUIProvider navigate={router.push}>{children}</NextUIProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
- Pages Router
|
||||
|
||||
Go to pages`/_app.js` or `pages/_app.tsx` (create it if it doesn't exist) and add the`useRouter` hook
|
||||
from `next/router`, it returns a router object that can be used to perform navigation.
|
||||
|
||||
```tsx
|
||||
// pages/_app.tsx
|
||||
import type {AppProps} from "next/app";
|
||||
import {useRouter} from "next/router";
|
||||
import {NextUIProvider} from "@nextui-org/react";
|
||||
|
||||
function MyApp({Component, pageProps}: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<NextUIProvider navigate={router.push}>
|
||||
<Component {...pageProps} />
|
||||
</NextUIProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
```
|
||||
|
||||
- Usage
|
||||
|
||||
Now that you have set up the `NextUIProvider` in your app, you can use the `href` prop in the `Tabs`,
|
||||
`Listbox` and `Dropdown` items to navigate between pages.
|
||||
|
||||
The [Link](/docs/components/link) component will also use the `navigate` function from the
|
||||
`NextUIProvider` to navigate between pages.
|
||||
|
||||
```jsx
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
Listbox,
|
||||
ListboxItem,
|
||||
Dropdown,
|
||||
DropdownTrigger,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Button,
|
||||
Link,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Tabs aria-label="Navigation">
|
||||
<Tab key="home" href="/home">
|
||||
Home
|
||||
</Tab>
|
||||
<Tab key="about" href="/about">
|
||||
About
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<Listbox aria-label="Navigation">
|
||||
<ListboxItem key="home" href="/home">
|
||||
Home
|
||||
</ListboxItem>
|
||||
<ListboxItem key="about" href="/about">
|
||||
About
|
||||
</ListboxItem>
|
||||
</Listbox>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button>Open</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Navigation">
|
||||
<DropdownItem key="home" href="/home">
|
||||
Home
|
||||
</DropdownItem>
|
||||
<DropdownItem key="about" href="/about">
|
||||
About
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Link href="/home">Home</Link>
|
||||
<Link href="/about">About</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
## Autocomplete
|
||||
|
||||
Creating an autocomplete component that is both accessible and customizable is a challenging task. We've spent a lot of time
|
||||
researching and testing different approaches to come up with a solution that works for everyone. The result is a
|
||||
component that is easy to use, fully accessible, and highly customizable.
|
||||
|
||||
The new **Autocomplete** component includes:
|
||||
|
||||
- Support for selecting a single option.
|
||||
- Support for disabled options.
|
||||
- Support for groups of items in sections.
|
||||
- Support for filtering a list of options by typing.
|
||||
- Support for controlled and uncontrolled options, selection and input value.
|
||||
- Support for custom filter functions.
|
||||
- Async loading and infinite scrolling support.
|
||||
- Required and invalid states exposed to assistive technology via ARIA.
|
||||
- Support for description and error message help text linked to the input via ARIA.
|
||||
- And much more...
|
||||
|
||||
We recommend to read this [Blog post](https://react-spectrum.adobe.com/blog/building-a-combobox.html) from [react-aria](https://react-spectrum.adobe.com/react-aria) team to learn more
|
||||
about the Autocomplete component.
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeDemo title="Usage" files={autocompleteContent.usage} />
|
||||
|
||||
### Custom Items
|
||||
|
||||
You can customize the autocomplete items by modifying the `AutocompleteItem` children.
|
||||
|
||||
<CodeDemo title="Custom Items" files={autocompleteContent.customItems} />
|
||||
|
||||
### Customizable
|
||||
|
||||
The autocomplete component is highly customizable, you can customize the popover, listbox and
|
||||
input components.
|
||||
|
||||
<CodeDemo title="Custom Styles" files={autocompleteContent.customStyles} />
|
||||
|
||||
Go to the [Autocomplete](/docs/components/autocomplete) component page to learn more about it.
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
## Slider
|
||||
|
||||
The **Slider** component allows users to make selections from a range of values. It is a great way to allow users to
|
||||
select a value from a fixed range, such as volume or temperature, or to select a value from a range that changes
|
||||
frequently, such as stock prices or budgets.
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeDemo title="Usage" files={sliderContent.usage} />
|
||||
|
||||
### Range Support
|
||||
|
||||
If you pass an array of values to the `value` prop or to the `defaultValue` prop, the slider will become a range slider.
|
||||
|
||||
<CodeDemo title="Range Slider" files={sliderContent.range} />
|
||||
|
||||
### With Tooltip
|
||||
|
||||
The slider component also supports a tooltip to display the current value.
|
||||
|
||||
<CodeDemo title="With Tooltip" files={sliderContent.tooltip} />
|
||||
|
||||
### Slider steps
|
||||
|
||||
You can use the `showSteps` prop to display small dots on each step.
|
||||
|
||||
<CodeDemo title="With Visible Steps" files={sliderContent.visibleSteps} />
|
||||
|
||||
### Slider Custom Styles
|
||||
|
||||
You can customize the `Slider` component by passing custom Tailwind CSS classes to the component slots.
|
||||
|
||||
<CodeDemo title="Custom Styles" files={sliderContent.customStyles} />
|
||||
|
||||
Go to the [Slider](/docs/components/slider) component page to learn more about it.
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
## Breadcrumbs
|
||||
|
||||
The **Breadcrumbs** is an essential component for navigating hierarchical content. It displays a hierarchy of links to the
|
||||
current page or resource in an application.
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeDemo title="Usage" files={breadcrumbsContent.usage} />
|
||||
|
||||
### Breadcrumbs as menu
|
||||
|
||||
It is possible to use the `Breadcrumbs` component as a horizontal menu. This is useful when you want to display a list
|
||||
of possible navigations and let the user choose one of them.
|
||||
|
||||
<CodeDemo title="Menu Type" files={breadcrumbsContent.menuType} />
|
||||
|
||||
### Start & End Content support
|
||||
|
||||
You can add any element to the start or end of the breadcrumbs by using the `startContent` and `endContent` props. The
|
||||
above example uses the `startContent` prop to add icons to the start of the breadcrumbs.
|
||||
|
||||
<CodeDemo title="Start & End Content" files={breadcrumbsContent.startEndContent} />
|
||||
|
||||
### Collapsing Items support
|
||||
|
||||
The `Breadcrumbs` component supports collapsing items, it is useful when you have a lot of items and you want to
|
||||
collapse them into a dropdown menu.
|
||||
|
||||
<CodeDemo title="Customizing the Ellipsis Item" files={breadcrumbsContent.customizingEllipsis} />
|
||||
|
||||
### Applying custom styles
|
||||
|
||||
You can customize the `Breadcrumbs` component by passing custom Tailwind CSS classes to the component slots.
|
||||
|
||||
<CodeDemo title="Custom Styles" files={breadcrumbsContent.customStyles} />
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
Go to the [Breadcrumbs](/docs/components/breadcrumbs) component page to learn more about it.
|
||||
|
||||
## Other Changes
|
||||
|
||||
- Styling Improvements:
|
||||
|
||||
- Transitioned spacing units from pixels (px) to rem units to optimize mobile component sizing.
|
||||
- Introduced a new `shouldSelectOnPressUp` property for `Tabs/Tab` with a default value of true, allowing tab selection on press-up instead of press-down.
|
||||
- Updated Chip component's font size to text-tiny for the sm size variant.
|
||||
- Enhanced the Button component to display only the spinner during loading when it contains an icon.
|
||||
|
||||
- Accessibility and Usability Enhancements:
|
||||
|
||||
- Resolved Popover component's open state issues for blur/opaque backdrops.
|
||||
- Enhanced the ScrollShadow API, introducing visibility and onVisibilityChange properties to manage shadow visibility.
|
||||
- Added `emptyContent`, `topContent`, and `bottomContent` properties to Listbox/Menu/Dropdown for customized content rendering.
|
||||
- Introduced baseRef to the Input component, allowing control over the parent element's reference.
|
||||
- Upgraded tailwind-variants to version `0.1.18`, incorporating slot props control.
|
||||
|
||||
- Right-to-Left (RTL) Support:
|
||||
|
||||
- Implemented RTL support for Accordion,
|
||||
- Implemented RTL support for Accordion Avatar & AvatarGroup components,
|
||||
- Implemented RTL support for ButtonGroup.
|
||||
|
||||
- Custom Implementations and Fixes:
|
||||
|
||||
- Implemented a custom `usePress` hook to address the corner press issue, with a corresponding pull request and issue submitted to the react-aria project.
|
||||
- Applied the custom `usePress` across all NextUI interactive components for consistent behavior.
|
||||
- Improved animations and positioning for Input & Select labels.
|
||||
- Upgraded `react-aria` packages and `framer-motion` for enhanced performance.
|
||||
- Enhanced TypeScript support for generic items in Menu and DropdownMenu.
|
||||
- Streamlined package dependencies to exclude globally or individually installed packages, reducing bundle sizes.
|
||||
|
||||
- Visual and Interactive Tweaks:
|
||||
|
||||
- Removed the outline on Input focus-visible for a cleaner aesthetic.
|
||||
- Fixed the radius property issue in `ButtonGroup`.
|
||||
- Introduced the `isActive` prop for `NavbarMenuItem` for active state management.
|
||||
- Corrected the Pagination custom-items example by adding the key prop.
|
||||
- Enabled Collection Items support within the `extendVariants` function.
|
||||
- Added transitions to menu/listbox items for smoother interaction.
|
||||
- Added a `disableAutosize` property to `Textarea` to control auto-resizing.
|
||||
- Resolved styling issues in Textarea and animated height adjustments for a fluid user experience.
|
||||
- Included a `hoverOpacity` key in the themes object within the plugin for customized hover effects.
|
||||
- Implemented a hover effect for the Button component to enhance user interaction feedback.
|
||||
- The padding of the CardBody has been updated from `p-5` to `p-3` for consistency with the padding of other Card elements.
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
Unfortunately, we had to make some small styles breaking changes in this release to improve the [Popover](/docs/components/popover) arrow
|
||||
dynamic positioning. Instead of having a specific slot for the popover arrow, we now use the `before` pseudo element, this allows the
|
||||
popover to also move the arrow altogether.
|
||||
|
||||
This change impacts the Popover, Dropdown, Tooltip and Select implementations.
|
||||
|
||||
Popover changes:
|
||||
|
||||
```diff-jsx
|
||||
<Popover
|
||||
showArrow
|
||||
backdrop="opaque"
|
||||
placement="right"
|
||||
classNames={{
|
||||
- base: "py-3 px-4 border border-default-200 bg-gradient-to-br from-white to-default-300 dark:from-default-100 dark:to-default-50",
|
||||
- arrow: "bg-default-200",
|
||||
+ base: [
|
||||
+ // the "before" pseudo element is now the popover' arrow
|
||||
+ "before:bg-default-200"
|
||||
+ ],
|
||||
+ content: [ // now we need to use the "content" slot to actually modify the popover' content styles
|
||||
+ "py-3 px-4 border border-default-200",
|
||||
+ "bg-gradient-to-br from-white to-default-300",
|
||||
+ "dark:from-default-100 dark:to-default-50",
|
||||
],
|
||||
}}
|
||||
>
|
||||
...
|
||||
</Popover>
|
||||
```
|
||||
|
||||
Tooltip changes:
|
||||
|
||||
```diff-jsx
|
||||
<Tooltip
|
||||
showArrow
|
||||
placement="right"
|
||||
content="I am a tooltip"
|
||||
classNames={{
|
||||
- base: "py-2 px-4 shadow-xl text-black bg-gradient-to-br from-white to-neutral-400",
|
||||
- arrow: "bg-neutral-400 dark:bg-white",
|
||||
+ base: [
|
||||
+ // the "before" pseudo element is now the popover' arrow
|
||||
+ "before:bg-neutral-400 dark:before:bg-white",
|
||||
+ ],
|
||||
+ content: [ // now we need to use the "content" slot to actually modify the popover' content styles
|
||||
+ "py-2 px-4 shadow-xl",
|
||||
+ "text-black bg-gradient-to-br from-white to-neutral-400",
|
||||
+ ],
|
||||
}}
|
||||
>
|
||||
<Button variant="flat">Hover me</Button>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
Select changes:
|
||||
|
||||
```diff-jsx
|
||||
<Select
|
||||
items={users}
|
||||
label="Assigned to"
|
||||
className="max-w-xs"
|
||||
variant="bordered"
|
||||
popoverProps={{
|
||||
classNames: {
|
||||
- base: "p-0 border-small border-divider bg-background",
|
||||
- arrow: "bg-default-200",
|
||||
+ base: "before:bg-default-200", // the before pseudo element controls the popover's arrow
|
||||
+ content: "p-0 border-small border-divider bg-background", // now instead of the "base" slot we use the "content" slot
|
||||
},
|
||||
}}
|
||||
>
|
||||
...
|
||||
</Select>
|
||||
);
|
||||
}`;
|
||||
```
|
||||
|
||||
Dropdown Changes
|
||||
|
||||
```diff-jsx
|
||||
<Dropdown
|
||||
showArrow
|
||||
classNames={{
|
||||
- base: "py-1 px-1 border border-default-200 bg-gradient-to-br from-white to-default-200 dark:from-default-50 dark:to-black",
|
||||
- arrow: "bg-default-200",
|
||||
+ content: "py-1 px-1 border border-default-200 bg-gradient-to-br from-white to-default-200 dark:from-default-50 dark:to-black", // now instead of the "base" slot we use the "content"
|
||||
+ base: "before:bg-default-200", // the before pseudo element controls the popover's arrow
|
||||
}}
|
||||
>
|
||||
...
|
||||
</Dropdown>
|
||||
```
|
||||
|
||||
<Spacer y={6} />
|
||||
|
||||
We hope you enjoy these new components and the new features. We're excited to see what you build with them!
|
||||
|
||||
Thanks for reading and happy coding! 🚀
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
We're excited to see the community adopt NextUI, raise issues, and provide feedback.
|
||||
Whether it's a feature request, bug report, or a project to showcase, please get involved!
|
||||
|
||||
<Community />
|
||||
|
||||
## Contributing
|
||||
|
||||
PR's on NextUI are always welcome, please see our [contribution guidelines](https://github.com/nextui-org/nextui/blob/main/CONTRIBUTING.md) to learn how you can contribute to this project.
|
||||
@@ -0,0 +1,406 @@
|
||||
---
|
||||
title: "Introducing v2.3.0 🎉"
|
||||
description: "NextUI v2.3.0 is here! includes six new components, NextUI CLI, bug fixes, React Aria, and a TailwindCSS upgrade, among other enhancements."
|
||||
date: "2024-04-16"
|
||||
image: "/blog/v2.3.0.jpg"
|
||||
tags: ["nextui", "cli", "date picker", "time input", "date input", "calendar"]
|
||||
author:
|
||||
name: "Junior Garcia"
|
||||
username: "@jrgarciadev"
|
||||
link: "https://twitter.com/jrgarciadev"
|
||||
avatar: "/avatars/junior-garcia.jpeg"
|
||||
---
|
||||
|
||||
import {dateInputContent} from "@/content/components/date-input";
|
||||
import {timeInputContent} from "@/content/components/time-input";
|
||||
import {calendarContent} from "@/content/components/calendar";
|
||||
import {rangeCalendarContent} from "@/content/components/range-calendar";
|
||||
import {datePickerContent} from "@/content/components/date-picker";
|
||||
import {dateRangePickerContent} from "@/content/components/date-range-picker";
|
||||
|
||||
<img
|
||||
src="/blog/v2.3.0_2x.jpg"
|
||||
width={700}
|
||||
height={350}
|
||||
alt="NextUI v2.3.0"
|
||||
className="w-full border border-transparent dark:border-default-200/50 object-fit rounded-xl shadow-lg"
|
||||
/>
|
||||
|
||||
We are excited to announce the latest update to NextUI, version **2.3.0**! This release introduces 6 new components,
|
||||
our new CLI, and several enhancements and bug fixes.
|
||||
|
||||
## What's New in v2.3.0?
|
||||
|
||||
- [NextUI CLI](/docs/guide/cli) - A command-line interface for creating and managing NextUI projects.
|
||||
- [DateInput](/docs/components/date-input) - Allows users to enter and edit date and time values using a keyboard.
|
||||
- [TimeInput](/docs/components/time-input) - Allows users to enter and edit time values using a keyboard.
|
||||
- [Calendar](/docs/components/calendar) - Displays a calendar for selecting dates and times.
|
||||
- [RangeCalendar](/docs/components/range-calendar) - Displays a calendar for selecting date ranges.
|
||||
- [DatePicker](/docs/components/date-picker) - Allows users to select a date from a calendar.
|
||||
- [DateRangePicker](/docs/components/date-range-picker) - Allows users to select a date range from a calendar.
|
||||
- [Other Changes](#other-changes) - Includes styling improvements, accessibility and usability enhancements.
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
Requirements:
|
||||
|
||||
- [Tailwind CSS 3.4](https://tailwindcss.com/) or later
|
||||
|
||||
Upgrade today by running one of the following commands:
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
<PackageManagers
|
||||
commands={{
|
||||
cli: "nextui upgrade",
|
||||
npm: "npx nextui-cli@latest upgrade",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
## NextUI CLI
|
||||
|
||||
We are thrilled to introduce the [NextUI CLI](https://github.com/nextui-org/nextui-cli), a command-line interface, It offers a comprehensive suite
|
||||
of commands to initialize, manage, and improve your NextUI projects. It enables you to `add`, `remove`, or
|
||||
`upgrade` individual components, assess the health of your project, and more.
|
||||
|
||||
### Installation
|
||||
|
||||
To install the CLI globally, execute one of the following commands in your terminal:
|
||||
|
||||
<PackageManagers
|
||||
commands={{
|
||||
npm: "npm install nextui-cli -g",
|
||||
yarn: "yarn add nextui-cli -g",
|
||||
pnpm: "pnpm add nextui-cli -g",
|
||||
bun: "bun add nextui-cli -g",
|
||||
}}
|
||||
/>
|
||||
|
||||
Alternatively, you can use the CLI without a global installation by employing `npx`:
|
||||
|
||||
```bash
|
||||
npx nextui-cli@latest
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Once the CLI is installed, run the following command to display available commands:
|
||||
|
||||
```bash
|
||||
nextui
|
||||
```
|
||||
|
||||
NextUI CLI can help you create new projects, add components, upgrade components, remove components, detect issues in
|
||||
you setup, know your environment, and more.
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
To initialize a new project, you can simply run:
|
||||
|
||||
```bash
|
||||
nextui init my-nextui-app
|
||||
```
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
You will be prompted to configure your project:
|
||||
|
||||
```codeBlock bash
|
||||
? Select a template › - Use arrow-keys. Return to submit.
|
||||
❯ App
|
||||
A Next.js 13 with app directory template pre-configured with NextUI (v2) and Tailwind CSS.
|
||||
Pages
|
||||
A Next.js 13 with pages directory template pre-configured with NextUI (v2) and Tailwind CSS.
|
||||
```
|
||||
|
||||
Select the template you want to use and the CLI will create a new project for you.
|
||||
|
||||
> We're working on adding more templates to the CLI, so stay tuned for updates!
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
If you already have a NextUI project, you can add components to it using the `add` command:
|
||||
|
||||
```bash
|
||||
nextui add date-input
|
||||
```
|
||||
|
||||
It will automatically detect the required dependencies, modify your `tailwind.config.(js|ts)` file,
|
||||
detect whether using `pnpm` if so, add the required configuration to your `.npmrc` file and add the component to your project.
|
||||
|
||||
If instead of installing a single component you want to install multiple components, you can do so by separating them with a space:
|
||||
|
||||
```bash
|
||||
nextui add date-input time-input calendar
|
||||
```
|
||||
|
||||
You can alternatively install the `main` package which includes all the components by passing the `--all` flag:
|
||||
|
||||
```bash
|
||||
nextui add --all
|
||||
```
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
> The CLI is currentl in `Alpha` stage, we're working on adding more features and improvements. If you find any issues or have any suggestions, please let us know by [opening an issue](https://github.com/nextui-org/nextui-cli/issues/new).
|
||||
|
||||
To learn more about the CLI and its commands, please refer to the [CLI documentation](/docs/guide/cli) and the [CLI API reference](/docs/api-references/cli-api).
|
||||
|
||||
## New Components
|
||||
|
||||
Since the beginning of NextUI, devs have been asking for date and time input components. After
|
||||
months of iteration and development, we are excited to introduce the following new components:
|
||||
|
||||
### DateInput
|
||||
|
||||
DateInput is a component that allows users to enter and edit date and time values using a keyboard.
|
||||
Each part of a date value is displayed in an individually editable segment.
|
||||
|
||||
<CodeDemo title="Usage" files={dateInputContent.usage} />
|
||||
|
||||
Go to the [DateInput documentation](/docs/components/date-input) to learn more about the component.
|
||||
|
||||
### TimeInput
|
||||
|
||||
The `TimeInput` component consists of a label, and a group of segments representing each unit of a time (e.g. hours, minutes, and seconds). Each segment is individually focusable and editable by the user, by typing or using the arrow keys to increment and decrement the value. This approach allows values to be formatted and parsed correctly regardless of the locale or time format, and offers an easy and error-free way to edit times using the keyboard.
|
||||
|
||||
<CodeDemo title="Usage" files={timeInputContent.usage} />
|
||||
|
||||
Go to the [TimeInput documentation](/docs/components/time-input) to learn more about the component.
|
||||
|
||||
### Calendar
|
||||
|
||||
A Calendar consists of a grouping element containing one or more date grids (e.g. months), and a previous and next button for navigating between date ranges. Each calendar grid consists of cells containing button elements that can be pressed and navigated to using the arrow keys to select a date.
|
||||
|
||||
<CodeDemo title="Usage" files={calendarContent.usage} />
|
||||
|
||||
The calendar also supports selecting years and months for rapid selection.
|
||||
|
||||
<CodeDemo title="With Month And Year Picker" files={calendarContent.withMonthAndYearPicker} />
|
||||
|
||||
Go to the [Calendar documentation](/docs/components/calendar) to learn more about the component.
|
||||
|
||||
### RangeCalendar
|
||||
|
||||
A Range calendar consists of a grouping element containing one or more date grids (e.g. months), and a previous and next button for navigating through time. Each calendar grid consists of cells containing button elements that can be pressed and navigated to using the arrow keys to select a date range. Once a start date is selected, the user can navigate to another date using the keyboard or by hovering over it, and clicking it or pressing the Enter key commits the selected date range.
|
||||
|
||||
<CodeDemo title="Usage" files={rangeCalendarContent.usage} />
|
||||
|
||||
Go to the [RangeCalendar documentation](/docs/components/range-calendar) to learn more about the component.
|
||||
|
||||
### DatePicker
|
||||
|
||||
A Date Picker combines a DateInput and a Calendar popover to allow users to enter or select a date and time value.
|
||||
|
||||
<CodeDemo title="Usage" files={datePickerContent.usage} />
|
||||
|
||||
Go to the [DatePicker documentation](/docs/components/date-picker) to learn more about the component.
|
||||
|
||||
### DateRangePicker
|
||||
|
||||
Date Range Picker combines two DateInputs and a RangeCalendar popover to allow users to enter or select a date and time range.
|
||||
|
||||
<CodeDemo title="Usage" files={dateRangePickerContent.usage} />
|
||||
|
||||
Go to the [DateRangePicker documentation](/docs/components/date-range-picker) to learn more about the component.
|
||||
|
||||
|
||||
### Calendar Presets
|
||||
|
||||
`Calendar` and `RangeCalendar` components support adding custom content at the top and bottom of the calendar, this is useful for adding presets or
|
||||
custom actions to the calendar.
|
||||
|
||||
Here's an example of how to add presets to the `Calendar` component:
|
||||
|
||||
<CodeDemo title="Calendar Presets" files={calendarContent.presets} />
|
||||
|
||||
### Internationalization
|
||||
|
||||
These new components have built-in internationalization, time zones and granularity support, they supports selecting dates
|
||||
in many calendar systems used around the world, including `Gregorian`, `Hebrew`, `Indian`, `Islamic`, `Buddhist`, and more.
|
||||
|
||||
Dates are automatically displayed in the appropriate calendar system for the user's locale this is possible thanks to [@internationalized/date](https://react-spectrum.adobe.com/internationalized/date/) package, which
|
||||
includes functions for parsing strings in multiple formats into `ZonedDateTime` objects.
|
||||
|
||||
Here's and example using the `Indian` calendar system:
|
||||
|
||||
<CodeDemo title="International Calendar" files={datePickerContent.internationalCalendar} />
|
||||
|
||||
Alternatively you can set the `locale` globally by using the `NextUIProvider` component:
|
||||
|
||||
```jsx
|
||||
// Next.js App Router example
|
||||
"use client";
|
||||
|
||||
import {NextUIProvider} from "@nextui-org/react";
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Providers({children}: ProvidersProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return <NextUIProvider locale="hi-IN-u-ca-indian">{children}</NextUIProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
If no `locale` is provided, it will extract the locale from the browser.
|
||||
|
||||
### NextUI Provider
|
||||
|
||||
The `NextUIProvider` component was updated to include the `createCalendar` function, which allows you to create a calendar instance with the specified locale and time zone,
|
||||
and the `defaultDates` object which allows you to set global minimum and maximum dates for the components.
|
||||
|
||||
```jsx
|
||||
// Next.js App Router example
|
||||
"use client";
|
||||
|
||||
import {NextUIProvider, SupportedCalendars} from "@nextui-org/react";
|
||||
import {CalendarDate, GregorianCalendar} from "@internationalized/date";
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function createCalendar(identifier: SupportedCalendars) {
|
||||
switch (identifier) {
|
||||
case "gregory":
|
||||
return new GregorianCalendar();
|
||||
default:
|
||||
throw new Error(`Unsupported calendar ${identifier}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function Providers({children}: ProvidersProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<NextUIProvider
|
||||
locale="hi-IN-u-ca-indian"
|
||||
defaultDates={{
|
||||
minDate: new CalendarDate(1900, 1, 1),
|
||||
maxDate: new CalendarDate(2099, 12, 31),
|
||||
}}
|
||||
createCalendar={createCalendar}
|
||||
>
|
||||
{children}
|
||||
</NextUIProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
In order to improve the performance and reduce the bundle size, we have removed the `units` creation from the
|
||||
`nextui` plugin. [TailwindCSS v3.4](https://tailwindcss.com/blog/tailwindcss-v3-4) added support for `min-h-*` and `min-w-*` classes, so it is no longer needed.
|
||||
|
||||
How to upgrade:
|
||||
|
||||
1. Upgrade TailwindCSS to version 3.4 or later (if you haven't already). You can do this by running:
|
||||
|
||||
```bash
|
||||
npm install tailwindcss@latest
|
||||
```
|
||||
|
||||
2. Remove the `spacingUnit` configuration from your `tailwind.config.(js|ts)` file (if you have it):
|
||||
|
||||
```diff-js
|
||||
plugins: [
|
||||
nextui({
|
||||
layout: {
|
||||
- spacingUnit: 4,
|
||||
},
|
||||
}),
|
||||
],
|
||||
```
|
||||
|
||||
3. Find all `-unit` classes in your project and replace them with a `-` separator. For example, replace `p-unit-4` with `p-4`.
|
||||
|
||||
```diff-jsx
|
||||
import {Button} from "@nextui-org/react";
|
||||
|
||||
export const MyButton = () => {
|
||||
return (
|
||||
- <Button className="px-unit-2 py-unit-1 min-w-unit-48">
|
||||
+ <Button className="px-2 py-1 min-w-48">
|
||||
My Button
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
That's it! Your project should now be using the latest version of TailwindCSS and NextUI.
|
||||
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
|
||||
## Other Changes
|
||||
|
||||
**Bug Fixes**:
|
||||
|
||||
- Fixed an HSL color rounding issue in the theme settings to ensure accurate color representation. [PR](https://github.com/nextui-org/nextui/pull/2702) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Removed conflicting transition definitions affecting CSS classes. [PR](https://github.com/nextui-org/nextui/pull/2677) - [@u3u](https://github.com/u3u)
|
||||
- Patched the "@nextui-org/autocomplete" package to fix an issue where empty items with `allowCustomValue` would not render properly due to a null node problem. [PR](https://github.com/nextui-org/nextui/pull/2674) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Implemented a fix in modal components to prevent carryover of IME (Input Method Editor) input when switching fields using the Tab key. [PR](https://github.com/nextui-org/nextui/pull/2709) - [@ryo-manba](https://github.com/ryo-manba)
|
||||
- Enhanced accessibility by handling Tab key press event in the `ModalContent` component. [PR](https://github.com/nextui-org/nextui/pull/2709) - [@ryo-manba](https://github.com/ryo-manba)
|
||||
- Fixed an issue where disabled select components could still be changed using blur and keyboard shortcuts. [PR](https://github.com/nextui-org/nextui/pull/2649) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Patched issues in "@nextui-org/use-aria-multiselect" and "@nextui-org/stories-utils" packages to fix a warning about `SELECT defaultSelectedKeys`. [PR](https://github.com/nextui-org/nextui/pull/2648) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Fixed an issue with incorrect `onChange` typing in Checkbox Group, ensuring it now correctly handles an array of strings as values. [PR](https://github.com/nextui-org/nextui/pull/2595) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Fixed the `label` placement issue in `Select` component if a `description` prop is used [PR](https://github.com/nextui-org/nextui/pull/2553) - [@novsource](https://github.com/novsource)
|
||||
- Fixed the `originalProps` spread issue in the `Dropdown` component. [PR](https://github.com/nextui-org/nextui/pull/2450) - [@wingkwong](https://github.com/wingkwong)
|
||||
|
||||
**Improvements**
|
||||
- Framer Motion was updated to the latest version, improving performance and reducing bundle size. [Docs](https://www.framer.com/motion/guide-reduce-bundle-size/) [PR](https://github.com/nextui-org/nextui/pull/2464) - [@mezotv](https://github.com/mezotv)
|
||||
- `LazyMotion` was added to all components that use Framer Motion, improving performance by only loading the required motion components.
|
||||
- We removed the custom `units` creation from the `nextui` plugin, it is no longer needed with TailwindCSS v3.4 and above. [PR](https://github.com/nextui-org/nextui/pull/2713) - [@jrgarciadev](https://github.com/jrgarciadev)
|
||||
- Updated `framer-motion` package across various components and utilities to version `11.0.22` for enhanced performance and consistency. [PR](https://github.com/nextui-org/nextui/pull/2596) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Ensured compatibility with `react@18.2.0` and `react-dom@18.2.0` across the board. [PR](https://github.com/nextui-org/nextui/pull/2596) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Introduced patches for NextUI components to improve animations, including support for keyframes with spring and inertia animations. [PR](https://github.com/nextui-org/nextui/pull/2596) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Improved handling of numeric keys in the multi-select component to ensure consistent behavior. [PR](https://github.com/nextui-org/nextui/pull/2589) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Updated the version of react-aria to include the latest changes as detailed in the [2024-02-13 release](https://react-spectrum.adobe.com/releases/2024-02-13.html). [PR](https://github.com/nextui-org/nextui/pull/2561) - [@ryo-manba](https://github.com/ryo-manba)
|
||||
- Added support for custom class names in the `AvatarGroup` component, enhancing flexibility in styling. [PR](https://github.com/nextui-org/nextui/pull/2669) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Introduced a `count` slot to the `AvatarGroup` for more customized rendering. [PR](https://github.com/nextui-org/nextui/pull/2669) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Improved the `AvatarGroup` component's count rendering logic for better performance and flexibility. [PR](https://github.com/nextui-org/nextui/pull/2669) - [@wingkwong](https://github.com/wingkwong)
|
||||
- Add RTL support to the kbd component. [PR](https://github.com/nextui-org/nextui/pull/2482) - [@mrbadri](https://github.com/mrbadri)
|
||||
- Add RTL support to the Select component. [PR](https://github.com/nextui-org/nextui/pull/2485) - [@mrbadri](https://github.com/mrbadri)
|
||||
- Add RTL support to the avatar group componen. [PR](https://github.com/nextui-org/nextui/pull/2498) - [@mrbadri](https://github.com/mrbadri)
|
||||
- Add RTL support to the Table component. [PR](https://github.com/nextui-org/nextui/pull/2472) - [@mrbadri](https://github.com/mrbadri)
|
||||
|
||||
**Documentation**:
|
||||
- Updated documentation to reflect the new features and changes in the `AvatarGroup` component.
|
||||
- Added support for the "bun" package manager across documentation and components. [PR](https://github.com/nextui-org/nextui/pull/2625) - [@sudongyuer](https://github.com/sudongyuer)
|
||||
- [Kapa.ai](https://www.kapa.ai/) widget was added to the documentation to provide AI support for users. [PR](https://github.com/nextui-org/nextui/pull/2428) - [@wingkwong](https://github.com/sudongyuer)
|
||||
- Layout docs updated to remove the `units` configuration from the `tailwind.config.(js|ts)` file.
|
||||
|
||||
|
||||
Special thanks to NextUI Team members [@kuri-sun](https://github.com/kuri-sun), [@ryo-manba](https://github.com/ryo-manba),
|
||||
[@sudongyuer](https://github.com/sudongyuer), [@winchesHe](https://github.com/winchesHe), [@wingkwong](https://github.com/wingkwong),
|
||||
[@tianenpang](https://github.com/tianenpang), [@smultar](https://github.com/smultar) and contributors for their contributions to this release.
|
||||
|
||||
For a full list of changes, please refer to the [release notes](https://github.com/nextui-org/nextui/releases/tag/%40nextui-org%2Freact%402.3.0).
|
||||
|
||||
<Spacer y={6} />
|
||||
|
||||
We hope you enjoy these new components and the new features. We're excited to see what you build with them!
|
||||
|
||||
Thanks for reading and happy coding! 🚀
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
We're excited to see the community adopt NextUI, raise issues, and provide feedback.
|
||||
Whether it's a feature request, bug report, or a project to showcase, please get involved!
|
||||
|
||||
<Community />
|
||||
|
||||
## Contributing
|
||||
|
||||
PR's on NextUI are always welcome, please see our [contribution guidelines](https://github.com/nextui-org/nextui/blob/main/CONTRIBUTING.md) to learn how you can contribute to this project.
|
||||
@@ -24,10 +24,41 @@ export default function App() {
|
||||
);
|
||||
}`;
|
||||
|
||||
const AppTs = `import type {Selection} from "@nextui-org/react";
|
||||
|
||||
import {Accordion, AccordionItem} from "@nextui-org/react";
|
||||
|
||||
export default function App() {
|
||||
const [selectedKeys, setSelectedKeys] = React.useState<Selection>(new Set(["1"]));
|
||||
const defaultContent =
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.";
|
||||
return (
|
||||
<Accordion
|
||||
selectedKeys={selectedKeys}
|
||||
onSelectionChange={setSelectedKeys}
|
||||
>
|
||||
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
};
|
||||
|
||||
const reactTs = {
|
||||
"/App.tsx": AppTs,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
...reactTs,
|
||||
};
|
||||
|
||||
@@ -214,7 +214,7 @@ export default function App() {
|
||||
startContent={<MonitorMobileIcon className="text-primary" />}
|
||||
subtitle={
|
||||
<p className="flex">
|
||||
2 issues to <p className="text-primary ml-1">fix now</p>
|
||||
2 issues to <span className="text-primary ml-1">fix now</span>
|
||||
</p>
|
||||
}
|
||||
title="Connected devices"
|
||||
@@ -249,7 +249,7 @@ export default function App() {
|
||||
title={
|
||||
<p className="flex gap-1 items-center">
|
||||
Card expired
|
||||
<p className="text-default-400 text-small">*4812</p>
|
||||
<span className="text-default-400 text-small">*4812</span>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function App() {
|
||||
}
|
||||
subtitle={
|
||||
<p className="flex">
|
||||
2 issues to<p className="text-primary ml-1">fix now</p>
|
||||
2 issues to<span className="text-primary ml-1">fix now</span>
|
||||
</p>
|
||||
}
|
||||
title="Zoey Lang"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
|
||||
export default function App() {
|
||||
let list = useAsyncList({
|
||||
async load({signal, filterText}) {
|
||||
let res = await fetch(\`https://swapi.py4e.com/api/people/?search=\${filterText}\`, {signal});
|
||||
let json = await res.json();
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
inputValue={list.filterText}
|
||||
isLoading={list.isLoading}
|
||||
items={list.items}
|
||||
label="Select a character"
|
||||
placeholder="Type to search..."
|
||||
variant="bordered"
|
||||
onInputChange={list.setFilterText}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.name} className="capitalize">
|
||||
{item.name}
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const AppTs = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, filterText}) {
|
||||
let res = await fetch(\`https://swapi.py4e.com/api/people/?search=\${filterText}\`, {signal});
|
||||
let json = await res.json();
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
inputValue={list.filterText}
|
||||
isLoading={list.isLoading}
|
||||
items={list.items}
|
||||
label="Select a character"
|
||||
placeholder="Type to search..."
|
||||
variant="bordered"
|
||||
onInputChange={list.setFilterText}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.name} className="capitalize">
|
||||
{item.name}
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
};
|
||||
|
||||
const reactTs = {
|
||||
"/App.tsx": AppTs,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
...reactTs,
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
const usePokemonListTs = `export type Pokemon = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type UsePokemonListProps = {
|
||||
/** Delay to wait before fetching more items */
|
||||
fetchDelay?: number;
|
||||
};
|
||||
|
||||
export function usePokemonList({fetchDelay = 0}: UsePokemonListProps = {}) {
|
||||
const [items, setItems] = React.useState<Pokemon[]>([]);
|
||||
const [hasMore, setHasMore] = React.useState(true);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const limit = 10; // Number of items per page, adjust as necessary
|
||||
|
||||
const loadPokemon = async (currentOffset: number) => {
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (offset > 0) {
|
||||
// Delay to simulate network latency
|
||||
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
|
||||
}
|
||||
|
||||
let res = await fetch(
|
||||
\`https://pokeapi.co/api/v2/pokemon?offset=\${currentOffset}&limit=\${limit}\`,
|
||||
{signal},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
let json = await res.json();
|
||||
|
||||
setHasMore(json.next !== null);
|
||||
// Append new results to existing ones
|
||||
setItems((prevItems) => [...prevItems, ...json.results]);
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Fetch aborted");
|
||||
} else {
|
||||
console.error("There was an error with the fetch operation:", error);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
loadPokemon(offset);
|
||||
}, []);
|
||||
|
||||
const onLoadMore = () => {
|
||||
const newOffset = offset + limit;
|
||||
|
||||
setOffset(newOffset);
|
||||
loadPokemon(newOffset);
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
hasMore,
|
||||
isLoading,
|
||||
onLoadMore,
|
||||
};
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
const usePokemonList = `export function usePokemonList({fetchDelay = 0} = {}) {
|
||||
const [items, setItems] = React.useState([]);
|
||||
const [hasMore, setHasMore] = React.useState(true);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const limit = 10; // Number of items per page, adjust as necessary
|
||||
|
||||
const loadPokemon = async (currentOffset) => {
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (offset > 0) {
|
||||
// Delay to simulate network latency
|
||||
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
|
||||
}
|
||||
|
||||
let res = await fetch(
|
||||
\`https://pokeapi.co/api/v2/pokemon?offset=\${currentOffset}&limit=\${limit}\`,
|
||||
{signal},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
let json = await res.json();
|
||||
|
||||
setHasMore(json.next !== null);
|
||||
// Append new results to existing ones
|
||||
setItems((prevItems) => [...prevItems, ...json.results]);
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Fetch aborted");
|
||||
} else {
|
||||
console.error("There was an error with the fetch operation:", error);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
loadPokemon(offset);
|
||||
}, []);
|
||||
|
||||
const onLoadMore = () => {
|
||||
const newOffset = offset + limit;
|
||||
|
||||
setOffset(newOffset);
|
||||
loadPokemon(newOffset);
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
hasMore,
|
||||
isLoading,
|
||||
onLoadMore,
|
||||
};
|
||||
};`;
|
||||
|
||||
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";
|
||||
import {usePokemonList} from "./usePokemonList";
|
||||
|
||||
export default function App() {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500});
|
||||
|
||||
const [, scrollerRef] = useInfiniteScroll({
|
||||
hasMore,
|
||||
isEnabled: isOpen,
|
||||
shouldUseLoader: false, // We don't want to show the loader at the bottom of the list
|
||||
onLoadMore,
|
||||
});
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
variant="bordered"
|
||||
isLoading={isLoading}
|
||||
defaultItems={items}
|
||||
label="Pick a Pokemon"
|
||||
placeholder="Select a Pokemon"
|
||||
scrollRef={scrollerRef}
|
||||
selectionMode="single"
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.name} className="capitalize">
|
||||
{item.name}
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
"/usePokemonList.js": usePokemonList,
|
||||
};
|
||||
|
||||
const reactTs = {
|
||||
"/App.tsx": App,
|
||||
"/usePokemonList.ts": usePokemonListTs,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
...reactTs,
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
const data = `export const animals = [
|
||||
{label: "Cat", value: "cat", description: "The second most popular pet in the world"},
|
||||
{label: "Dog", value: "dog", description: "The most popular pet in the world"},
|
||||
{label: "Elephant", value: "elephant", description: "The largest land animal"},
|
||||
{label: "Lion", value: "lion", description: "The king of the jungle"},
|
||||
{label: "Tiger", value: "tiger", description: "The largest cat species"},
|
||||
{label: "Giraffe", value: "giraffe", description: "The tallest land animal"},
|
||||
{
|
||||
label: "Dolphin",
|
||||
value: "dolphin",
|
||||
description: "A widely distributed and diverse group of aquatic mammals",
|
||||
},
|
||||
{label: "Penguin", value: "penguin", description: "A group of aquatic flightless birds"},
|
||||
{label: "Zebra", value: "zebra", description: "A several species of African equids"},
|
||||
{
|
||||
label: "Shark",
|
||||
value: "shark",
|
||||
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
|
||||
},
|
||||
{
|
||||
label: "Whale",
|
||||
value: "whale",
|
||||
description: "Diverse group of fully aquatic placental marine mammals",
|
||||
},
|
||||
{label: "Otter", value: "otter", description: "A carnivorous mammal in the subfamily Lutrinae"},
|
||||
{label: "Crocodile", value: "crocodile", description: "A large semiaquatic reptile"},
|
||||
];`;
|
||||
|
||||
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {animals} from "./data";
|
||||
|
||||
export default function App() {
|
||||
const colors = [
|
||||
"default",
|
||||
"primary",
|
||||
"secondary",
|
||||
"success",
|
||||
"warning",
|
||||
"danger",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row flex-wrap gap-4">
|
||||
{colors.map((color) => (
|
||||
<Autocomplete
|
||||
key={color}
|
||||
color={color}
|
||||
defaultItems={animals}
|
||||
label="Favorite Animal"
|
||||
placeholder="Search an animal"
|
||||
defaultSelectedKey={"cat"}
|
||||
className="max-w-xs"
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
"/data.js": data,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
const data = `export const animals = [
|
||||
{label: "Cat", value: "cat", description: "The second most popular pet in the world"},
|
||||
{label: "Dog", value: "dog", description: "The most popular pet in the world"},
|
||||
{label: "Elephant", value: "elephant", description: "The largest land animal"},
|
||||
{label: "Lion", value: "lion", description: "The king of the jungle"},
|
||||
{label: "Tiger", value: "tiger", description: "The largest cat species"},
|
||||
{label: "Giraffe", value: "giraffe", description: "The tallest land animal"},
|
||||
{
|
||||
label: "Dolphin",
|
||||
value: "dolphin",
|
||||
description: "A widely distributed and diverse group of aquatic mammals",
|
||||
},
|
||||
{label: "Penguin", value: "penguin", description: "A group of aquatic flightless birds"},
|
||||
{label: "Zebra", value: "zebra", description: "A several species of African equids"},
|
||||
{
|
||||
label: "Shark",
|
||||
value: "shark",
|
||||
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
|
||||
},
|
||||
{
|
||||
label: "Whale",
|
||||
value: "whale",
|
||||
description: "Diverse group of fully aquatic placental marine mammals",
|
||||
},
|
||||
{label: "Otter", value: "otter", description: "A carnivorous mammal in the subfamily Lutrinae"},
|
||||
{label: "Crocodile", value: "crocodile", description: "A large semiaquatic reptile"},
|
||||
];`;
|
||||
|
||||
const AppTs = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {animals} from "./data";
|
||||
|
||||
export default function App() {
|
||||
const [value, setValue] = React.useState<React.Key>("cat");
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-xs flex-col gap-2">
|
||||
<Autocomplete
|
||||
label="Favorite Animal"
|
||||
variant="bordered"
|
||||
defaultItems={animals}
|
||||
placeholder="Search an animal"
|
||||
className="max-w-xs"
|
||||
selectedKey={value}
|
||||
onSelectionChange={setValue}
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
<p className="text-default-500 text-small">Selected: {value}</p>
|
||||
</div>
|
||||
);
|
||||
}`;
|
||||
|
||||
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {animals} from "./data";
|
||||
|
||||
export default function App() {
|
||||
const [value, setValue] = React.useState("cat");
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-xs flex-col gap-2">
|
||||
<Autocomplete
|
||||
label="Favorite Animal"
|
||||
variant="bordered"
|
||||
defaultItems={animals}
|
||||
placeholder="Search an animal"
|
||||
className="max-w-xs"
|
||||
selectedKey={value}
|
||||
onSelectionChange={setValue}
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
<p className="text-default-500 text-small">Selected: {value}</p>
|
||||
</div>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
"/data.js": data,
|
||||
};
|
||||
|
||||
const reactTs = {
|
||||
"/App.tsx": AppTs,
|
||||
"/data.js": data,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
...reactTs,
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
const data = `export const animals = [
|
||||
{label: "Cat", value: "cat", description: "The second most popular pet in the world"},
|
||||
{label: "Dog", value: "dog", description: "The most popular pet in the world"},
|
||||
{label: "Elephant", value: "elephant", description: "The largest land animal"},
|
||||
{label: "Lion", value: "lion", description: "The king of the jungle"},
|
||||
{label: "Tiger", value: "tiger", description: "The largest cat species"},
|
||||
{label: "Giraffe", value: "giraffe", description: "The tallest land animal"},
|
||||
{
|
||||
label: "Dolphin",
|
||||
value: "dolphin",
|
||||
description: "A widely distributed and diverse group of aquatic mammals",
|
||||
},
|
||||
{label: "Penguin", value: "penguin", description: "A group of aquatic flightless birds"},
|
||||
{label: "Zebra", value: "zebra", description: "A several species of African equids"},
|
||||
{
|
||||
label: "Shark",
|
||||
value: "shark",
|
||||
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
|
||||
},
|
||||
{
|
||||
label: "Whale",
|
||||
value: "whale",
|
||||
description: "Diverse group of fully aquatic placental marine mammals",
|
||||
},
|
||||
{label: "Otter", value: "otter", description: "A carnivorous mammal in the subfamily Lutrinae"},
|
||||
{label: "Crocodile", value: "crocodile", description: "A large semiaquatic reptile"},
|
||||
];`;
|
||||
|
||||
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {animals} from "./data";
|
||||
|
||||
export default function App() {
|
||||
const myFilter = (textValue, inputValue) => {
|
||||
if (inputValue.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Normalize both strings so we can slice safely
|
||||
// take into account the ignorePunctuation option as well...
|
||||
textValue = textValue.normalize("NFC").toLocaleLowerCase();
|
||||
inputValue = inputValue.normalize("NFC").toLocaleLowerCase();
|
||||
|
||||
return textValue.slice(0, inputValue.length) === inputValue;
|
||||
};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
allowsCustomValue
|
||||
className="max-w-xs"
|
||||
defaultFilter={myFilter}
|
||||
defaultItems={animals}
|
||||
label="Search an animal"
|
||||
variant="bordered"
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const AppTs = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {animals} from "./data";
|
||||
|
||||
export default function App() {
|
||||
const myFilter = (textValue: string, inputValue: string) => {
|
||||
if (inputValue.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Normalize both strings so we can slice safely
|
||||
// take into account the ignorePunctuation option as well...
|
||||
textValue = textValue.normalize("NFC").toLocaleLowerCase();
|
||||
inputValue = inputValue.normalize("NFC").toLocaleLowerCase();
|
||||
|
||||
return textValue.slice(0, inputValue.length) === inputValue;
|
||||
};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
className="max-w-xs"
|
||||
defaultFilter={myFilter}
|
||||
defaultItems={animals}
|
||||
label="Search an animal"
|
||||
variant="bordered"
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
"/data.js": data,
|
||||
};
|
||||
|
||||
const reactTs = {
|
||||
"/App.tsx": AppTs,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
...reactTs,
|
||||
};
|
||||
@@ -0,0 +1,239 @@
|
||||
const data = `export const users = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Tony Reichert",
|
||||
role: "CEO",
|
||||
team: "Management",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/1.png",
|
||||
email: "tony.reichert@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Zoey Lang",
|
||||
role: "Tech Lead",
|
||||
team: "Development",
|
||||
status: "paused",
|
||||
age: "25",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/1.png",
|
||||
email: "zoey.lang@example.com",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Jane Fisher",
|
||||
role: "Sr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/2.png",
|
||||
email: "jane.fisher@example.com",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "William Howard",
|
||||
role: "C.M.",
|
||||
team: "Marketing",
|
||||
status: "vacation",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/2.png",
|
||||
email: "william.howard@example.com",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kristen Copper",
|
||||
role: "S. Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "24",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/3.png",
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/3.png",
|
||||
email: "brian.kim@example.com",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Michael Hunt",
|
||||
role: "Designer",
|
||||
team: "Design",
|
||||
status: "paused",
|
||||
age: "27",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/4.png",
|
||||
email: "michael.hunt@example.com",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Samantha Brooks",
|
||||
role: "HR Manager",
|
||||
team: "HR",
|
||||
status: "active",
|
||||
age: "31",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/4.png",
|
||||
email: "samantha.brooks@example.com",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Frank Harrison",
|
||||
role: "F. Manager",
|
||||
team: "Finance",
|
||||
status: "vacation",
|
||||
age: "33",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/5.png",
|
||||
email: "frank.harrison@example.com",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Emma Adams",
|
||||
role: "Ops Manager",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "35",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/5.png",
|
||||
email: "emma.adams@example.com",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Brandon Stevens",
|
||||
role: "Jr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/7.png",
|
||||
email: "brandon.stevens@example.com",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Megan Richards",
|
||||
role: "P. Manager",
|
||||
team: "Product",
|
||||
status: "paused",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/7.png",
|
||||
email: "megan.richards@example.com",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Oliver Scott",
|
||||
role: "S. Manager",
|
||||
team: "Security",
|
||||
status: "active",
|
||||
age: "37",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/8.png",
|
||||
email: "oliver.scott@example.com",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Grace Allen",
|
||||
role: "M. Specialist",
|
||||
team: "Marketing",
|
||||
status: "active",
|
||||
age: "30",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/8.png",
|
||||
email: "grace.allen@example.com",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Noah Carter",
|
||||
role: "IT Specialist",
|
||||
team: "I. Technology",
|
||||
status: "paused",
|
||||
age: "31",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/9.png",
|
||||
email: "noah.carter@example.com",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Ava Perez",
|
||||
role: "Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/9.png",
|
||||
email: "ava.perez@example.com",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Liam Johnson",
|
||||
role: "Data Analyst",
|
||||
team: "Analysis",
|
||||
status: "active",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/11.png",
|
||||
email: "liam.johnson@example.com",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Sophia Taylor",
|
||||
role: "QA Analyst",
|
||||
team: "Testing",
|
||||
status: "active",
|
||||
age: "27",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/11.png",
|
||||
email: "sophia.taylor@example.com",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Lucas Harris",
|
||||
role: "Administrator",
|
||||
team: "Information Technology",
|
||||
status: "paused",
|
||||
age: "32",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/12.png",
|
||||
email: "lucas.harris@example.com",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Mia Robinson",
|
||||
role: "Coordinator",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "26",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/12.png",
|
||||
email: "mia.robinson@example.com",
|
||||
},
|
||||
];`;
|
||||
|
||||
const App = `import {Autocomplete, AutocompleteItem, Avatar} from "@nextui-org/react";
|
||||
import {users} from "./data";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Autocomplete
|
||||
defaultItems={users}
|
||||
variant="bordered"
|
||||
label="Assigned to"
|
||||
placeholder="Select a user"
|
||||
labelPlacement="inside"
|
||||
className="max-w-xs"
|
||||
>
|
||||
{(user) => (
|
||||
<AutocompleteItem key={user.id} textValue={user.name}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar alt={user.name} className="flex-shrink-0" size="sm" src={user.avatar} />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-small">{user.name}</span>
|
||||
<span className="text-tiny text-default-400">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
"/data.js": data,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
const App = `import {Autocomplete, AutocompleteItem, AutocompleteSection} from "@nextui-org/react";
|
||||
|
||||
export default function App() {
|
||||
const headingClasses = "flex w-full sticky top-1 z-20 py-1.5 px-2 bg-default-100 shadow-small rounded-small";
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
label="Favorite Animal"
|
||||
variant="bordered"
|
||||
placeholder="Search an animal"
|
||||
className="max-w-xs"
|
||||
scrollShadowProps={{
|
||||
isEnabled: false,
|
||||
}}
|
||||
>
|
||||
<AutocompleteSection
|
||||
title="Mammals"
|
||||
classNames={{
|
||||
heading: headingClasses,
|
||||
}}
|
||||
>
|
||||
<AutocompleteItem key="Lion">Lion</AutocompleteItem>
|
||||
<AutocompleteItem key="Tiger">Tiger</AutocompleteItem>
|
||||
<AutocompleteItem key="Elephant">Elephant</AutocompleteItem>
|
||||
<AutocompleteItem key="Kangaroo">Kangaroo</AutocompleteItem>
|
||||
<AutocompleteItem key="Panda">Panda</AutocompleteItem>
|
||||
<AutocompleteItem key="Giraffe">Giraffe</AutocompleteItem>
|
||||
<AutocompleteItem key="Zebra">Zebra</AutocompleteItem>
|
||||
<AutocompleteItem key="Cheetah">Cheetah</AutocompleteItem>
|
||||
</AutocompleteSection>
|
||||
<AutocompleteSection
|
||||
title="Birds"
|
||||
classNames={{
|
||||
heading: headingClasses,
|
||||
}}
|
||||
>
|
||||
<AutocompleteItem key="Eagle">Eagle</AutocompleteItem>
|
||||
<AutocompleteItem key="Parrot">Parrot</AutocompleteItem>
|
||||
<AutocompleteItem key="Penguin">Penguin</AutocompleteItem>
|
||||
<AutocompleteItem key="Ostrich">Ostrich</AutocompleteItem>
|
||||
<AutocompleteItem key="Peacock">Peacock</AutocompleteItem>
|
||||
<AutocompleteItem key="Swan">Swan</AutocompleteItem>
|
||||
<AutocompleteItem key="Falcon">Falcon</AutocompleteItem>
|
||||
<AutocompleteItem key="Flamingo">Flamingo</AutocompleteItem>
|
||||
</AutocompleteSection>
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
const data = `export const animals = [
|
||||
{label: "Cat", value: "cat", description: "The second most popular pet in the world"},
|
||||
{label: "Dog", value: "dog", description: "The most popular pet in the world"},
|
||||
{label: "Elephant", value: "elephant", description: "The largest land animal"},
|
||||
{label: "Lion", value: "lion", description: "The king of the jungle"},
|
||||
{label: "Tiger", value: "tiger", description: "The largest cat species"},
|
||||
{label: "Giraffe", value: "giraffe", description: "The tallest land animal"},
|
||||
{
|
||||
label: "Dolphin",
|
||||
value: "dolphin",
|
||||
description: "A widely distributed and diverse group of aquatic mammals",
|
||||
},
|
||||
{label: "Penguin", value: "penguin", description: "A group of aquatic flightless birds"},
|
||||
{label: "Zebra", value: "zebra", description: "A several species of African equids"},
|
||||
{
|
||||
label: "Shark",
|
||||
value: "shark",
|
||||
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
|
||||
},
|
||||
{
|
||||
label: "Whale",
|
||||
value: "whale",
|
||||
description: "Diverse group of fully aquatic placental marine mammals",
|
||||
},
|
||||
{label: "Otter", value: "otter", description: "A carnivorous mammal in the subfamily Lutrinae"},
|
||||
{label: "Crocodile", value: "crocodile", description: "A large semiaquatic reptile"},
|
||||
];`;
|
||||
|
||||
const SelectorIcon = `export const SelectorIcon = (props) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
{...props}
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" stroke="none" />
|
||||
<path d="M8 9l4 -4l4 4" />
|
||||
<path d="M16 15l-4 4l-4 -4" />
|
||||
</svg>
|
||||
);`;
|
||||
|
||||
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {SelectorIcon} from "./SelectorIcon";
|
||||
import {animals} from "./data";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Autocomplete
|
||||
label="Favorite Animal"
|
||||
placeholder="Search an animal"
|
||||
defaultItems={animals}
|
||||
labelPlacement="outside"
|
||||
className="max-w-xs"
|
||||
disableSelectorIconRotation
|
||||
selectorIcon={<SelectorIcon />}
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
"/data.js": data,
|
||||
"/SelectorIcon.jsx": SelectorIcon,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
};
|
||||
@@ -0,0 +1,319 @@
|
||||
const data = `export const users = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Tony Reichert",
|
||||
role: "CEO",
|
||||
team: "Management",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/1.png",
|
||||
email: "tony.reichert@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Zoey Lang",
|
||||
role: "Tech Lead",
|
||||
team: "Development",
|
||||
status: "paused",
|
||||
age: "25",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/1.png",
|
||||
email: "zoey.lang@example.com",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Jane Fisher",
|
||||
role: "Sr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/2.png",
|
||||
email: "jane.fisher@example.com",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "William Howard",
|
||||
role: "C.M.",
|
||||
team: "Marketing",
|
||||
status: "vacation",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/2.png",
|
||||
email: "william.howard@example.com",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kristen Copper",
|
||||
role: "S. Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "24",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/3.png",
|
||||
email: "kristen.cooper@example.com",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Brian Kim",
|
||||
role: "P. Manager",
|
||||
team: "Management",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/3.png",
|
||||
email: "brian.kim@example.com",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Michael Hunt",
|
||||
role: "Designer",
|
||||
team: "Design",
|
||||
status: "paused",
|
||||
age: "27",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/4.png",
|
||||
email: "michael.hunt@example.com",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Samantha Brooks",
|
||||
role: "HR Manager",
|
||||
team: "HR",
|
||||
status: "active",
|
||||
age: "31",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/4.png",
|
||||
email: "samantha.brooks@example.com",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Frank Harrison",
|
||||
role: "F. Manager",
|
||||
team: "Finance",
|
||||
status: "vacation",
|
||||
age: "33",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/5.png",
|
||||
email: "frank.harrison@example.com",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Emma Adams",
|
||||
role: "Ops Manager",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "35",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/5.png",
|
||||
email: "emma.adams@example.com",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Brandon Stevens",
|
||||
role: "Jr. Dev",
|
||||
team: "Development",
|
||||
status: "active",
|
||||
age: "22",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/7.png",
|
||||
email: "brandon.stevens@example.com",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Megan Richards",
|
||||
role: "P. Manager",
|
||||
team: "Product",
|
||||
status: "paused",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/7.png",
|
||||
email: "megan.richards@example.com",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Oliver Scott",
|
||||
role: "S. Manager",
|
||||
team: "Security",
|
||||
status: "active",
|
||||
age: "37",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/8.png",
|
||||
email: "oliver.scott@example.com",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Grace Allen",
|
||||
role: "M. Specialist",
|
||||
team: "Marketing",
|
||||
status: "active",
|
||||
age: "30",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/8.png",
|
||||
email: "grace.allen@example.com",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Noah Carter",
|
||||
role: "IT Specialist",
|
||||
team: "I. Technology",
|
||||
status: "paused",
|
||||
age: "31",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/9.png",
|
||||
email: "noah.carter@example.com",
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Ava Perez",
|
||||
role: "Manager",
|
||||
team: "Sales",
|
||||
status: "active",
|
||||
age: "29",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/9.png",
|
||||
email: "ava.perez@example.com",
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Liam Johnson",
|
||||
role: "Data Analyst",
|
||||
team: "Analysis",
|
||||
status: "active",
|
||||
age: "28",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/11.png",
|
||||
email: "liam.johnson@example.com",
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: "Sophia Taylor",
|
||||
role: "QA Analyst",
|
||||
team: "Testing",
|
||||
status: "active",
|
||||
age: "27",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/11.png",
|
||||
email: "sophia.taylor@example.com",
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: "Lucas Harris",
|
||||
role: "Administrator",
|
||||
team: "Information Technology",
|
||||
status: "paused",
|
||||
age: "32",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/12.png",
|
||||
email: "lucas.harris@example.com",
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: "Mia Robinson",
|
||||
role: "Coordinator",
|
||||
team: "Operations",
|
||||
status: "active",
|
||||
age: "26",
|
||||
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/12.png",
|
||||
email: "mia.robinson@example.com",
|
||||
},
|
||||
];`;
|
||||
|
||||
const SearchIcon = `export const SearchIcon = ({
|
||||
size = 24,
|
||||
strokeWidth = 1.5,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={height || size}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={width || size}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
<path
|
||||
d="M22 22L20 20"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</svg>
|
||||
);`;
|
||||
|
||||
const App = `import {Autocomplete, AutocompleteItem, Avatar, Button} from "@nextui-org/react";
|
||||
import {SearchIcon} from "./SearchIcon";
|
||||
import {users} from "./data";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Autocomplete
|
||||
classNames={{
|
||||
base: "max-w-xs",
|
||||
listboxWrapper: "max-h-[320px]",
|
||||
selectorButton: "text-default-500"
|
||||
}}
|
||||
defaultItems={users}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
input: "ml-1",
|
||||
inputWrapper: "h-[48px]",
|
||||
},
|
||||
}}
|
||||
listboxProps={{
|
||||
hideSelectedIcon: true,
|
||||
itemClasses: {
|
||||
base: [
|
||||
"rounded-medium",
|
||||
"text-default-500",
|
||||
"transition-opacity",
|
||||
"data-[hover=true]:text-foreground",
|
||||
"dark:data-[hover=true]:bg-default-50",
|
||||
"data-[pressed=true]:opacity-70",
|
||||
"data-[hover=true]:bg-default-200",
|
||||
"data-[selectable=true]:focus:bg-default-100",
|
||||
"data-[focus-visible=true]:ring-default-500",
|
||||
],
|
||||
},
|
||||
}}
|
||||
aria-label="Select an employee"
|
||||
placeholder="Enter employee name"
|
||||
popoverProps={{
|
||||
offset: 10,
|
||||
classNames: {
|
||||
base: "rounded-large",
|
||||
content: "p-1 border-small border-default-100 bg-background",
|
||||
},
|
||||
}}
|
||||
startContent={<SearchIcon className="text-default-400" strokeWidth={2.5} size={20} />}
|
||||
radius="full"
|
||||
variant="bordered"
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.id} textValue={item.name}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar alt={item.name} className="flex-shrink-0" size="sm" src={item.avatar} />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-small">{item.name}</span>
|
||||
<span className="text-tiny text-default-400">{item.team}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="border-small mr-0.5 font-medium shadow-small"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
"/SearchIcon.jsx": SearchIcon,
|
||||
"/data.js": data,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
const data = `export const animals = [
|
||||
{label: "Cat", value: "cat", description: "The second most popular pet in the world"},
|
||||
{label: "Dog", value: "dog", description: "The most popular pet in the world"},
|
||||
{label: "Elephant", value: "elephant", description: "The largest land animal"},
|
||||
{label: "Lion", value: "lion", description: "The king of the jungle"},
|
||||
{label: "Tiger", value: "tiger", description: "The largest cat species"},
|
||||
{label: "Giraffe", value: "giraffe", description: "The tallest land animal"},
|
||||
{
|
||||
label: "Dolphin",
|
||||
value: "dolphin",
|
||||
description: "A widely distributed and diverse group of aquatic mammals",
|
||||
},
|
||||
{label: "Penguin", value: "penguin", description: "A group of aquatic flightless birds"},
|
||||
{label: "Zebra", value: "zebra", description: "A several species of African equids"},
|
||||
{
|
||||
label: "Shark",
|
||||
value: "shark",
|
||||
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
|
||||
},
|
||||
{
|
||||
label: "Whale",
|
||||
value: "whale",
|
||||
description: "Diverse group of fully aquatic placental marine mammals",
|
||||
},
|
||||
{label: "Otter", value: "otter", description: "A carnivorous mammal in the subfamily Lutrinae"},
|
||||
{label: "Crocodile", value: "crocodile", description: "A large semiaquatic reptile"},
|
||||
];`;
|
||||
|
||||
const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
|
||||
import {animals} from "./data";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Autocomplete
|
||||
allowsCustomValue
|
||||
label="Search an animal"
|
||||
variant="bordered"
|
||||
className="max-w-xs"
|
||||
defaultItems={animals}
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
);
|
||||
}`;
|
||||
|
||||
const react = {
|
||||
"/App.jsx": App,
|
||||
"/data.js": data,
|
||||
};
|
||||
|
||||
export default {
|
||||
...react,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user