feat(site): add component documentation site
Add Vite, React, MDX, and Tailwind documentation infrastructure. Document component, handbook, overview, and utility routes. Add interactive examples for every component family. Deduplicate React resolution and load the Tailwind 4 theme configuration.
This commit is contained in:
+613
@@ -0,0 +1,613 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@tanstack/react-virtual": "^3.14.5",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"lucide-react": "^1.23.0",
|
||||
"match-sorter": "^8.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rehype-pretty-code": "^0.14.4",
|
||||
"remark-directive": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shiki": "^4.3.1",
|
||||
"vite": "^8.1.3",
|
||||
"zod": "^4.4.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdx-js/rollup": "^3.1.0",
|
||||
"@tailwindcss/postcss": "^4.3.2",
|
||||
"@types/node": "^26.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "^6.0.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
||||
|
||||
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
|
||||
|
||||
"@mdx-js/rollup": ["@mdx-js/rollup@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@rollup/pluginutils": "^5.0.0", "source-map": "^0.7.0", "vfile": "^6.0.0" }, "peerDependencies": { "rollup": ">=2" } }, "sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.4", "", { "os": "none", "cpu": "arm64" }, "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.4", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" } }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="],
|
||||
|
||||
"@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.2" } }, "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.2", "@tailwindcss/oxide-darwin-arm64": "4.3.2", "@tailwindcss/oxide-darwin-x64": "4.3.2", "@tailwindcss/oxide-freebsd-x64": "4.3.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", "@tailwindcss/oxide-linux-x64-musl": "4.3.2", "@tailwindcss/oxide-wasm32-wasi": "4.3.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.2", "", { "os": "android", "cpu": "arm64" }, "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.2", "", { "cpu": "none" }, "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ=="],
|
||||
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.2", "@tailwindcss/oxide": "4.3.2", "postcss": "^8.5.15", "tailwindcss": "4.3.2" } }, "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g=="],
|
||||
|
||||
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.5", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A=="],
|
||||
|
||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.2", "", {}, "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="],
|
||||
|
||||
"acorn": ["acorn@8.17.0", "", { "bin": "bin/acorn" }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"astring": ["astring@1.9.0", "", { "bin": "bin/astring" }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.5.2", "", { "dependencies": { "browserslist": "^4.28.4", "caniuse-lite": "^1.0.30001799", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.42", "", { "bin": "dist/cli.cjs" }, "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.4", "", { "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", "electron-to-chromium": "^1.5.376", "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": "cli.js" }, "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001800", "", {}, "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.387", "", {}, "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
|
||||
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
|
||||
|
||||
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
||||
|
||||
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
|
||||
|
||||
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
||||
|
||||
"estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="],
|
||||
|
||||
"estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="],
|
||||
|
||||
"estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
|
||||
|
||||
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
||||
|
||||
"hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||
|
||||
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
|
||||
|
||||
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"jiti": ["jiti@2.7.0", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lucide-react": ["lucide-react@1.23.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"match-sorter": ["match-sorter@8.3.0", "", { "dependencies": { "@babel/runtime": "^7.23.8", "remove-accents": "0.5.0" } }, "sha512-8Py1GbZi5zsclYSFcPAH4H5xfTbeD0bOREA7qP/t8bW4MbOSlPl8sbqHOedEV7O+Bxyvxm6xs/v6BXJGe+JDNA=="],
|
||||
|
||||
"mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="],
|
||||
|
||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||
|
||||
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||
|
||||
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||
|
||||
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||
|
||||
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||
|
||||
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||
|
||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||
|
||||
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
|
||||
|
||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||
|
||||
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
|
||||
|
||||
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
|
||||
|
||||
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
|
||||
|
||||
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="],
|
||||
|
||||
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||
|
||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||
|
||||
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||
|
||||
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||
|
||||
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||
|
||||
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||
|
||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||
|
||||
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
|
||||
|
||||
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
|
||||
|
||||
"micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="],
|
||||
|
||||
"micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="],
|
||||
|
||||
"micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="],
|
||||
|
||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||
|
||||
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||
|
||||
"micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="],
|
||||
|
||||
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
|
||||
|
||||
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
|
||||
|
||||
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
|
||||
|
||||
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
|
||||
|
||||
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
|
||||
|
||||
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
|
||||
|
||||
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="],
|
||||
|
||||
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
|
||||
|
||||
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
|
||||
|
||||
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.15", "", { "bin": "bin/nanoid.cjs" }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
||||
"parse-numeric-range": ["parse-numeric-range@1.3.0", "", {}, "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
|
||||
|
||||
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||
|
||||
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
||||
|
||||
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
|
||||
|
||||
"recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="],
|
||||
|
||||
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="],
|
||||
|
||||
"rehype-pretty-code": ["rehype-pretty-code@0.14.4", "", { "dependencies": { "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.0", "parse-numeric-range": "^1.3.0", "rehype-parse": "^9.0.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "shiki": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, "sha512-FKPkiSnTqtJHKjCwfqfQs0tXSkmcq7K3jsryZI0rpazo2kWgSYw+sk18GAhxDzKdPk004WWWZ5yiZFEUQhN4Bg=="],
|
||||
|
||||
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
|
||||
|
||||
"remark-directive": ["remark-directive@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^4.0.0", "unified": "^11.0.0" } }, "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
|
||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"remove-accents": ["remove-accents@0.5.0", "", {}, "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="],
|
||||
|
||||
"rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": "bin/cli.mjs" }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="],
|
||||
|
||||
"rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.2", "", {}, "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vite": ["vite@8.1.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.16", "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@vitejs/devtools", "esbuild", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Base UI Tailwind Components</title>
|
||||
<script>
|
||||
; (() => {
|
||||
const storedTheme = localStorage.getItem("base-ui-tailwind-theme")
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
const theme = storedTheme === "light" || storedTheme === "dark" ? storedTheme : prefersDark ? "dark" : "light"
|
||||
document.documentElement.classList.toggle("dark", theme === "dark")
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="isolate" id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview --host 127.0.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@tanstack/react-virtual": "^3.14.5",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"lucide-react": "^1.23.0",
|
||||
"match-sorter": "^8.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rehype-pretty-code": "^0.14.4",
|
||||
"remark-directive": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shiki": "^4.3.1",
|
||||
"vite": "^8.1.3",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdx-js/rollup": "^3.1.0",
|
||||
"@tailwindcss/postcss": "^4.3.2",
|
||||
"@types/node": "^26.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import * as React from "react"
|
||||
import { Layout } from "./components/layout"
|
||||
import { mdxComponents } from "./components/mdx-components"
|
||||
import {
|
||||
componentDocs,
|
||||
getCurrentRoute,
|
||||
handbookDocs,
|
||||
overviewDocs,
|
||||
utilityDocs,
|
||||
} from "./routes"
|
||||
|
||||
export type Theme = "light" | "dark"
|
||||
export type TocItem = {
|
||||
id: string
|
||||
depth: 1 | 2 | 3
|
||||
title: string
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const [route, setRoute] = React.useState(getCurrentRoute)
|
||||
const [activeHash, setActiveHash] = React.useState(() =>
|
||||
window.location.hash.replace("#", ""),
|
||||
)
|
||||
const [theme, setTheme] = React.useState<Theme>(() =>
|
||||
document.documentElement.classList.contains("dark") ? "dark" : "light",
|
||||
)
|
||||
const [tocItems, setTocItems] = React.useState<TocItem[]>([])
|
||||
const [activeTocIds, setActiveTocIds] = React.useState<string[]>([])
|
||||
const articleRef = React.useRef<HTMLElement>(null)
|
||||
const docRoute = route || "overview/quick-start"
|
||||
const activeOverviewDoc = docRoute.startsWith("overview/")
|
||||
? overviewDocs.find((doc) => `overview/${doc.slug}` === docRoute)
|
||||
: undefined
|
||||
const activeHandbookDoc = docRoute.startsWith("handbook/")
|
||||
? handbookDocs.find((doc) => `handbook/${doc.slug}` === docRoute)
|
||||
: undefined
|
||||
const activeComponentDoc = docRoute.startsWith("components/")
|
||||
? componentDocs.find((doc) => `components/${doc.slug}` === docRoute)
|
||||
: undefined
|
||||
const activeUtilityDoc = docRoute.startsWith("utils/")
|
||||
? utilityDocs.find((doc) => `utils/${doc.slug}` === docRoute)
|
||||
: undefined
|
||||
const Page =
|
||||
activeOverviewDoc?.Component ??
|
||||
activeHandbookDoc?.Component ??
|
||||
activeComponentDoc?.Component ??
|
||||
activeUtilityDoc?.Component ??
|
||||
overviewDocs[0].Component
|
||||
const activeDocTitle =
|
||||
activeOverviewDoc?.title ??
|
||||
activeHandbookDoc?.title ??
|
||||
activeComponentDoc?.title ??
|
||||
activeUtilityDoc?.title ??
|
||||
overviewDocs[0].title
|
||||
const toggleTheme = React.useCallback(() => {
|
||||
setTheme((currentTheme) => (currentTheme === "dark" ? "light" : "dark"))
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const syncLocation = () => {
|
||||
setRoute(getCurrentRoute())
|
||||
setActiveHash(window.location.hash.replace("#", ""))
|
||||
}
|
||||
|
||||
const onPopState = () => syncLocation()
|
||||
const onHashChange = () => syncLocation()
|
||||
window.addEventListener("popstate", onPopState)
|
||||
window.addEventListener("hashchange", onHashChange)
|
||||
return () => {
|
||||
window.removeEventListener("popstate", onPopState)
|
||||
window.removeEventListener("hashchange", onHashChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
window.history.scrollRestoration = "manual"
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
document.title = `${activeDocTitle} - Base UI Tailwind`
|
||||
}, [activeDocTitle])
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
if (activeHash) {
|
||||
document.getElementById(activeHash)?.scrollIntoView({ block: "start" })
|
||||
return
|
||||
}
|
||||
|
||||
window.scrollTo({ left: 0, top: 0 })
|
||||
})
|
||||
|
||||
return () => window.cancelAnimationFrame(frame)
|
||||
}, [docRoute, activeHash])
|
||||
|
||||
React.useEffect(() => {
|
||||
document.documentElement.classList.toggle("dark", theme === "dark")
|
||||
localStorage.setItem("base-ui-tailwind-theme", theme)
|
||||
}, [theme])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key.toLowerCase() !== "d" || !event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
toggleTheme()
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown)
|
||||
return () => window.removeEventListener("keydown", onKeyDown)
|
||||
}, [toggleTheme])
|
||||
|
||||
React.useEffect(() => {
|
||||
const headings = Array.from(
|
||||
articleRef.current?.querySelectorAll<HTMLHeadingElement>("h1[id], h2[id], h3[id]") ??
|
||||
[],
|
||||
)
|
||||
const seenIds = new Map<string, number>()
|
||||
|
||||
setTocItems(
|
||||
headings.map((heading) => {
|
||||
const baseId = heading.id || "section"
|
||||
const count = seenIds.get(baseId) ?? 0
|
||||
seenIds.set(baseId, count + 1)
|
||||
|
||||
const id = count === 0 ? baseId : `${baseId}-${count + 1}`
|
||||
if (heading.id !== id) {
|
||||
heading.id = id
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
depth: heading.tagName === "H1" ? 1 : heading.tagName === "H3" ? 3 : 2,
|
||||
title: heading.textContent ?? "",
|
||||
}
|
||||
}),
|
||||
)
|
||||
setActiveTocIds([])
|
||||
}, [docRoute, Page])
|
||||
|
||||
React.useEffect(() => {
|
||||
const headings = Array.from(
|
||||
articleRef.current?.querySelectorAll<HTMLHeadingElement>("h1[id], h2[id], h3[id]") ??
|
||||
[],
|
||||
)
|
||||
if (headings.length === 0) {
|
||||
setActiveTocIds([])
|
||||
return
|
||||
}
|
||||
|
||||
const visibleIds = new Set<string>()
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
const id = entry.target.id
|
||||
if (!id) continue
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
visibleIds.add(id)
|
||||
} else {
|
||||
visibleIds.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
setActiveTocIds(Array.from(visibleIds))
|
||||
},
|
||||
{
|
||||
rootMargin: "0px 0px -15% 0px",
|
||||
threshold: 0,
|
||||
},
|
||||
)
|
||||
|
||||
headings.forEach((heading) => observer.observe(heading))
|
||||
return () => observer.disconnect()
|
||||
}, [docRoute, tocItems])
|
||||
|
||||
function navigate(nextRoute: string, hash?: string) {
|
||||
const nextPath = nextRoute ? `/${nextRoute}` : "/"
|
||||
const nextUrl = hash ? `${nextPath}#${hash}` : nextPath
|
||||
|
||||
window.history.pushState({}, "", nextUrl)
|
||||
setRoute(nextRoute)
|
||||
setActiveHash(hash ?? "")
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
activeRoute={docRoute}
|
||||
activeHash={activeHash}
|
||||
theme={theme}
|
||||
activeTocIds={activeTocIds}
|
||||
tocItems={tocItems}
|
||||
onNavigate={navigate}
|
||||
onThemeToggle={toggleTheme}
|
||||
>
|
||||
<article ref={articleRef} className="w-full max-w-4xl">
|
||||
<Page components={mdxComponents} />
|
||||
</article>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import * as React from "react"
|
||||
import { useExampleCodeContext, useInExampleContext } from "./example-container"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type CodeBlockProps = React.HTMLAttributes<HTMLPreElement>
|
||||
|
||||
const collapsedHeight = "9rem"
|
||||
|
||||
export function CodeBlock({ children, ...props }: CodeBlockProps) {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
const blockRef = React.useRef<HTMLDivElement>(null)
|
||||
const exampleCodeContext = useExampleCodeContext()
|
||||
const inExampleContext = useInExampleContext()
|
||||
const isExampleCode = exampleCodeContext != null
|
||||
const code = extractText(children)
|
||||
const lineCount = exampleCodeContext?.lineCount ?? code.split("\n").length
|
||||
const collapsible = isExampleCode && lineCount > 6
|
||||
|
||||
React.useEffect(() => {
|
||||
setExpanded(false)
|
||||
}, [code])
|
||||
|
||||
function toggleExpanded() {
|
||||
if (expanded) {
|
||||
setExpanded(false)
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const preview = blockRef.current
|
||||
?.closest(".component-example")
|
||||
?.querySelector<HTMLElement>(".example-preview")
|
||||
|
||||
if (!preview) {
|
||||
return
|
||||
}
|
||||
|
||||
const rect = preview.getBoundingClientRect()
|
||||
const belowViewport = rect.bottom > window.innerHeight
|
||||
if (rect.top < 16 || belowViewport) {
|
||||
window.scrollTo({
|
||||
left: window.scrollX,
|
||||
top: window.scrollY + rect.top - 16,
|
||||
})
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setExpanded(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"code-block group/code relative z-1 mt-4 overflow-visible border border-neutral-200 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-900",
|
||||
inExampleContext ? "rounded-b-xl" : "rounded-xl",
|
||||
)} ref={blockRef}>
|
||||
<pre
|
||||
{...props}
|
||||
className={[
|
||||
"bg-transparent p-4 text-xs leading-6",
|
||||
collapsible && !expanded ? "overflow-hidden" : "overflow-auto",
|
||||
].join(" ")}
|
||||
style={{
|
||||
...(props.style ?? {}),
|
||||
maxHeight: collapsible && !expanded ? collapsedHeight : undefined,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
{collapsible ? (
|
||||
<div
|
||||
className={[
|
||||
"sticky z-1 flex justify-center",
|
||||
expanded
|
||||
? "bg-transparent bottom-0 h-11 -mt-11"
|
||||
: "pointer-events-none -mt-32 h-32 items-end bg-gradient-to-b from-neutral-50/0 via-neutral-50/85 to-neutral-50 pb-8 dark:from-neutral-900/0 dark:via-neutral-900/85 dark:to-neutral-900 rounded-b-xl",
|
||||
].join(" ")}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleExpanded}
|
||||
className="pointer-events-auto inline-flex h-8 items-center rounded-full border border-neutral-200 bg-white px-4 text-xs font-medium text-neutral-950 shadow-sm hover:bg-neutral-100 focus-visible:outline-2 focus-visible:[outline-style:solid] focus-visible:outline-offset-2 focus-visible:outline-neutral-950 dark:border-neutral-700 dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:focus-visible:outline-white"
|
||||
>
|
||||
{expanded ? "Hide Code" : "Show Code"}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function extractText(node: React.ReactNode): string {
|
||||
if (typeof node === "string" || typeof node === "number") {
|
||||
return String(node)
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
return node.map(extractText).join("")
|
||||
}
|
||||
|
||||
if (React.isValidElement<{ children?: React.ReactNode }>(node)) {
|
||||
return extractText(node.props.children)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as React from "react"
|
||||
|
||||
type ExampleCodeContextValue = {
|
||||
lineCount: number | null
|
||||
}
|
||||
|
||||
const ExampleCodeContext = React.createContext<ExampleCodeContextValue | null>(null)
|
||||
const InExampleContext = React.createContext(false);
|
||||
|
||||
type ExampleContainerProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
type ExamplePreviewProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
type ExampleCodeProps = {
|
||||
children: React.ReactNode
|
||||
lineCount?: number | string
|
||||
}
|
||||
|
||||
export function ExampleContainer({ children }: ExampleContainerProps) {
|
||||
return (
|
||||
<InExampleContext.Provider value={true}>
|
||||
<div className="component-example rounded-xl mt-4 overflow-visible border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-950">
|
||||
{children}
|
||||
</div>
|
||||
</InExampleContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function ExamplePreview({ children }: ExamplePreviewProps) {
|
||||
return (
|
||||
<div className="example-preview relative isolate flex min-h-[8rem] items-center justify-center overflow-hidden p-6 rounded-t-xl">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ExampleCode({ children, lineCount }: ExampleCodeProps) {
|
||||
const parsedLineCount =
|
||||
typeof lineCount === "number" ? lineCount : Number.parseInt(lineCount ?? "", 10)
|
||||
const value = Number.isFinite(parsedLineCount) ? parsedLineCount : null
|
||||
|
||||
return (
|
||||
<ExampleCodeContext.Provider value={{ lineCount: value }}>
|
||||
<div className="[&_.code-block]:mt-0 [&_.code-block]:border-x-0 [&_.code-block]:border-b-0 rounded-b-xl">
|
||||
{children}
|
||||
</div>
|
||||
</ExampleCodeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useExampleCodeContext() {
|
||||
return React.useContext(ExampleCodeContext)
|
||||
}
|
||||
|
||||
export function useInExampleContext() {
|
||||
return React.useContext(InExampleContext)
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import { ArrowLeftIcon, ArrowRightIcon, Moon, Sun } from "lucide-react"
|
||||
import * as React from "react"
|
||||
import type { Theme, TocItem } from "../app"
|
||||
import { componentDocs, handbookDocs, overviewDocs, utilityDocs } from "../routes"
|
||||
|
||||
type LayoutProps = {
|
||||
activeRoute: string
|
||||
activeHash: string
|
||||
children: React.ReactNode
|
||||
theme: Theme
|
||||
activeTocIds: string[]
|
||||
tocItems: TocItem[]
|
||||
onNavigate: (route: string, hash?: string) => void
|
||||
onThemeToggle: () => void
|
||||
}
|
||||
|
||||
export function Layout({
|
||||
activeRoute,
|
||||
activeHash,
|
||||
children,
|
||||
theme,
|
||||
activeTocIds,
|
||||
tocItems,
|
||||
onNavigate,
|
||||
onThemeToggle,
|
||||
}: LayoutProps) {
|
||||
const nextTheme = theme === "dark" ? "light" : "dark"
|
||||
const routeLinks = React.useMemo(
|
||||
() => sidebarGroups.flatMap((group) => group.items.filter((item) => !item.hash)),
|
||||
[],
|
||||
)
|
||||
const activeRouteIndex = routeLinks.findIndex((item) => item.route === activeRoute)
|
||||
const previousRoute = activeRouteIndex > 0 ? routeLinks[activeRouteIndex - 1] : undefined
|
||||
const nextRoute =
|
||||
activeRouteIndex !== -1 && activeRouteIndex < routeLinks.length - 1
|
||||
? routeLinks[activeRouteIndex + 1]
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white text-neutral-950 dark:bg-neutral-950 dark:text-white">
|
||||
<div className="mx-auto grid max-w-screen-2xl grid-cols-1 lg:grid-cols-[260px_minmax(0,1fr)] xl:grid-cols-[260px_minmax(0,1fr)_240px]">
|
||||
<aside className="p-4 lg:sticky lg:top-0 lg:h-screen lg:overflow-auto lg:px-6 dark:border-neutral-800 pb-10">
|
||||
<a
|
||||
href="/overview/quick-start"
|
||||
onClick={(event) =>
|
||||
handleRouteLinkClick(
|
||||
event,
|
||||
{ route: "overview/quick-start" },
|
||||
onNavigate,
|
||||
)
|
||||
}
|
||||
className="mb-8 inline-flex items-center gap-3 text-left text-sm font-semibold text-neutral-950 hover:underline focus-visible:outline-2 focus-visible:outline-neutral-950 dark:text-white dark:focus-visible:outline-white"
|
||||
>
|
||||
<BaseUiLogo className="size-6" />
|
||||
Base UI Tailwind
|
||||
</a>
|
||||
<SidebarNav
|
||||
activeHash={activeHash}
|
||||
activeRoute={activeRoute}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
</aside>
|
||||
<main className="min-w-0 px-4 py-10 sm:px-6 lg:px-10 xl:px-16">
|
||||
{children}
|
||||
<hr className="h-0 border-t border-dashed my-12" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{previousRoute ? (
|
||||
<a
|
||||
href={getRouteHref(previousRoute)}
|
||||
onClick={(event) => handleRouteLinkClick(event, previousRoute, onNavigate)}
|
||||
className="group flex items-center gap-3 text-left text-sm text-neutral-700 underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-neutral-950 dark:text-neutral-300 dark:focus-visible:outline-white duration-150 transition-colors"
|
||||
>
|
||||
<span className="flex size-8 items-center justify-center rounded-full border border-neutral-200 group-hover:bg-neutral-100 dark:border-neutral-800 dark:group-hover:bg-neutral-900">
|
||||
<ArrowLeftIcon size={18} />
|
||||
</span>
|
||||
<span>{previousRoute.title}</span>
|
||||
</a>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
{nextRoute ? (
|
||||
<a
|
||||
href={getRouteHref(nextRoute)}
|
||||
onClick={(event) => handleRouteLinkClick(event, nextRoute, onNavigate)}
|
||||
className="group flex items-center justify-end gap-3 text-right text-sm text-neutral-700 underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-neutral-950 dark:text-neutral-300 dark:focus-visible:outline-white duration-150 transition-colors"
|
||||
>
|
||||
<span>{nextRoute.title}</span>
|
||||
<span className="flex size-8 items-center justify-center rounded-full border border-neutral-200 group-hover:bg-neutral-100 dark:border-neutral-800 dark:group-hover:bg-neutral-900">
|
||||
<ArrowRightIcon size={18} />
|
||||
</span>
|
||||
</a>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<aside className="hidden xl:block">
|
||||
<div className="sticky top-0 h-screen overflow-auto px-6 py-10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onThemeToggle}
|
||||
aria-label={`Switch to ${nextTheme} theme`}
|
||||
title={`Switch to ${nextTheme} theme`}
|
||||
className="mb-8 inline-flex h-8 items-center gap-2 px-2 -mx-2 text-sm text-neutral-600 hover:bg-neutral-100 hover:text-neutral-950 focus-visible:outline-2 focus-visible:outline-neutral-950 dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-white dark:focus-visible:outline-white rounded-xl"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Sun className="size-4" />
|
||||
) : (
|
||||
<Moon className="size-4" />
|
||||
)}
|
||||
<span>{theme === "dark" ? "Light" : "Dark"}</span>
|
||||
<span className="text-neutral-500 dark:text-neutral-400">(⌘D)</span>
|
||||
</button>
|
||||
<nav aria-label="On this page" className="grid gap-2 text-sm">
|
||||
{tocItems.map((item, index) => (
|
||||
<a
|
||||
key={`${item.id}-${index}`}
|
||||
href={`#${item.id}`}
|
||||
className={tocLinkClass(item, activeTocIds.includes(item.id))}
|
||||
>
|
||||
{item.depth === 1 ? "(Top)" : item.title}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BaseUiLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
aria-label="Base UI"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M13.082 6.562a.52.52 0 0 0-.546.529V24a8.727 8.727 0 0 0 .546-17.438M11.446 9.6V24c-4.82 0-8.728-4.298-8.728-9.6V0c4.82 0 8.728 4.298 8.728 9.6Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
type SidebarItem = {
|
||||
title: string
|
||||
route: string
|
||||
hash?: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
status?: "stub"
|
||||
}
|
||||
|
||||
const sidebarGroups: Array<{ title: string; items: SidebarItem[] }> = [
|
||||
{
|
||||
title: "Overview",
|
||||
items: overviewDocs.map((doc, index) => ({
|
||||
title: doc.title,
|
||||
route: `overview/${doc.slug}`,
|
||||
})),
|
||||
},
|
||||
{
|
||||
title: "Handbook",
|
||||
items: handbookDocs.map((doc) => ({
|
||||
title: doc.title,
|
||||
route: `handbook/${doc.slug}`,
|
||||
status: doc.status,
|
||||
})),
|
||||
},
|
||||
{
|
||||
title: "Components",
|
||||
items: componentDocs.map((doc) => ({
|
||||
title: doc.title,
|
||||
route: `components/${doc.slug}`,
|
||||
status: doc.status === "stub" ? "stub" : undefined,
|
||||
})).sort((a, b) => a.title.localeCompare(b.title)),
|
||||
},
|
||||
{
|
||||
title: "Utils",
|
||||
items: utilityDocs.map((doc) => ({
|
||||
title: doc.title,
|
||||
route: `utils/${doc.slug}`,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
function SidebarNav({
|
||||
activeHash,
|
||||
activeRoute,
|
||||
onNavigate,
|
||||
}: {
|
||||
activeHash: string
|
||||
activeRoute: string
|
||||
onNavigate: (route: string, hash?: string) => void
|
||||
}) {
|
||||
return (
|
||||
<nav className="grid gap-6">
|
||||
{sidebarGroups.map((group) => (
|
||||
<div key={group.title} className="grid gap-1">
|
||||
<NavGroupLabel>{group.title}</NavGroupLabel>
|
||||
{group.items.map((item) => (
|
||||
<NavButton
|
||||
key={`${item.route}#${item.hash ?? ""}`}
|
||||
active={isSidebarItemActive(item, activeRoute, activeHash)}
|
||||
item={item}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function NavButton({
|
||||
active,
|
||||
item,
|
||||
onNavigate,
|
||||
}: {
|
||||
active: boolean
|
||||
item: SidebarItem
|
||||
onNavigate: (route: string, hash?: string) => void
|
||||
}) {
|
||||
const Icon = item.icon
|
||||
|
||||
return (
|
||||
<a
|
||||
href={getRouteHref(item)}
|
||||
onClick={(event) => handleRouteLinkClick(event, item, onNavigate)}
|
||||
className={navClass(active)}
|
||||
>
|
||||
{Icon ? <Icon className="size-4" /> : null}
|
||||
<span>{item.title}</span>
|
||||
{item.status === "stub" ? <StubBadge /> : null}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function StubBadge() {
|
||||
return (
|
||||
<span className="ml-auto py-0 text-[10px] uppercase tracking-wide text-neutral-500 dark:border-neutral-800 dark:text-neutral-400 pointer-events-none">
|
||||
stub
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function isSidebarItemActive(
|
||||
item: SidebarItem,
|
||||
activeRoute: string,
|
||||
activeHash: string,
|
||||
) {
|
||||
return item.route === activeRoute && (item.hash ?? "") === activeHash
|
||||
}
|
||||
|
||||
function NavGroupLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="pb-1 text-xs font-medium text-neutral-500 dark:text-neutral-400 capitalize">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function navClass(active: boolean) {
|
||||
return [
|
||||
"flex h-7 items-center gap-2 text-left text-sm",
|
||||
active
|
||||
? "bg-neutral-100 text-neutral-950 no-underline dark:bg-neutral-800 dark:text-white px-2 -mx-2 rounded-xl"
|
||||
: "text-neutral-700 underline-offset-4 hover:underline dark:text-neutral-300",
|
||||
].join(" ")
|
||||
}
|
||||
|
||||
function getRouteHref(item: Pick<SidebarItem, "route" | "hash">) {
|
||||
return `/${item.route}${item.hash ? `#${item.hash}` : ""}`
|
||||
}
|
||||
|
||||
function handleRouteLinkClick(
|
||||
event: React.MouseEvent<HTMLAnchorElement>,
|
||||
item: Pick<SidebarItem, "route" | "hash">,
|
||||
onNavigate: (route: string, hash?: string) => void,
|
||||
) {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
event.button !== 0 ||
|
||||
event.metaKey ||
|
||||
event.altKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
onNavigate(item.route, item.hash)
|
||||
}
|
||||
|
||||
function tocLinkClass(item: TocItem, active: boolean) {
|
||||
return [
|
||||
"underline-offset-4 transition-colors",
|
||||
item.depth === 3 ? "pl-4" : "",
|
||||
active
|
||||
? "font-medium text-neutral-950 dark:text-white"
|
||||
: "text-neutral-500 hover:underline dark:text-neutral-400",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { MDXComponents } from "mdx/types"
|
||||
import * as React from "react"
|
||||
import { CodeBlock } from "./code-block"
|
||||
import { ExampleCode, ExampleContainer, ExamplePreview } from "./example-container"
|
||||
|
||||
export const mdxComponents: MDXComponents = {
|
||||
ExampleCode,
|
||||
ExampleContainer,
|
||||
ExamplePreview,
|
||||
h1: ({ children, id, ...props }) => (
|
||||
<h1
|
||||
id={id ?? "top"}
|
||||
className="mb-4 scroll-mt-20 text-4xl font-semibold tracking-normal text-neutral-950 dark:text-white"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children, id, ...props }) => (
|
||||
<h2
|
||||
id={id ?? slugify(getTextContent(children))}
|
||||
className="mt-10 scroll-mt-20 border-t border-neutral-200 pt-8 text-2xl font-semibold text-neutral-950 dark:border-neutral-800 dark:text-white"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children, id, ...props }) => (
|
||||
<h3
|
||||
id={id ?? slugify(getTextContent(children))}
|
||||
className="mt-8 scroll-mt-20 text-lg font-semibold text-neutral-950 dark:text-white"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: (props) => (
|
||||
<h4 className="mt-6 text-base font-semibold text-neutral-950 dark:text-white" {...props} />
|
||||
),
|
||||
p: (props) => <p className="mt-4 max-w-3xl leading-7 text-neutral-700 dark:text-neutral-300" {...props} />,
|
||||
a: (props) => (
|
||||
<a
|
||||
className="font-medium text-neutral-950 underline underline-offset-2 hover:text-neutral-700 dark:text-white dark:hover:text-neutral-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: (props) => (
|
||||
<ul className="mt-4 grid max-w-3xl gap-2 pl-5 text-neutral-700 dark:text-neutral-300" {...props} />
|
||||
),
|
||||
ol: (props) => (
|
||||
<ol className="mt-4 grid max-w-3xl gap-2 pl-5 text-neutral-700 dark:text-neutral-300" {...props} />
|
||||
),
|
||||
li: (props) => <li className="pl-1 marker:text-neutral-400" {...props} />,
|
||||
blockquote: (props) => (
|
||||
<blockquote
|
||||
className="mt-4 border-l-2 border-neutral-300 pl-4 text-neutral-700 dark:border-neutral-700 dark:text-neutral-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
hr: (props) => <hr className="my-10 border-neutral-200 dark:border-neutral-800" {...props} />,
|
||||
code: ({ className, ...props }) => {
|
||||
const isCodeBlock =
|
||||
className?.includes("language-") ||
|
||||
props["data-language" as keyof typeof props] != null ||
|
||||
props["data-theme" as keyof typeof props] != null
|
||||
|
||||
if (isCodeBlock) {
|
||||
return <code className={className} {...props} />
|
||||
}
|
||||
|
||||
return (
|
||||
<code
|
||||
className={[
|
||||
"rounded bg-neutral-100 px-1.5 py-0.5 text-[0.9em] text-neutral-950 dark:bg-neutral-900 dark:text-white",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
pre: CodeBlock,
|
||||
table: (props) => (
|
||||
<div className="mt-4 overflow-auto">
|
||||
<table className="w-full min-w-[560px] border-collapse text-sm" {...props} />
|
||||
</div>
|
||||
),
|
||||
th: (props) => (
|
||||
<th
|
||||
className="border-b border-neutral-200 px-3 py-2 text-left font-medium dark:border-neutral-800"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
td: (props) => (
|
||||
<td className="border-b border-neutral-200 px-3 py-2 dark:border-neutral-800" {...props} />
|
||||
),
|
||||
}
|
||||
|
||||
function getTextContent(node: React.ReactNode): string {
|
||||
if (typeof node === "string" || typeof node === "number") {
|
||||
return String(node)
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
return node.map(getTextContent).join(" ")
|
||||
}
|
||||
|
||||
if (React.isValidElement<{ children?: React.ReactNode }>(node)) {
|
||||
return getTextContent(node.props.children)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
function slugify(value: string) {
|
||||
return (
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") || "section"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
# Accordion
|
||||
|
||||
Accordion wraps Base UI's accordion primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/accordion/accordion.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Accordion from "@/components/ui/accordion"
|
||||
|
||||
<Accordion.Root>
|
||||
<Accordion.Item />
|
||||
<Accordion.Header />
|
||||
<Accordion.Trigger />
|
||||
<Accordion.Panel />
|
||||
</Accordion.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Accordion,
|
||||
AccordionHeader,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Accordion`:
|
||||
|
||||
```txt
|
||||
Accordion
|
||||
├── AccordionTrigger
|
||||
├── AccordionItem
|
||||
├── AccordionHeader
|
||||
└── AccordionPanel
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open multiple panels
|
||||
|
||||
::example{src="components/accordion/open-multiple-panels.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Accordion API](https://base-ui.com/react/components/accordion#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Accordion API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Accordion` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Item
|
||||
|
||||
`Accordion.Item` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Header
|
||||
|
||||
`Accordion.Header` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`Accordion.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Panel
|
||||
|
||||
`Accordion.Panel` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,169 @@
|
||||
# Alert Dialog
|
||||
|
||||
Alert Dialog wraps Base UI's alert dialog primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/alert-dialog/alert-dialog.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import AlertDialog from "@/components/ui/alert-dialog"
|
||||
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Backdrop />
|
||||
<AlertDialog.Close />
|
||||
<AlertDialog.Description />
|
||||
<AlertDialog.Popup />
|
||||
<AlertDialog.Portal />
|
||||
</AlertDialog.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogClose,
|
||||
AlertDialogPopup,
|
||||
AlertDialogPortal,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `AlertDialog`:
|
||||
|
||||
```txt
|
||||
AlertDialog
|
||||
├── AlertDialogTrigger
|
||||
├── AlertDialogPortal
|
||||
│ ├── AlertDialogBackdrop
|
||||
│ └── AlertDialogPopup
|
||||
├── AlertDialogClose
|
||||
├── AlertDialogDescription
|
||||
├── AlertDialogTitle
|
||||
└── AlertDialogViewport
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Close confirmation
|
||||
|
||||
::example{src="components/alert-dialog/close-confirmation.tsx"}
|
||||
|
||||
### Detached triggers
|
||||
|
||||
::example{src="components/alert-dialog/detached-triggers.tsx"}
|
||||
|
||||
### Controlled mode with multiple triggers
|
||||
|
||||
::example{src="components/alert-dialog/controlled-mode-with-multiple-triggers.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Alert Dialog API](https://base-ui.com/react/components/alert-dialog#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Alert Dialog API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`AlertDialog` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Backdrop
|
||||
|
||||
`AlertDialog.Backdrop` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Close
|
||||
|
||||
`AlertDialog.Close` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Description
|
||||
|
||||
`AlertDialog.Description` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Popup
|
||||
|
||||
`AlertDialog.Popup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Portal
|
||||
|
||||
`AlertDialog.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Title
|
||||
|
||||
`AlertDialog.Title` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`AlertDialog.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Viewport
|
||||
|
||||
`AlertDialog.Viewport` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,385 @@
|
||||
# Autocomplete
|
||||
|
||||
Autocomplete wraps Base UI's autocomplete primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/autocomplete/autocomplete.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Autocomplete from "@/components/ui/autocomplete"
|
||||
|
||||
<Autocomplete.Root items={tags}>
|
||||
<Autocomplete.Input placeholder="e.g. feature" />
|
||||
<Autocomplete.Portal>
|
||||
<Autocomplete.Positioner sideOffset={4}>
|
||||
<Autocomplete.Popup>
|
||||
<Autocomplete.Empty>No tags found.</Autocomplete.Empty>
|
||||
<Autocomplete.List>
|
||||
{(tag) => <Autocomplete.Item value={tag}>{tag.value}</Autocomplete.Item>}
|
||||
</Autocomplete.List>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteInput,
|
||||
AutocompleteItem,
|
||||
AutocompleteList,
|
||||
AutocompletePopup,
|
||||
} from "@/components/ui/autocomplete"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Autocomplete`:
|
||||
|
||||
```txt
|
||||
Autocomplete
|
||||
├── AutocompleteTrigger
|
||||
├── AutocompleteInput
|
||||
├── AutocompleteGroup
|
||||
├── AutocompletePortal
|
||||
│ ├── AutocompleteBackdrop
|
||||
│ └── AutocompletePositioner
|
||||
│ └── AutocompletePopup
|
||||
│ ├── AutocompleteArrow
|
||||
│ ├── AutocompleteList
|
||||
│ ├── AutocompleteCollection
|
||||
│ ├── AutocompleteGroup
|
||||
│ ├── AutocompleteItem
|
||||
│ └── AutocompleteEmpty
|
||||
├── AutocompleteValue
|
||||
├── AutocompleteInputGroup
|
||||
├── AutocompleteIcon
|
||||
├── AutocompleteClear
|
||||
├── AutocompleteGroupLabel
|
||||
├── AutocompleteRow
|
||||
├── Autocomplete.Separator
|
||||
└── AutocompleteStatus
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Autocomplete.
|
||||
|
||||
### Async search
|
||||
|
||||
Load items asynchronously while typing and render custom status content.
|
||||
|
||||
### Inline autocomplete
|
||||
|
||||
Autofill the input with the highlighted item while navigating with arrow keys using the `mode` prop. Accepts `aria-autocomplete` values `list`, `both`, `inline`, or `none`.
|
||||
|
||||
### Grouped
|
||||
|
||||
Organize related options with `<Autocomplete.Group>` and `<Autocomplete.GroupLabel>` to add section headings inside the popup.
|
||||
|
||||
```tsx
|
||||
interface ProduceGroupItem {
|
||||
value: string
|
||||
|
||||
items: string[]
|
||||
}
|
||||
|
||||
const groups: ProduceGroupItem[] = [
|
||||
{
|
||||
value: "Fruits",
|
||||
|
||||
items: ["Apple", "Banana", "Orange"],
|
||||
},
|
||||
{
|
||||
value: "Vegetables",
|
||||
|
||||
items: ["Carrot", "Lettuce", "Spinach"],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Fuzzy matching
|
||||
|
||||
Use fuzzy matching to find relevant results even when the query doesn't exactly match the item text.
|
||||
|
||||
|
||||
### Async search
|
||||
|
||||
::example{src="components/autocomplete/async-search.tsx"}
|
||||
|
||||
### Inline autocomplete
|
||||
|
||||
::example{src="components/autocomplete/inline-autocomplete.tsx"}
|
||||
|
||||
### Grouped
|
||||
|
||||
::example{src="components/autocomplete/grouped.tsx"}
|
||||
|
||||
### Fuzzy matching
|
||||
|
||||
::example{src="components/autocomplete/fuzzy-matching.tsx"}
|
||||
|
||||
### Limit results
|
||||
|
||||
::example{src="components/autocomplete/limit-results.tsx"}
|
||||
|
||||
### Auto highlight
|
||||
|
||||
::example{src="components/autocomplete/auto-highlight.tsx"}
|
||||
|
||||
### Command palette
|
||||
|
||||
::example{src="components/autocomplete/command-palette.tsx"}
|
||||
|
||||
### Grid layout
|
||||
|
||||
::example{src="components/autocomplete/grid-layout.tsx"}
|
||||
|
||||
### Virtualized
|
||||
|
||||
::example{src="components/autocomplete/virtualized.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Autocomplete API](https://base-ui.com/react/components/autocomplete#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Autocomplete API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Autocomplete` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Value
|
||||
|
||||
`Autocomplete.Value` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`Autocomplete.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Input
|
||||
|
||||
`Autocomplete.Input` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### InputGroup
|
||||
|
||||
`Autocomplete.InputGroup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Icon
|
||||
|
||||
`Autocomplete.Icon` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Clear
|
||||
|
||||
`Autocomplete.Clear` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### List
|
||||
|
||||
`Autocomplete.List` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Portal
|
||||
|
||||
`Autocomplete.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Backdrop
|
||||
|
||||
`Autocomplete.Backdrop` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Positioner
|
||||
|
||||
`Autocomplete.Positioner` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `Side` | - | Preferred side for floating content. |
|
||||
| `align` | `Align` | - | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance from the anchor. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flip and shift behavior. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement state. |
|
||||
|
||||
### Popup
|
||||
|
||||
`Autocomplete.Popup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Arrow
|
||||
|
||||
`Autocomplete.Arrow` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Group
|
||||
|
||||
`Autocomplete.Group` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### GroupLabel
|
||||
|
||||
`Autocomplete.GroupLabel` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Item
|
||||
|
||||
`Autocomplete.Item` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Row
|
||||
|
||||
`Autocomplete.Row` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Collection
|
||||
|
||||
`Autocomplete.Collection` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Empty
|
||||
|
||||
`Autocomplete.Empty` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Separator
|
||||
|
||||
`Autocomplete.Separator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Status
|
||||
|
||||
`Autocomplete.Status` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,79 @@
|
||||
# Avatar
|
||||
|
||||
Avatar wraps Base UI's avatar primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/avatar/avatar.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Avatar from "@/components/ui/avatar"
|
||||
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="/avatar.jpg" width="48" height="48" />
|
||||
<Avatar.Fallback delay={600}>LT</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
|
||||
<Avatar>
|
||||
<AvatarImage src="/avatar.jpg" />
|
||||
<AvatarFallback>LT</AvatarFallback>
|
||||
</Avatar>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Avatar`:
|
||||
|
||||
```txt
|
||||
Avatar
|
||||
├── AvatarImage
|
||||
└── AvatarFallback
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Avatar API](https://base-ui.com/react/components/avatar#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Avatar API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Avatar` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Image
|
||||
|
||||
`Avatar.Image` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Fallback
|
||||
|
||||
`Avatar.Fallback` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,77 @@
|
||||
# Button
|
||||
|
||||
Button wraps Base UI's button primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/button/button.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Button from "@/components/ui/button"
|
||||
|
||||
<Button focusableWhenDisabled>Save changes</Button>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
<Button>Save changes</Button>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Button`:
|
||||
|
||||
```txt
|
||||
Button
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Button.
|
||||
|
||||
### Rendering as another tag
|
||||
|
||||
The button can remain keyboard accessible while being rendered as another tag, such as a `<div>`, by specifying `nativeButton={false}`.
|
||||
|
||||
```jsx
|
||||
import Button from "@/components/ui/button"
|
||||
|
||||
<Button render={<div />} nativeButton={false}>
|
||||
Button that can contain complex children
|
||||
</Button>;
|
||||
```
|
||||
|
||||
### Rendering links as buttons
|
||||
|
||||
The Button component enforces button semantics. `nativeButton={false}` signals that the rendered tag is not a `<button>`, but it must still be a tag that can receive button semantics (`role="button"`, keyboard interaction handlers). Links (`<a>`) have their own semantics and should not be rendered as buttons through the `render` prop.
|
||||
|
||||
### Loading states
|
||||
|
||||
For buttons that enter a loading state after being clicked, specify the `focusableWhenDisabled` prop to ensure focus remains on the button when it becomes disabled. This prevents focus from being lost and maintains the tab order.
|
||||
|
||||
|
||||
### Loading states
|
||||
|
||||
::example{src="components/button/loading-states.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Button API](https://base-ui.com/react/components/button#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Button API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Button` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
@@ -0,0 +1,133 @@
|
||||
# Checkbox Group
|
||||
|
||||
Checkbox Group wraps Base UI's checkbox group primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/checkbox-group/checkbox-group.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Checkbox from "@/components/ui/checkbox"
|
||||
import CheckboxGroup from "@/components/ui/checkbox-group"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
function ExampleCheckboxGroup() {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<CheckboxGroup aria-labelledby={id} defaultValue={["fuji-apple"]}>
|
||||
<div id={id} className="text-sm font-bold">Apples</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox.Root name="apple" value="fuji-apple">
|
||||
<Checkbox.Indicator>
|
||||
<Check className="size-4" />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
Fuji
|
||||
</label>
|
||||
</CheckboxGroup>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { CheckboxGroup } from "@/components/ui/checkbox-group"
|
||||
|
||||
<CheckboxGroup defaultValue={["fuji-apple"]} />
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `CheckboxGroup`:
|
||||
|
||||
```txt
|
||||
CheckboxGroup
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Checkbox Group.
|
||||
|
||||
### Labeling a checkbox group
|
||||
|
||||
Label the group with `aria-labelledby` and a sibling label element:
|
||||
|
||||
```tsx
|
||||
<div id="protocols-label">Allowed network protocols</div>
|
||||
<CheckboxGroup aria-labelledby="protocols-label">{/* ... */}</CheckboxGroup>
|
||||
```
|
||||
|
||||
### Rendering as a native button
|
||||
|
||||
By default, `<Checkbox.Root>` renders a `<span>` element to support enclosing labels. Prefer rendering each checkbox as a native button when using sibling labels (`htmlFor`/`id`).
|
||||
|
||||
```tsx
|
||||
<div id="protocols-label">Allowed network protocols</div>
|
||||
<CheckboxGroup aria-labelledby="protocols-label">
|
||||
<div>
|
||||
<label htmlFor="protocol-http">HTTP</label>
|
||||
|
||||
<Checkbox.Root id="protocol-http" value="http" nativeButton render={<button />}>
|
||||
<Checkbox.Indicator />
|
||||
</Checkbox.Root>
|
||||
</div>
|
||||
</CheckboxGroup>
|
||||
```
|
||||
|
||||
### Form integration
|
||||
|
||||
Use Field and Fieldset for group labeling and form integration:
|
||||
|
||||
```tsx
|
||||
<Form>
|
||||
|
||||
<Field.Root name="allowedNetworkProtocols">
|
||||
<Fieldset.Root render={<CheckboxGroup />}>
|
||||
<Fieldset.Legend>Allowed network protocols</Fieldset.Legend>
|
||||
<Field.Item>
|
||||
<Field.Label>
|
||||
<Checkbox.Root value="http" />
|
||||
HTTP
|
||||
</Field.Label>
|
||||
</Field.Item>
|
||||
<Field.Item>
|
||||
<Field.Label>
|
||||
<Checkbox.Root value="https" />
|
||||
HTTPS
|
||||
</Field.Label>
|
||||
</Field.Item>
|
||||
<Field.Item>
|
||||
<Field.Label>
|
||||
<Checkbox.Root value="ssh" />
|
||||
SSH
|
||||
</Field.Label>
|
||||
</Field.Item>
|
||||
</Fieldset.Root>
|
||||
</Field.Root>
|
||||
</Form>
|
||||
```
|
||||
|
||||
### Parent checkbox
|
||||
|
||||
A checkbox that controls other checkboxes within a `<CheckboxGroup>` can be created:
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Checkbox Group API](https://base-ui.com/react/components/checkbox-group#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Checkbox Group API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`CheckboxGroup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
@@ -0,0 +1,74 @@
|
||||
# Checkbox
|
||||
|
||||
Checkbox wraps Base UI's checkbox primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/checkbox/checkbox.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Checkbox from "@/components/ui/checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox.Root defaultChecked>
|
||||
<Checkbox.Indicator>
|
||||
<Check className="size-4" />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
Enable notifications
|
||||
</label>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Checkbox, CheckboxIndicator } from "@/components/ui/checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
<Checkbox defaultChecked>
|
||||
<CheckboxIndicator>
|
||||
<Check className="size-4" />
|
||||
</CheckboxIndicator>
|
||||
</Checkbox>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Checkbox`:
|
||||
|
||||
```txt
|
||||
Checkbox
|
||||
└── CheckboxIndicator
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Checkbox API](https://base-ui.com/react/components/checkbox#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Checkbox API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Checkbox` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Indicator
|
||||
|
||||
`Checkbox.Indicator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,81 @@
|
||||
# Collapsible
|
||||
|
||||
Collapsible wraps Base UI's collapsible primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/collapsible/collapsible.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Collapsible from "@/components/ui/collapsible"
|
||||
|
||||
<Collapsible.Root>
|
||||
<Collapsible.Trigger>Recovery keys</Collapsible.Trigger>
|
||||
<Collapsible.Panel>
|
||||
<div>alien-bean-pasta</div>
|
||||
</Collapsible.Panel>
|
||||
</Collapsible.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsiblePanel,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Collapsible`:
|
||||
|
||||
```txt
|
||||
Collapsible
|
||||
├── CollapsibleTrigger
|
||||
└── CollapsiblePanel
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Collapsible API](https://base-ui.com/react/components/collapsible#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Collapsible API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Collapsible` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`Collapsible.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Panel
|
||||
|
||||
`Collapsible.Panel` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,401 @@
|
||||
# Combobox
|
||||
|
||||
Combobox wraps Base UI's combobox primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/combobox/combobox.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Combobox from "@/components/ui/combobox"
|
||||
|
||||
<Combobox.Root items={fruits}>
|
||||
<Combobox.InputGroup>
|
||||
<Combobox.Input placeholder="e.g. Apple" />
|
||||
<Combobox.Clear />
|
||||
<Combobox.Trigger />
|
||||
</Combobox.InputGroup>
|
||||
<Combobox.Portal>
|
||||
<Combobox.Positioner sideOffset={4}>
|
||||
<Combobox.Popup>
|
||||
<Combobox.List>
|
||||
{(item) => <Combobox.Item value={item}>{item.label}</Combobox.Item>}
|
||||
</Combobox.List>
|
||||
</Combobox.Popup>
|
||||
</Combobox.Positioner>
|
||||
</Combobox.Portal>
|
||||
</Combobox.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxInputGroup,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxPopup,
|
||||
} from "@/components/ui/combobox"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Combobox`:
|
||||
|
||||
```txt
|
||||
Combobox
|
||||
├── ComboboxTrigger
|
||||
├── ComboboxLabel
|
||||
├── ComboboxInput
|
||||
├── ComboboxGroup
|
||||
├── ComboboxPortal
|
||||
│ ├── ComboboxBackdrop
|
||||
│ └── ComboboxPositioner
|
||||
│ └── ComboboxPopup
|
||||
│ ├── ComboboxArrow
|
||||
│ ├── ComboboxList
|
||||
│ ├── ComboboxCollection
|
||||
│ ├── ComboboxGroup
|
||||
│ ├── ComboboxItem
|
||||
│ └── ComboboxEmpty
|
||||
├── ComboboxValue
|
||||
├── ComboboxInputGroup
|
||||
├── ComboboxIcon
|
||||
├── ComboboxGroupLabel
|
||||
├── ComboboxItemIndicator
|
||||
├── ComboboxChips
|
||||
├── ComboboxChip
|
||||
├── ComboboxChipRemove
|
||||
├── ComboboxRow
|
||||
├── ComboboxClear
|
||||
├── Combobox.Separator
|
||||
└── ComboboxStatus
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Multiple select
|
||||
|
||||
::example{src="components/combobox/multiple-select.tsx"}
|
||||
|
||||
### Input inside popup
|
||||
|
||||
::example{src="components/combobox/input-inside-popup.tsx"}
|
||||
|
||||
### Grouped
|
||||
|
||||
::example{src="components/combobox/grouped.tsx"}
|
||||
|
||||
### Async search (single)
|
||||
|
||||
::example{src="components/combobox/async-search-single.tsx"}
|
||||
|
||||
### Async search (multiple)
|
||||
|
||||
::example{src="components/combobox/async-search-multiple.tsx"}
|
||||
|
||||
### Creatable
|
||||
|
||||
::example{src="components/combobox/creatable.tsx"}
|
||||
|
||||
### Virtualized
|
||||
|
||||
::example{src="components/combobox/virtualized.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Combobox API](https://base-ui.com/react/components/combobox#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Combobox API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Combobox` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Label
|
||||
|
||||
`Combobox.Label` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Value
|
||||
|
||||
`Combobox.Value` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Input
|
||||
|
||||
`Combobox.Input` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### InputGroup
|
||||
|
||||
`Combobox.InputGroup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`Combobox.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### List
|
||||
|
||||
`Combobox.List` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Portal
|
||||
|
||||
`Combobox.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Backdrop
|
||||
|
||||
`Combobox.Backdrop` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Positioner
|
||||
|
||||
`Combobox.Positioner` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `Side` | - | Preferred side for floating content. |
|
||||
| `align` | `Align` | - | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance from the anchor. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flip and shift behavior. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement state. |
|
||||
|
||||
### Popup
|
||||
|
||||
`Combobox.Popup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Arrow
|
||||
|
||||
`Combobox.Arrow` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Icon
|
||||
|
||||
`Combobox.Icon` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Group
|
||||
|
||||
`Combobox.Group` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### GroupLabel
|
||||
|
||||
`Combobox.GroupLabel` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Item
|
||||
|
||||
`Combobox.Item` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### ItemIndicator
|
||||
|
||||
`Combobox.ItemIndicator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Chips
|
||||
|
||||
`Combobox.Chips` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Chip
|
||||
|
||||
`Combobox.Chip` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### ChipRemove
|
||||
|
||||
`Combobox.ChipRemove` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Row
|
||||
|
||||
`Combobox.Row` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Collection
|
||||
|
||||
`Combobox.Collection` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Empty
|
||||
|
||||
`Combobox.Empty` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Clear
|
||||
|
||||
`Combobox.Clear` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Separator
|
||||
|
||||
`Combobox.Separator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Status
|
||||
|
||||
`Combobox.Status` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,229 @@
|
||||
# Context Menu
|
||||
|
||||
A menu that appears at the pointer on right click or long press.
|
||||
|
||||
Context Menu wraps Base UI's context menu primitive with Tailwind classes from the official Base UI example. Most menu composition patterns also apply here, but the trigger opens from context-menu gestures instead of a normal press.
|
||||
|
||||
::example{src="components/context-menu/context-menu.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import ContextMenu from "@/components/ui/context-menu"
|
||||
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>Right click here</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Positioner>
|
||||
<ContextMenu.Popup>
|
||||
<ContextMenu.Item>Add to Library</ContextMenu.Item>
|
||||
<ContextMenu.Item>Add to Playlist</ContextMenu.Item>
|
||||
</ContextMenu.Popup>
|
||||
</ContextMenu.Positioner>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuItem,
|
||||
ContextMenuPopup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuPositioner,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu"
|
||||
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>Right click here</ContextMenuTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuPositioner>
|
||||
<ContextMenuPopup>
|
||||
<ContextMenuItem>Add to Library</ContextMenuItem>
|
||||
<ContextMenuItem>Add to Playlist</ContextMenuItem>
|
||||
</ContextMenuPopup>
|
||||
</ContextMenuPositioner>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenu>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `ContextMenu`:
|
||||
|
||||
```txt
|
||||
ContextMenu
|
||||
├── ContextMenuTrigger
|
||||
└── ContextMenuPortal
|
||||
├── ContextMenuBackdrop
|
||||
└── ContextMenuPositioner
|
||||
└── ContextMenuPopup
|
||||
├── ContextMenuArrow
|
||||
├── ContextMenuItem
|
||||
├── ContextMenuLinkItem
|
||||
├── ContextMenu.Separator
|
||||
├── ContextMenuGroup
|
||||
│ └── ContextMenuGroupLabel
|
||||
├── ContextMenuRadioGroup
|
||||
│ └── ContextMenuRadioItem
|
||||
│ └── ContextMenuRadioItemIndicator
|
||||
├── ContextMenuCheckboxItem
|
||||
│ └── ContextMenuCheckboxItemIndicator
|
||||
└── ContextMenuSubmenuRoot
|
||||
└── ContextMenuSubmenuTrigger
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
The official Context Menu page includes the base context menu and a nested menu example. Base UI's Menu page documents additional item patterns such as checkbox items, radio items, group labels, link items, and arrows; those same parts are exposed here for context menus.
|
||||
|
||||
### Nested menu
|
||||
|
||||
Create a submenu with `<ContextMenu.SubmenuRoot>` inside the parent context menu. Use `<ContextMenu.SubmenuTrigger>` for the item that opens the nested menu.
|
||||
|
||||
::example{src="components/context-menu/nested-menu.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Context Menu API](https://base-ui.com/react/components/context-menu#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Context Menu API. Props, controlled state, event details, render props, and data attributes follow the corresponding Base UI component documentation. Styled parts forward `className` and can be replaced with the `render` prop where Base UI supports it.
|
||||
|
||||
### Root
|
||||
|
||||
Owns the open state for a context menu.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultOpen` | `boolean` | `false` | Initial uncontrolled open state. |
|
||||
| `open` | `boolean` | - | Controlled open state. |
|
||||
| `onOpenChange` | `(open, eventDetails) => void` | - | Called when the context menu requests to open or close. |
|
||||
| `modal` | `boolean` | `true` | Whether interactions outside the menu are blocked while open. |
|
||||
| `children` | `React.ReactNode` | - | Trigger, portal, and popup parts. |
|
||||
|
||||
### Trigger
|
||||
|
||||
Defines the area that opens the menu on right click, context-menu key, or long press.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the trigger element. |
|
||||
| `children` | `React.ReactNode` | - | Content that receives the context-menu gesture. |
|
||||
|
||||
### Portal
|
||||
|
||||
Moves floating content to the document body by default.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives the portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps portal children mounted while closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating context-menu content. |
|
||||
|
||||
Use the Base UI `CSPProvider` at the application root when portal content needs consistent clipping, z-index, and stacking behavior.
|
||||
|
||||
### Backdrop
|
||||
|
||||
Renders the dismissable layer behind the context menu when modal behavior is active.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the backdrop state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
|
||||
### Positioner
|
||||
|
||||
Positions the popup at the pointer location.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `"top" \| "right" \| "bottom" \| "left" \| "inline-start" \| "inline-end"` | `"bottom"` | Preferred side for the popup. |
|
||||
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance between pointer anchor and popup. |
|
||||
| `alignOffset` | `number \| OffsetFunction` | `0` | Offset along the alignment axis. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flipping and shifting when space is limited. |
|
||||
| `collisionBoundary` | `Boundary` | `"clipping-ancestors"` | Boundary used for collision detection. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement and open state. |
|
||||
|
||||
### Popup
|
||||
|
||||
Contains the context-menu surface.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for open, closing, side, and alignment state. |
|
||||
| `style` | `CSSProperties \| (state) => CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the popup element. |
|
||||
| `children` | `React.ReactNode` | - | Context-menu items, groups, submenus, and arrows. |
|
||||
|
||||
### Item and LinkItem
|
||||
|
||||
`ContextMenu.Item` renders an action item. `ContextMenu.LinkItem` renders a navigational item.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction. |
|
||||
| `closeOnClick` | `boolean` | `true` | Controls whether selecting the item closes the menu. |
|
||||
| `nativeButton` | `boolean` | `false` | Treats a rendered native button as the semantic control. |
|
||||
| `label` | `string` | - | Text used for keyboard text navigation. |
|
||||
| `href` | `string` | - | Link target for `ContextMenu.LinkItem`. |
|
||||
| `onClick` | `(event) => void` | - | Called when the item is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for highlighted and disabled state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the item element. |
|
||||
|
||||
Attributes include `data-highlighted` and `data-disabled`.
|
||||
|
||||
### CheckboxItem and RadioItem
|
||||
|
||||
Use `ContextMenu.CheckboxItem` for independent boolean options and `ContextMenu.RadioItem` inside `ContextMenu.RadioGroup` for mutually exclusive choices.
|
||||
|
||||
| Part | Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ContextMenu.CheckboxItem` | `checked` | `boolean` | - | Controlled checked state. |
|
||||
| `ContextMenu.CheckboxItem` | `defaultChecked` | `boolean` | `false` | Initial uncontrolled checked state. |
|
||||
| `ContextMenu.CheckboxItem` | `onCheckedChange` | `(checked, eventDetails) => void` | - | Called when checked state changes. |
|
||||
| `ContextMenu.RadioGroup` | `value` | `any` | - | Controlled selected value. |
|
||||
| `ContextMenu.RadioGroup` | `defaultValue` | `any` | - | Initial uncontrolled selected value. |
|
||||
| `ContextMenu.RadioGroup` | `onValueChange` | `(value, eventDetails) => void` | - | Called when the selected value changes. |
|
||||
| `ContextMenu.RadioItem` | `value` | `any` | - | Value selected by this radio item. |
|
||||
| `ContextMenu.RadioItem` | `disabled` | `boolean` | `false` | Prevents user interaction. |
|
||||
|
||||
Indicators render inside `ContextMenu.CheckboxItemIndicator` and `ContextMenu.RadioItemIndicator`.
|
||||
|
||||
### Group and Separator
|
||||
|
||||
Use `ContextMenu.Group` and `ContextMenu.GroupLabel` to label related choices. Use `ContextMenu.Separator` to visually separate item groups.
|
||||
|
||||
| Part | Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ContextMenu.Group` | `children` | `React.ReactNode` | - | Related menu items. |
|
||||
| `ContextMenu.GroupLabel` | `children` | `React.ReactNode` | - | Accessible label for the group. |
|
||||
| `ContextMenu.Separator` | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Visual separator orientation. |
|
||||
| `ContextMenu.Separator` | `className` | `string` | - | Classes for the separator element. |
|
||||
|
||||
### SubmenuRoot and SubmenuTrigger
|
||||
|
||||
Create nested context menus with `ContextMenu.SubmenuRoot` and open them with `ContextMenu.SubmenuTrigger`.
|
||||
|
||||
| Part | Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ContextMenu.SubmenuRoot` | `defaultOpen` | `boolean` | `false` | Initial uncontrolled submenu open state. |
|
||||
| `ContextMenu.SubmenuRoot` | `open` | `boolean` | - | Controlled submenu open state. |
|
||||
| `ContextMenu.SubmenuRoot` | `onOpenChange` | `(open, eventDetails) => void` | - | Called when the submenu requests to open or close. |
|
||||
| `ContextMenu.SubmenuTrigger` | `disabled` | `boolean` | `false` | Prevents opening the submenu. |
|
||||
| `ContextMenu.SubmenuTrigger` | `className` | `string \| (state) => string` | - | Classes for highlighted, open, and disabled state. |
|
||||
|
||||
### Arrow
|
||||
|
||||
Visually connects the popup to the pointer location.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for side and alignment state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the arrow element. |
|
||||
@@ -0,0 +1,270 @@
|
||||
# Dialog
|
||||
|
||||
Dialog is split into styled parts that mirror Base UI anatomy. The named `DialogContent` component composes `Portal`, `Backdrop`, and `Popup`.
|
||||
|
||||
::example{src="components/dialog/dialog.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Dialog from "@/components/ui/dialog"
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>Open</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Popup>
|
||||
<Dialog.Title>Notifications</Dialog.Title>
|
||||
<Dialog.Description>You are all caught up. Good job!</Dialog.Description>
|
||||
<Dialog.Close>Close</Dialog.Close>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger>Open</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>Notifications</DialogTitle>
|
||||
<DialogDescription>You are all caught up. Good job!</DialogDescription>
|
||||
<DialogClose>Close</DialogClose>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Dialog`:
|
||||
|
||||
```txt
|
||||
Dialog
|
||||
├── DialogTrigger
|
||||
├── DialogPortal
|
||||
│ ├── DialogBackdrop
|
||||
│ └── DialogPopup
|
||||
├── DialogClose
|
||||
├── DialogDescription
|
||||
├── DialogViewport
|
||||
└── DialogTitle
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Dialog.
|
||||
|
||||
### State
|
||||
|
||||
By default, Dialog is an uncontrolled component that manages its own state.
|
||||
|
||||
```tsx
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>Open</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Popup>
|
||||
<Dialog.Title>Example dialog</Dialog.Title>
|
||||
<Dialog.Close>Close</Dialog.Close>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
```
|
||||
|
||||
### Open from a menu
|
||||
|
||||
In order to open a dialog using a menu, control the dialog state and open it imperatively using the `onClick` handler on the menu item.
|
||||
|
||||
```tsx
|
||||
import * as React from "react"
|
||||
import Dialog from "@/components/ui/dialog"
|
||||
import Menu from "@/components/ui/menu"
|
||||
|
||||
function ExampleMenu() {
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Menu.Root>
|
||||
<Menu.Trigger>Open menu</Menu.Trigger>
|
||||
<Menu.Portal>
|
||||
<Menu.Positioner>
|
||||
<Menu.Popup>
|
||||
|
||||
{/* Open the dialog when the menu item is clicked */}
|
||||
<Menu.Item onClick={() => setDialogOpen(true)}>Open dialog</Menu.Item>
|
||||
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
</Menu.Root>
|
||||
|
||||
{/* Control the dialog state */}
|
||||
<Dialog.Root open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Popup>
|
||||
{/* prettier-ignore */}
|
||||
{/* Rest of the dialog */}
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Nested dialogs
|
||||
|
||||
You can nest dialogs within one another normally.
|
||||
|
||||
### Close confirmation
|
||||
|
||||
This example shows a nested confirmation dialog that opens if the text entered in the parent dialog is going to be discarded.
|
||||
|
||||
|
||||
### Nested dialogs
|
||||
|
||||
::example{src="components/dialog/nested-dialogs.tsx"}
|
||||
|
||||
### Close confirmation
|
||||
|
||||
::example{src="components/dialog/close-confirmation.tsx"}
|
||||
|
||||
### Outside scroll dialog
|
||||
|
||||
::example{src="components/dialog/outside-scroll-dialog.tsx"}
|
||||
|
||||
### Inside scroll dialog
|
||||
|
||||
::example{src="components/dialog/inside-scroll-dialog.tsx"}
|
||||
|
||||
### Placing elements outside the popup
|
||||
|
||||
::example{src="components/dialog/placing-elements-outside-the-popup.tsx"}
|
||||
|
||||
### Detached triggers
|
||||
|
||||
::example{src="components/dialog/detached-triggers.tsx"}
|
||||
|
||||
### Controlled mode with multiple triggers
|
||||
|
||||
::example{src="components/dialog/controlled-mode-with-multiple-triggers.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Dialog API](https://base-ui.com/react/components/dialog#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Dialog API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Backdrop
|
||||
|
||||
`Dialog.Backdrop` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Close
|
||||
|
||||
`Dialog.Close` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Description
|
||||
|
||||
`Dialog.Description` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Popup
|
||||
|
||||
`Dialog.Popup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Portal
|
||||
|
||||
`Dialog.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Root
|
||||
|
||||
`Dialog` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Viewport
|
||||
|
||||
`Dialog.Viewport` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Title
|
||||
|
||||
`Dialog.Title` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`Dialog.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
@@ -0,0 +1,277 @@
|
||||
# Drawer
|
||||
|
||||
Drawer wraps Base UI's drawer primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/drawer/drawer.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Drawer from "@/components/ui/drawer"
|
||||
|
||||
<Drawer.Root swipeDirection="right">
|
||||
<Drawer.Trigger>Open drawer</Drawer.Trigger>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Backdrop />
|
||||
<Drawer.Viewport>
|
||||
<Drawer.Popup>
|
||||
<Drawer.Content>
|
||||
<Drawer.Title>Drawer</Drawer.Title>
|
||||
<Drawer.Description>This is a drawer.</Drawer.Description>
|
||||
<Drawer.Close>Close</Drawer.Close>
|
||||
</Drawer.Content>
|
||||
</Drawer.Popup>
|
||||
</Drawer.Viewport>
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerPopup,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Drawer`:
|
||||
|
||||
```txt
|
||||
Drawer
|
||||
├── DrawerTrigger
|
||||
├── DrawerPortal
|
||||
│ ├── DrawerBackdrop
|
||||
│ └── DrawerPopup
|
||||
├── DrawerClose
|
||||
├── DrawerContent
|
||||
├── DrawerDescription
|
||||
├── DrawerIndent
|
||||
├── DrawerIndentBackground
|
||||
├── DrawerProvider
|
||||
├── DrawerSwipeArea
|
||||
├── DrawerTitle
|
||||
├── DrawerViewport
|
||||
└── DrawerVirtualKeyboardProvider
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Position
|
||||
|
||||
::example{src="components/drawer/position.tsx"}
|
||||
|
||||
### Nested drawers
|
||||
|
||||
::example{src="components/drawer/nested-drawers.tsx"}
|
||||
|
||||
### Snap points
|
||||
|
||||
::example{src="components/drawer/snap-points.tsx"}
|
||||
|
||||
### Virtual keyboard aware
|
||||
|
||||
::example{src="components/drawer/virtual-keyboard-aware.tsx"}
|
||||
|
||||
### Indent effect
|
||||
|
||||
::example{src="components/drawer/indent-effect.tsx"}
|
||||
|
||||
### Non-modal
|
||||
|
||||
::example{src="components/drawer/non-modal.tsx"}
|
||||
|
||||
### Mobile navigation
|
||||
|
||||
::example{src="components/drawer/mobile-navigation.tsx"}
|
||||
|
||||
### Swipe to open
|
||||
|
||||
::example{src="components/drawer/swipe-to-open.tsx"}
|
||||
|
||||
### Close confirmation
|
||||
|
||||
::example{src="components/drawer/close-confirmation.tsx"}
|
||||
|
||||
### Action sheet with separate destructive action
|
||||
|
||||
::example{src="components/drawer/action-sheet-with-separate-destructive-action.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Drawer API](https://base-ui.com/react/components/drawer#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Drawer API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Backdrop
|
||||
|
||||
`Drawer.Backdrop` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Close
|
||||
|
||||
`Drawer.Close` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Content
|
||||
|
||||
`Drawer.Content` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Description
|
||||
|
||||
`Drawer.Description` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Indent
|
||||
|
||||
`Drawer.Indent` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### IndentBackground
|
||||
|
||||
`Drawer.IndentBackground` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Popup
|
||||
|
||||
`Drawer.Popup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Portal
|
||||
|
||||
`Drawer.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Provider
|
||||
|
||||
`Drawer.Provider` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Root
|
||||
|
||||
`Drawer` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### SwipeArea
|
||||
|
||||
`Drawer.SwipeArea` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Title
|
||||
|
||||
`Drawer.Title` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`Drawer.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Viewport
|
||||
|
||||
`Drawer.Viewport` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### VirtualKeyboardProvider
|
||||
|
||||
`Drawer.VirtualKeyboardProvider` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,131 @@
|
||||
# Field
|
||||
|
||||
Field wraps Base UI's field primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/field/field.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Field from "@/components/ui/field"
|
||||
|
||||
<Field.Root>
|
||||
<Field.Label>Name</Field.Label>
|
||||
<Field.Control required placeholder="Required" />
|
||||
<Field.Error match="valueMissing">Please enter your name</Field.Error>
|
||||
<Field.Description>Visible on your profile</Field.Description>
|
||||
</Field.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Field,
|
||||
FieldControl,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
} from "@/components/ui/field"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Field`:
|
||||
|
||||
```txt
|
||||
Field
|
||||
├── FieldLabel
|
||||
├── FieldControl
|
||||
├── FieldError
|
||||
├── FieldDescription
|
||||
├── FieldValidity
|
||||
└── FieldItem
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Field API](https://base-ui.com/react/components/field#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Field API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Field` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Label
|
||||
|
||||
`Field.Label` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Error
|
||||
|
||||
`Field.Error` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Description
|
||||
|
||||
`Field.Description` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Control
|
||||
|
||||
`Field.Control` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Validity
|
||||
|
||||
`Field.Validity` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Item
|
||||
|
||||
`Field.Item` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
@@ -0,0 +1,66 @@
|
||||
# Fieldset
|
||||
|
||||
Fieldset wraps Base UI's fieldset primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/fieldset/fieldset.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Field from "@/components/ui/field"
|
||||
import Fieldset from "@/components/ui/fieldset"
|
||||
|
||||
<Fieldset.Root>
|
||||
<Fieldset.Legend>Billing details</Fieldset.Legend>
|
||||
<Field.Root>
|
||||
<Field.Label>Company</Field.Label>
|
||||
<Field.Control />
|
||||
</Field.Root>
|
||||
</Fieldset.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Fieldset, FieldsetLegend } from "@/components/ui/fieldset"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Fieldset`:
|
||||
|
||||
```txt
|
||||
Fieldset
|
||||
└── FieldsetLegend
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Fieldset API](https://base-ui.com/react/components/fieldset#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Fieldset API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Fieldset` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Legend
|
||||
|
||||
`Fieldset.Legend` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,95 @@
|
||||
# Form
|
||||
|
||||
Form wraps Base UI's form primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/form/form.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Field from "@/components/ui/field"
|
||||
import Form from "@/components/ui/form"
|
||||
|
||||
<Form errors={errors} onSubmit={handleSubmit}>
|
||||
<Field.Root name="url">
|
||||
<Field.Label>Homepage</Field.Label>
|
||||
<Field.Control type="url" required />
|
||||
<Field.Error />
|
||||
</Field.Root>
|
||||
</Form>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Form } from "@/components/ui/form"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Form`:
|
||||
|
||||
```txt
|
||||
Form
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Form.
|
||||
|
||||
### Submit with a Server Function
|
||||
|
||||
Forms using `useActionState` can be submitted with a Server Function instead of `onSubmit`.
|
||||
|
||||
### Submit form values as a JavaScript object
|
||||
|
||||
You can use `onFormSubmit` instead of the native `onSubmit` to access form values as a JavaScript object. This is useful when you need to transform the values before submission, or integrate with 3rd party APIs.
|
||||
|
||||
```tsx
|
||||
<Form
|
||||
onFormSubmit={async (formValues: { id: string; quantity: number }) => {
|
||||
const payload = {
|
||||
product_id: formValues.id,
|
||||
order_quantity: formValues.quantity,
|
||||
}
|
||||
|
||||
const response = await fetch("https://api.example.com", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Using with Zod
|
||||
|
||||
When parsing the schema using `schema.safeParse()`, the `z.flattenError(result.error).fieldErrors` data can be used to map the errors to each field's `name`.
|
||||
|
||||
|
||||
### Submit with a Server Function
|
||||
|
||||
::example{src="components/form/submit-with-a-server-function.tsx"}
|
||||
|
||||
### Using with Zod
|
||||
|
||||
::example{src="components/form/using-with-zod.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Form API](https://base-ui.com/react/components/form#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Form API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Form` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
@@ -0,0 +1,52 @@
|
||||
# Input
|
||||
|
||||
Input wraps Base UI's input primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/input/input.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Input from "@/components/ui/input"
|
||||
|
||||
<label className="flex flex-col items-start gap-1 text-sm font-bold">
|
||||
Name
|
||||
<Input placeholder="e.g. Colm Tuite" />
|
||||
</label>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
<Input placeholder="e.g. Colm Tuite" />
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Input`:
|
||||
|
||||
```txt
|
||||
Input
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Input API](https://base-ui.com/react/components/input#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Input API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Input` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
@@ -0,0 +1,320 @@
|
||||
# Menu
|
||||
|
||||
A dropdown list of actions with keyboard navigation, submenu support, checkbox and radio items, detached triggers, and optional arrows.
|
||||
|
||||
Menu wraps Base UI's menu primitive with Tailwind classes from the official Base UI examples. The wrapper styles the common parts while preserving the Base UI part model.
|
||||
|
||||
::example{src="components/menu/menu.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Menu from "@/components/ui/menu"
|
||||
|
||||
<Menu.Root>
|
||||
<Menu.Trigger>Song</Menu.Trigger>
|
||||
<Menu.Portal>
|
||||
<Menu.Positioner sideOffset={8}>
|
||||
<Menu.Popup>
|
||||
<Menu.Item>Add to Library</Menu.Item>
|
||||
<Menu.Item>Add to Playlist</Menu.Item>
|
||||
<Menu.Separator className="my-1 h-px bg-neutral-950 dark:bg-white" />
|
||||
<Menu.Item>Play Next</Menu.Item>
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
</Menu.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuPopup,
|
||||
MenuPortal,
|
||||
MenuPositioner,
|
||||
MenuTrigger,
|
||||
} from "@/components/ui/menu"
|
||||
|
||||
<Menu>
|
||||
<MenuTrigger>Song</MenuTrigger>
|
||||
<MenuPortal>
|
||||
<MenuPositioner sideOffset={8}>
|
||||
<MenuPopup>
|
||||
<MenuItem>Add to Library</MenuItem>
|
||||
<MenuItem>Add to Playlist</MenuItem>
|
||||
</MenuPopup>
|
||||
</MenuPositioner>
|
||||
</MenuPortal>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `Menu`:
|
||||
|
||||
```txt
|
||||
Menu
|
||||
├── MenuTrigger
|
||||
└── MenuPortal
|
||||
├── MenuBackdrop
|
||||
└── MenuPositioner
|
||||
└── MenuPopup
|
||||
├── MenuArrow
|
||||
├── MenuItem
|
||||
├── MenuLinkItem
|
||||
├── Menu.Separator
|
||||
├── MenuGroup
|
||||
│ └── MenuGroupLabel
|
||||
├── MenuRadioGroup
|
||||
│ ├── MenuGroupLabel
|
||||
│ └── MenuRadioItem
|
||||
│ └── MenuRadioItemIndicator
|
||||
├── MenuCheckboxItem
|
||||
│ └── MenuCheckboxItemIndicator
|
||||
├── MenuSubmenuRoot
|
||||
│ └── MenuSubmenuTrigger
|
||||
└── MenuViewport
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Menu.
|
||||
|
||||
### Open on hover
|
||||
|
||||
Add `openOnHover` to `<Menu.Trigger>` to open the menu from pointer hover. Use the `delay` prop to tune how quickly it opens.
|
||||
|
||||
::example{src="components/menu/open-on-hover.tsx"}
|
||||
|
||||
### Checkbox items
|
||||
|
||||
Use `<Menu.CheckboxItem>` for a menu item that toggles a setting on or off. The indicator is rendered inside `<Menu.CheckboxItemIndicator>`.
|
||||
|
||||
::example{src="components/menu/checkbox-items.tsx"}
|
||||
|
||||
### Radio items
|
||||
|
||||
Use `<Menu.RadioGroup>` and `<Menu.RadioItem>` when only one option in a set can be selected.
|
||||
|
||||
::example{src="components/menu/radio-items.tsx"}
|
||||
|
||||
### Close on click
|
||||
|
||||
Use `closeOnClick` to control whether selecting an item closes the menu.
|
||||
|
||||
```tsx
|
||||
<Menu.CheckboxItem closeOnClick />
|
||||
<Menu.Item closeOnClick={false} />
|
||||
```
|
||||
|
||||
### Group labels
|
||||
|
||||
Use `<Menu.GroupLabel>` inside `<Menu.Group>` or `<Menu.RadioGroup>` to label related options.
|
||||
|
||||
::example{src="components/menu/group-labels.tsx"}
|
||||
|
||||
### Nested menu
|
||||
|
||||
Create a submenu with `<Menu.SubmenuRoot>` and use `<Menu.SubmenuTrigger>` for the parent item that opens it.
|
||||
|
||||
::example{src="components/menu/nested-menu.tsx"}
|
||||
|
||||
### Navigate to another page
|
||||
|
||||
Use `<Menu.LinkItem>` for items that navigate like links.
|
||||
|
||||
::example{src="components/menu/navigate-to-another-page.tsx"}
|
||||
|
||||
### Open a dialog
|
||||
|
||||
When a menu item needs to open a dialog, control the dialog state and open it from the menu item's `onClick` handler.
|
||||
|
||||
::example{src="components/menu/open-dialog.tsx"}
|
||||
|
||||
### Detached triggers
|
||||
|
||||
For triggers outside the menu root, create a handle with `Menu.createHandle()` and pass it to both `<Menu.Trigger>` and `<Menu.Root>`.
|
||||
|
||||
Only top-level menus can use detached triggers. Submenus keep their trigger inside `<Menu.SubmenuRoot>`.
|
||||
|
||||
::example{src="components/menu/detached-triggers.tsx"}
|
||||
|
||||
### Multiple triggers
|
||||
|
||||
Detached triggers can pass different `payload` values to one menu root, allowing one popup to render different items depending on which trigger opened it.
|
||||
|
||||
::example{src="components/menu/multiple-triggers.tsx"}
|
||||
|
||||
### Controlled mode with multiple triggers
|
||||
|
||||
Use `open`, `onOpenChange`, and `triggerId` on `<Menu.Root>` when more than one trigger can open a controlled menu.
|
||||
|
||||
::example{src="components/menu/controlled-mode-with-multiple-triggers.tsx"}
|
||||
|
||||
### Arrow
|
||||
|
||||
Render `<Menu.Arrow>` inside the popup to visually connect the menu to its trigger.
|
||||
|
||||
::example{src="components/menu/arrow.tsx"}
|
||||
|
||||
### Animating the Menu
|
||||
|
||||
When one menu is opened by multiple detached triggers, animate the positioner and popup to transition position, size, and content.
|
||||
|
||||
::example{src="components/menu/animating-the-menu.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Menu API](https://base-ui.com/react/components/menu#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Menu API. Props, controlled state, event details, render props, and data attributes follow the corresponding Base UI component documentation. Styled parts forward `className` and can be replaced with the `render` prop where Base UI supports it.
|
||||
|
||||
### Root
|
||||
|
||||
Owns the open state for a menu.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultOpen` | `boolean` | `false` | Initial uncontrolled open state. |
|
||||
| `open` | `boolean` | - | Controlled open state. |
|
||||
| `onOpenChange` | `(open, eventDetails) => void` | - | Called when the menu requests to open or close. |
|
||||
| `handle` | `Menu.Root.Handle` | - | Connects detached triggers created with `Menu.createHandle()`. |
|
||||
| `modal` | `boolean` | `true` | Whether interactions outside the menu are blocked while open. |
|
||||
| `children` | `React.ReactNode` | - | Menu trigger, portal, and popup parts. |
|
||||
|
||||
### Trigger
|
||||
|
||||
Opens the menu and provides the active anchor for positioning.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `openOnHover` | `boolean` | `false` | Opens the menu when the trigger is hovered. |
|
||||
| `delay` | `number` | - | Delay before hover-open behavior runs. |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction. |
|
||||
| `payload` | `any` | - | Value passed to the root for detached or multi-trigger menus. |
|
||||
| `handle` | `Menu.Root.Handle` | - | Connects this trigger to a detached root. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the trigger state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
|
||||
Attributes include `data-popup-open`, `data-pressed`, and `data-disabled`.
|
||||
|
||||
### Portal
|
||||
|
||||
Moves floating content to the document body by default.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives the portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps portal children mounted while closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating menu content. |
|
||||
|
||||
Use the Base UI `CSPProvider` at the application root when your page needs portal clipping and stacking behavior to be coordinated with the document layout.
|
||||
|
||||
### Backdrop
|
||||
|
||||
Renders the dismissable layer behind the menu when modal behavior is active.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the backdrop state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
|
||||
### Positioner
|
||||
|
||||
Positions the popup relative to the active trigger.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `"top" \| "right" \| "bottom" \| "left" \| "inline-start" \| "inline-end"` | `"bottom"` | Preferred side for the popup. |
|
||||
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance between trigger and popup. |
|
||||
| `alignOffset` | `number \| OffsetFunction` | `0` | Offset along the alignment axis. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flipping and shifting when space is limited. |
|
||||
| `anchor` | `Element \| VirtualElement \| RefObject \| (() => Element)` | active trigger | Custom anchor for positioning. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement and open state. |
|
||||
|
||||
### Popup
|
||||
|
||||
Contains the menu surface.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for open, closing, side, and alignment state. |
|
||||
| `style` | `CSSProperties \| (state) => CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the popup element. |
|
||||
| `children` | `React.ReactNode` | - | Menu items, groups, submenus, arrows, and viewport. |
|
||||
|
||||
### Item and LinkItem
|
||||
|
||||
`Menu.Item` renders an action item. `Menu.LinkItem` renders a navigational item and should be used when the item behaves like a link.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction. |
|
||||
| `closeOnClick` | `boolean` | `true` | Controls whether selecting the item closes the menu. |
|
||||
| `nativeButton` | `boolean` | `false` | Treats a rendered native button as the semantic control. |
|
||||
| `label` | `string` | - | Text used for keyboard text navigation. |
|
||||
| `href` | `string` | - | Link target for `Menu.LinkItem`. |
|
||||
| `onClick` | `(event) => void` | - | Called when the item is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for highlighted and disabled state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the item element. |
|
||||
|
||||
Attributes include `data-highlighted` and `data-disabled`.
|
||||
|
||||
### CheckboxItem and RadioItem
|
||||
|
||||
Use `Menu.CheckboxItem` for independent boolean options and `Menu.RadioItem` inside `Menu.RadioGroup` for mutually exclusive choices.
|
||||
|
||||
| Part | Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `Menu.CheckboxItem` | `checked` | `boolean` | - | Controlled checked state. |
|
||||
| `Menu.CheckboxItem` | `defaultChecked` | `boolean` | `false` | Initial uncontrolled checked state. |
|
||||
| `Menu.CheckboxItem` | `onCheckedChange` | `(checked, eventDetails) => void` | - | Called when checked state changes. |
|
||||
| `Menu.RadioGroup` | `value` | `any` | - | Controlled selected value. |
|
||||
| `Menu.RadioGroup` | `defaultValue` | `any` | - | Initial uncontrolled selected value. |
|
||||
| `Menu.RadioGroup` | `onValueChange` | `(value, eventDetails) => void` | - | Called when the selected value changes. |
|
||||
| `Menu.RadioItem` | `value` | `any` | - | Value selected by this radio item. |
|
||||
| `Menu.RadioItem` | `disabled` | `boolean` | `false` | Prevents user interaction. |
|
||||
|
||||
Indicators render inside `Menu.CheckboxItemIndicator` and `Menu.RadioItemIndicator`. They receive checked state attributes such as `data-checked` and `data-unchecked`.
|
||||
|
||||
### Group and Separator
|
||||
|
||||
Use `Menu.Group` and `Menu.GroupLabel` to label related choices. Use `Menu.Separator` to visually separate item groups.
|
||||
|
||||
| Part | Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `Menu.Group` | `children` | `React.ReactNode` | - | Related menu items. |
|
||||
| `Menu.GroupLabel` | `children` | `React.ReactNode` | - | Accessible label for the group. |
|
||||
| `Menu.Separator` | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Visual separator orientation. |
|
||||
| `Menu.Separator` | `className` | `string` | - | Classes for the separator element. |
|
||||
|
||||
### SubmenuRoot and SubmenuTrigger
|
||||
|
||||
Create nested menus with `Menu.SubmenuRoot` and open them with `Menu.SubmenuTrigger`.
|
||||
|
||||
| Part | Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `Menu.SubmenuRoot` | `defaultOpen` | `boolean` | `false` | Initial uncontrolled submenu open state. |
|
||||
| `Menu.SubmenuRoot` | `open` | `boolean` | - | Controlled submenu open state. |
|
||||
| `Menu.SubmenuRoot` | `onOpenChange` | `(open, eventDetails) => void` | - | Called when the submenu requests to open or close. |
|
||||
| `Menu.SubmenuTrigger` | `disabled` | `boolean` | `false` | Prevents opening the submenu. |
|
||||
| `Menu.SubmenuTrigger` | `className` | `string \| (state) => string` | - | Classes for highlighted, open, and disabled state. |
|
||||
|
||||
Submenus keep their trigger inside the submenu root and do not use detached trigger handles.
|
||||
|
||||
### Arrow and Viewport
|
||||
|
||||
`Menu.Arrow` visually connects the popup to its trigger. `Menu.Viewport` supports animated content and size changes when a menu swaps content across triggers.
|
||||
|
||||
| Part | Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `Menu.Arrow` | `className` | `string \| (state) => string` | - | Classes for side and alignment state. |
|
||||
| `Menu.Arrow` | `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the arrow element. |
|
||||
| `Menu.Viewport` | `className` | `string \| (state) => string` | - | Classes for animated size and content state. |
|
||||
| `Menu.Viewport` | `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the viewport element. |
|
||||
@@ -0,0 +1,59 @@
|
||||
# Menubar
|
||||
|
||||
Menubar wraps Base UI's menubar primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/menubar/menubar.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Menubar from "@/components/ui/menubar"
|
||||
import Menu from "@/components/ui/menu"
|
||||
|
||||
<Menubar>
|
||||
<Menu.Root>
|
||||
<Menu.Trigger>File</Menu.Trigger>
|
||||
<Menu.Portal>
|
||||
<Menu.Positioner>
|
||||
<Menu.Popup>
|
||||
<Menu.Item>New</Menu.Item>
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
</Menu.Root>
|
||||
</Menubar>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Menubar } from "@/components/ui/menubar"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Menubar`:
|
||||
|
||||
```txt
|
||||
Menubar
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Menubar API](https://base-ui.com/react/components/menubar#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Menubar API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Menubar` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
@@ -0,0 +1,107 @@
|
||||
# Meter
|
||||
|
||||
Meter wraps Base UI's meter primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/meter/meter.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Meter from "@/components/ui/meter"
|
||||
|
||||
<Meter.Root value={24}>
|
||||
<Meter.Label>Storage Used</Meter.Label>
|
||||
<Meter.Value />
|
||||
<Meter.Track>
|
||||
<Meter.Indicator />
|
||||
</Meter.Track>
|
||||
</Meter.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Meter,
|
||||
MeterIndicator,
|
||||
MeterLabel,
|
||||
MeterTrack,
|
||||
MeterValue,
|
||||
} from "@/components/ui/meter"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Meter`:
|
||||
|
||||
```txt
|
||||
Meter
|
||||
├── MeterLabel
|
||||
├── MeterTrack
|
||||
├── MeterIndicator
|
||||
└── MeterValue
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Meter API](https://base-ui.com/react/components/meter#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Meter API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Meter` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Track
|
||||
|
||||
`Meter.Track` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Indicator
|
||||
|
||||
`Meter.Indicator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Value
|
||||
|
||||
`Meter.Value` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Label
|
||||
|
||||
`Meter.Label` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,257 @@
|
||||
# Navigation Menu
|
||||
|
||||
A collection of links and menus for website navigation.
|
||||
|
||||
Navigation Menu wraps Base UI's navigation menu primitive with Tailwind classes from the official Base UI example. Use it for top-level website navigation, animated dropdown panels, nested navigation, and framework-specific links.
|
||||
|
||||
::example{src="components/navigation-menu/navigation-menu.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import NavigationMenu from "@/components/ui/navigation-menu"
|
||||
|
||||
<NavigationMenu.Root>
|
||||
<NavigationMenu.List>
|
||||
<NavigationMenu.Item>
|
||||
<NavigationMenu.Trigger>Overview</NavigationMenu.Trigger>
|
||||
<NavigationMenu.Content>
|
||||
<NavigationMenu.Link href="#">Quick Start</NavigationMenu.Link>
|
||||
</NavigationMenu.Content>
|
||||
</NavigationMenu.Item>
|
||||
</NavigationMenu.List>
|
||||
<NavigationMenu.Portal>
|
||||
<NavigationMenu.Positioner>
|
||||
<NavigationMenu.Popup>
|
||||
<NavigationMenu.Viewport />
|
||||
</NavigationMenu.Popup>
|
||||
</NavigationMenu.Positioner>
|
||||
</NavigationMenu.Portal>
|
||||
</NavigationMenu.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuPopup,
|
||||
NavigationMenuPortal,
|
||||
NavigationMenuPositioner,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuViewport,
|
||||
} from "@/components/ui/navigation-menu"
|
||||
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>Overview</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<NavigationMenuLink href="#">Quick Start</NavigationMenuLink>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
<NavigationMenuPortal>
|
||||
<NavigationMenuPositioner>
|
||||
<NavigationMenuPopup>
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPopup>
|
||||
</NavigationMenuPositioner>
|
||||
</NavigationMenuPortal>
|
||||
</NavigationMenu>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build a `NavigationMenu`:
|
||||
|
||||
```txt
|
||||
NavigationMenu
|
||||
├── NavigationMenuList
|
||||
│ └── NavigationMenuItem
|
||||
│ ├── NavigationMenuTrigger
|
||||
│ │ └── NavigationMenuIcon
|
||||
│ └── NavigationMenuContent
|
||||
│ └── NavigationMenuLink
|
||||
└── NavigationMenuPortal
|
||||
├── NavigationMenuBackdrop
|
||||
└── NavigationMenuPositioner
|
||||
└── NavigationMenuPopup
|
||||
├── NavigationMenuArrow
|
||||
└── NavigationMenuViewport
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Navigation Menu.
|
||||
|
||||
### Nested submenus
|
||||
|
||||
`<NavigationMenu.Root>` can be nested within a higher-level `<NavigationMenu.Content>` part to create a multi-level navigation menu.
|
||||
|
||||
::example{src="components/navigation-menu/nested-submenus.tsx"}
|
||||
|
||||
### Nested inline submenus
|
||||
|
||||
For second-level navigation that should stay in the same panel, omit the nested `<NavigationMenu.Portal>` and render only `List` plus `Viewport` with a `defaultValue`.
|
||||
|
||||
::example{src="components/navigation-menu/nested-inline-submenus.tsx"}
|
||||
|
||||
### Custom links
|
||||
|
||||
The `<NavigationMenu.Link>` part can be customized to render the link from your framework using the `render` prop to enable client-side routing.
|
||||
|
||||
```tsx
|
||||
import NextLink from "next/link"
|
||||
import NavigationMenu from "@/components/ui/navigation-menu"
|
||||
|
||||
function Link(props: NavigationMenu.Link.Props) {
|
||||
return (
|
||||
<NavigationMenu.Link
|
||||
render={<NextLink href={props.href} />}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Large menus
|
||||
|
||||
When menu content may not fit in the viewport, use collision-aware positioning for floating popups. For very large navigation surfaces, render the content inline or move to a dedicated page-level navigation layout instead of forcing a giant popup.
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Navigation Menu API](https://base-ui.com/react/components/navigation-menu#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Navigation Menu API. Props, popup positioning, activation direction, viewport behavior, and data attributes follow the corresponding Base UI component documentation. Styled parts forward `className` and can be replaced with the `render` prop where Base UI supports it.
|
||||
|
||||
### Root
|
||||
|
||||
Owns navigation state, active item value, orientation, and delay behavior.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` | `string` | - | Initial uncontrolled active item value. |
|
||||
| `value` | `string` | - | Controlled active item value. |
|
||||
| `onValueChange` | `(value, eventDetails) => void` | - | Called when the active item changes. |
|
||||
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Direction of the navigation menu. |
|
||||
| `delay` | `number` | - | Delay before opening content after pointer movement. |
|
||||
| `children` | `React.ReactNode` | - | List and portal parts. |
|
||||
|
||||
### List
|
||||
|
||||
Groups top-level navigation items and establishes keyboard navigation for the collection.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for orientation state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the list element. |
|
||||
| `children` | `React.ReactNode` | - | Top-level items. |
|
||||
|
||||
### Item
|
||||
|
||||
Defines one top-level navigation item and connects a trigger to its associated content.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `value` | `string` | - | Stable item value used by the root. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for active and orientation state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the item element. |
|
||||
| `children` | `React.ReactNode` | - | Trigger and content parts. |
|
||||
|
||||
### Trigger and Icon
|
||||
|
||||
`NavigationMenu.Trigger` opens an item's popup content. `NavigationMenu.Icon` receives popup state and is useful for caret rotation or disclosure indicators.
|
||||
|
||||
| Part | Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `NavigationMenu.Trigger` | `disabled` | `boolean` | `false` | Prevents user interaction. |
|
||||
| `NavigationMenu.Trigger` | `className` | `string \| (state) => string` | - | Classes for active, highlighted, and disabled state. |
|
||||
| `NavigationMenu.Trigger` | `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the trigger element. |
|
||||
| `NavigationMenu.Icon` | `className` | `string \| (state) => string` | - | Classes for open and closed state. |
|
||||
| `NavigationMenu.Icon` | `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the icon element. |
|
||||
|
||||
### Content and Link
|
||||
|
||||
`NavigationMenu.Content` holds the panel associated with a trigger. `NavigationMenu.Link` renders a navigation link and supports the `render` prop for framework router links.
|
||||
|
||||
| Part | Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `NavigationMenu.Content` | `className` | `string \| (state) => string` | - | Classes for active and transition state. |
|
||||
| `NavigationMenu.Content` | `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the content element. |
|
||||
| `NavigationMenu.Link` | `href` | `string` | - | Link target. |
|
||||
| `NavigationMenu.Link` | `disabled` | `boolean` | `false` | Prevents user interaction. |
|
||||
| `NavigationMenu.Link` | `className` | `string \| (state) => string` | - | Classes for active, highlighted, and disabled state. |
|
||||
| `NavigationMenu.Link` | `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the link element, often for router links. |
|
||||
|
||||
### Portal
|
||||
|
||||
Moves floating content to the document body by default.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives the portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps portal children mounted while closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating navigation content. |
|
||||
|
||||
Use the Base UI `CSPProvider` at the application root when portal content needs consistent clipping, z-index, and stacking behavior.
|
||||
|
||||
### Backdrop
|
||||
|
||||
Renders the dismissable layer behind the navigation popup when modal behavior is active.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the backdrop state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
|
||||
### Positioner
|
||||
|
||||
Positions the popup relative to the active trigger.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `"top" \| "right" \| "bottom" \| "left" \| "inline-start" \| "inline-end"` | `"bottom"` | Preferred side for the popup. |
|
||||
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance between trigger and popup. |
|
||||
| `alignOffset` | `number \| OffsetFunction` | `0` | Offset along the alignment axis. |
|
||||
| `arrowPadding` | `number` | `5` | Minimum distance between arrow and popup edge. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flipping and shifting when space is limited. |
|
||||
| `collisionBoundary` | `Boundary` | `"clipping-ancestors"` | Boundary used for collision detection. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement and open state. |
|
||||
|
||||
### Popup
|
||||
|
||||
Contains the floating navigation surface.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for open, closing, side, and alignment state. |
|
||||
| `style` | `CSSProperties \| (state) => CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the popup element. |
|
||||
| `children` | `React.ReactNode` | - | Viewport and optional arrow. |
|
||||
|
||||
### Viewport
|
||||
|
||||
Hosts the active content and supports animated size changes as the active navigation item changes.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for animated size and active content state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the viewport element. |
|
||||
| `children` | `React.ReactNode` | - | Active content. |
|
||||
|
||||
### Arrow
|
||||
|
||||
Visually connects the popup to the active trigger.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for side and alignment state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the arrow element. |
|
||||
@@ -0,0 +1,135 @@
|
||||
# Number Field
|
||||
|
||||
Number Field wraps Base UI's number field primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/number-field/number-field.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import NumberField from "@/components/ui/number-field"
|
||||
|
||||
<NumberField.Root defaultValue={100}>
|
||||
<NumberField.ScrubArea>
|
||||
<label>Amount</label>
|
||||
<NumberField.ScrubAreaCursor />
|
||||
</NumberField.ScrubArea>
|
||||
<NumberField.Group>
|
||||
<NumberField.Decrement />
|
||||
<NumberField.Input />
|
||||
<NumberField.Increment />
|
||||
</NumberField.Group>
|
||||
</NumberField.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldDecrement,
|
||||
NumberFieldGroup,
|
||||
NumberFieldIncrement,
|
||||
NumberFieldInput,
|
||||
} from "@/components/ui/number-field"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `NumberField`:
|
||||
|
||||
```txt
|
||||
NumberField
|
||||
├── NumberFieldInput
|
||||
├── NumberFieldGroup
|
||||
├── NumberFieldIncrement
|
||||
├── NumberFieldDecrement
|
||||
├── NumberFieldScrubArea
|
||||
└── NumberFieldScrubAreaCursor
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Number Field API](https://base-ui.com/react/components/number-field#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Number Field API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`NumberField` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Group
|
||||
|
||||
`NumberField.Group` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Increment
|
||||
|
||||
`NumberField.Increment` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Decrement
|
||||
|
||||
`NumberField.Decrement` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Input
|
||||
|
||||
`NumberField.Input` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### ScrubArea
|
||||
|
||||
`NumberField.ScrubArea` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### ScrubAreaCursor
|
||||
|
||||
`NumberField.ScrubAreaCursor` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,143 @@
|
||||
# OTP Field
|
||||
|
||||
OTP Field wraps Base UI's OTP field primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/otp-field/otp-field.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import OTPField from "@/components/ui/otp-field"
|
||||
|
||||
<OTPField.Root length={6}>
|
||||
{Array.from({ length: 6 }, (_, index) => (
|
||||
<OTPField.Input key={index} />
|
||||
))}
|
||||
</OTPField.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { OTPField, OTPFieldInput } from "@/components/ui/otp-field"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `OtpField`:
|
||||
|
||||
```txt
|
||||
OtpField
|
||||
├── OtpFieldInput
|
||||
└── OtpField.Separator
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for OTP Field.
|
||||
|
||||
### Labeling an OTP field
|
||||
|
||||
Pass an `id` to `<OTPField.Root>` and use a native `<label>` with a matching `htmlFor`. Let the first input use the field label, and add `aria-label` to the remaining inputs so assistive technology can announce which slot is focused.
|
||||
|
||||
```tsx
|
||||
<div>
|
||||
<label htmlFor="verification-code">Verification code</label>
|
||||
<OTPField.Root id="verification-code" length={6} aria-describedby="verification-code-description">
|
||||
<OTPField.Input />
|
||||
<OTPField.Input aria-label="Character 2 of 6" />
|
||||
<OTPField.Input aria-label="Character 3 of 6" />
|
||||
<OTPField.Input aria-label="Character 4 of 6" />
|
||||
<OTPField.Input aria-label="Character 5 of 6" />
|
||||
<OTPField.Input aria-label="Character 6 of 6" />
|
||||
</OTPField.Root>
|
||||
<p id="verification-code-description">Enter the 6-character code we sent to your device.</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Form integration
|
||||
|
||||
Use Field to handle label associations and form integration:
|
||||
|
||||
```tsx
|
||||
<Form>
|
||||
<Field.Root name="verificationCode">
|
||||
<Field.Label>Verification code</Field.Label>
|
||||
<Field.Description>Enter the 6-character code we sent to your device.</Field.Description>
|
||||
<OTPField.Root length={6}>
|
||||
<OTPField.Input />
|
||||
<OTPField.Input aria-label="Character 2 of 6" />
|
||||
<OTPField.Input aria-label="Character 3 of 6" />
|
||||
<OTPField.Input aria-label="Character 4 of 6" />
|
||||
<OTPField.Input aria-label="Character 5 of 6" />
|
||||
<OTPField.Input aria-label="Character 6 of 6" />
|
||||
</OTPField.Root>
|
||||
</Field.Root>
|
||||
</Form>
|
||||
```
|
||||
|
||||
### Alphanumeric verification codes
|
||||
|
||||
Use `validationType="alphanumeric"` for recovery, backup, or invite codes that mix letters and numbers.
|
||||
|
||||
### Grouped layouts
|
||||
|
||||
Wrap subsets of inputs in your own layout elements and use `<OTPField.Separator>` when you want the code presented in smaller visual chunks such as `123-456`.
|
||||
|
||||
|
||||
### Alphanumeric verification codes
|
||||
|
||||
::example{src="components/otp-field/alphanumeric-verification-codes.tsx"}
|
||||
|
||||
### Grouped layouts
|
||||
|
||||
::example{src="components/otp-field/grouped-layouts.tsx"}
|
||||
|
||||
### Placeholder hints
|
||||
|
||||
::example{src="components/otp-field/placeholder-hints.tsx"}
|
||||
|
||||
### Masked entry
|
||||
|
||||
::example{src="components/otp-field/masked-entry.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI OTP Field API](https://base-ui.com/react/components/otp-field#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI OTP Field API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`OtpField` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Input
|
||||
|
||||
`OtpField.Input` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Separator
|
||||
|
||||
`OtpField.Separator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,243 @@
|
||||
# Popover
|
||||
|
||||
Popover wraps Base UI's popover primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/popover/popover.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Popover from "@/components/ui/popover"
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>Notifications</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Positioner sideOffset={8}>
|
||||
<Popover.Popup>
|
||||
<Popover.Arrow />
|
||||
<Popover.Title>Notifications</Popover.Title>
|
||||
<Popover.Description>You are all caught up. Good job!</Popover.Description>
|
||||
</Popover.Popup>
|
||||
</Popover.Positioner>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverPopup,
|
||||
PopoverPortal,
|
||||
PopoverPositioner,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Popover`:
|
||||
|
||||
```txt
|
||||
Popover
|
||||
├── PopoverTrigger
|
||||
├── PopoverPortal
|
||||
│ ├── PopoverBackdrop
|
||||
│ └── PopoverPositioner
|
||||
│ └── PopoverPopup
|
||||
│ ├── PopoverArrow
|
||||
│ └── PopoverViewport
|
||||
├── PopoverTitle
|
||||
├── PopoverDescription
|
||||
└── PopoverClose
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Popover.
|
||||
|
||||
### Opening on hover
|
||||
|
||||
This example shows how you can configure the popover to open on hover using the `openOnHover` prop.
|
||||
|
||||
### Detached triggers
|
||||
|
||||
A popover can be controlled by a trigger located either inside or outside the `<Popover.Root>` component. For simple, one-off interactions, place the `<Popover.Trigger>` inside `<Popover.Root>`, as shown in the example at the top of this page.
|
||||
|
||||
```jsx
|
||||
const demoPopover = Popover.createHandle()
|
||||
|
||||
<Popover.Trigger handle={demoPopover}>
|
||||
Trigger
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Root handle={demoPopover}>
|
||||
...
|
||||
</Popover.Root>
|
||||
```
|
||||
|
||||
### Multiple triggers
|
||||
|
||||
A single popover can be opened by multiple trigger elements. You can achieve this by using the same `handle` for several detached triggers, or by placing multiple `<Popover.Trigger>` components inside a single `<Popover.Root>`.
|
||||
|
||||
```jsx
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>Trigger 1</Popover.Trigger>
|
||||
<Popover.Trigger>Trigger 2</Popover.Trigger>
|
||||
...
|
||||
</Popover.Root>
|
||||
```
|
||||
|
||||
### Controlled mode with multiple triggers
|
||||
|
||||
You can control the popover's open state externally using the `open` and `onOpenChange` props on `<Popover.Root>`. This allows you to manage the popover's visibility based on your application's state. When using multiple triggers, you have to manage which trigger is active with the `triggerId` prop on `<Popover.Root>` and the `id` prop on each `<Popover.Trigger>`.
|
||||
|
||||
|
||||
### Opening on hover
|
||||
|
||||
::example{src="components/popover/opening-on-hover.tsx"}
|
||||
|
||||
### Detached triggers
|
||||
|
||||
::example{src="components/popover/detached-triggers.tsx"}
|
||||
|
||||
### Controlled mode with multiple triggers
|
||||
|
||||
::example{src="components/popover/controlled-mode-with-multiple-triggers.tsx"}
|
||||
|
||||
### Animating the Popover
|
||||
|
||||
::example{src="components/popover/animating-the-popover.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Popover API](https://base-ui.com/react/components/popover#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Popover API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Popover` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`Popover.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Portal
|
||||
|
||||
`Popover.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Positioner
|
||||
|
||||
`Popover.Positioner` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `Side` | - | Preferred side for floating content. |
|
||||
| `align` | `Align` | - | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance from the anchor. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flip and shift behavior. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement state. |
|
||||
|
||||
### Popup
|
||||
|
||||
`Popover.Popup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Arrow
|
||||
|
||||
`Popover.Arrow` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Backdrop
|
||||
|
||||
`Popover.Backdrop` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Title
|
||||
|
||||
`Popover.Title` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Description
|
||||
|
||||
`Popover.Description` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Close
|
||||
|
||||
`Popover.Close` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Viewport
|
||||
|
||||
`Popover.Viewport` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,200 @@
|
||||
# Preview Card
|
||||
|
||||
Preview Card wraps Base UI's preview card primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/preview-card/preview-card.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import PreviewCard from "@/components/ui/preview-card"
|
||||
|
||||
<PreviewCard.Root>
|
||||
<PreviewCard.Trigger href="https://en.wikipedia.org/wiki/Typography">
|
||||
typography
|
||||
</PreviewCard.Trigger>
|
||||
<PreviewCard.Portal>
|
||||
<PreviewCard.Positioner sideOffset={8}>
|
||||
<PreviewCard.Popup>
|
||||
<PreviewCard.Arrow />
|
||||
</PreviewCard.Popup>
|
||||
</PreviewCard.Positioner>
|
||||
</PreviewCard.Portal>
|
||||
</PreviewCard.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
PreviewCard,
|
||||
PreviewCardPopup,
|
||||
PreviewCardPortal,
|
||||
PreviewCardTrigger,
|
||||
} from "@/components/ui/preview-card"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `PreviewCard`:
|
||||
|
||||
```txt
|
||||
PreviewCard
|
||||
├── PreviewCardTrigger
|
||||
└── PreviewCardPortal
|
||||
├── PreviewCardBackdrop
|
||||
└── PreviewCardPositioner
|
||||
└── PreviewCardPopup
|
||||
├── PreviewCardArrow
|
||||
└── PreviewCardViewport
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Preview Card.
|
||||
|
||||
### Detached triggers
|
||||
|
||||
A preview card can be controlled by a trigger located either inside or outside the `<PreviewCard.Root>` component. For simple, one-off interactions, place the `<PreviewCard.Trigger>` inside `<PreviewCard.Root>`, as shown in the example at the top of this page.
|
||||
|
||||
```jsx
|
||||
const demoPreviewCard = PreviewCard.createHandle()
|
||||
|
||||
<PreviewCard.Trigger handle={demoPreviewCard} href="#">
|
||||
Link
|
||||
</PreviewCard.Trigger>
|
||||
|
||||
<PreviewCard.Root handle={demoPreviewCard}>
|
||||
...
|
||||
</PreviewCard.Root>;
|
||||
```
|
||||
|
||||
### Multiple triggers
|
||||
|
||||
A single preview card can be opened by multiple trigger elements. You can achieve this by using the same `handle` for several detached triggers, or by placing multiple `<PreviewCard.Trigger>` components inside a single `<PreviewCard.Root>`.
|
||||
|
||||
```jsx
|
||||
<PreviewCard.Root>
|
||||
<PreviewCard.Trigger href="#">Trigger 1</PreviewCard.Trigger>
|
||||
<PreviewCard.Trigger href="#">Trigger 2</PreviewCard.Trigger>
|
||||
...
|
||||
</PreviewCard.Root>
|
||||
```
|
||||
|
||||
### Controlled mode with multiple triggers
|
||||
|
||||
You can control the preview card's open state externally using the `open` and `onOpenChange` props on `<PreviewCard.Root>`. This allows you to manage the preview card's visibility based on your application's state. When using multiple triggers, you have to manage which trigger is active with the `triggerId` prop on `<PreviewCard.Root>` and the `id` prop on each `<PreviewCard.Trigger>`.
|
||||
|
||||
### Animating the Preview Card
|
||||
|
||||
You can animate a preview card as it moves between different trigger elements. This includes animating its position, size, and content.
|
||||
|
||||
|
||||
### Detached triggers
|
||||
|
||||
::example{src="components/preview-card/detached-triggers.tsx"}
|
||||
|
||||
### Controlled mode with multiple triggers
|
||||
|
||||
::example{src="components/preview-card/controlled-mode-with-multiple-triggers.tsx"}
|
||||
|
||||
### Animating the Preview Card
|
||||
|
||||
::example{src="components/preview-card/animating-the-preview-card.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Preview Card API](https://base-ui.com/react/components/preview-card#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Preview Card API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`PreviewCard` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Portal
|
||||
|
||||
`PreviewCard.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`PreviewCard.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Positioner
|
||||
|
||||
`PreviewCard.Positioner` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `Side` | - | Preferred side for floating content. |
|
||||
| `align` | `Align` | - | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance from the anchor. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flip and shift behavior. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement state. |
|
||||
|
||||
### Popup
|
||||
|
||||
`PreviewCard.Popup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Arrow
|
||||
|
||||
`PreviewCard.Arrow` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Backdrop
|
||||
|
||||
`PreviewCard.Backdrop` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Viewport
|
||||
|
||||
`PreviewCard.Viewport` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,107 @@
|
||||
# Progress
|
||||
|
||||
Progress wraps Base UI's progress primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/progress/progress.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Progress from "@/components/ui/progress"
|
||||
|
||||
<Progress.Root value={20}>
|
||||
<Progress.Label>Export data</Progress.Label>
|
||||
<Progress.Value />
|
||||
<Progress.Track>
|
||||
<Progress.Indicator />
|
||||
</Progress.Track>
|
||||
</Progress.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Progress,
|
||||
ProgressIndicator,
|
||||
ProgressLabel,
|
||||
ProgressTrack,
|
||||
ProgressValue,
|
||||
} from "@/components/ui/progress"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Progress`:
|
||||
|
||||
```txt
|
||||
Progress
|
||||
├── ProgressLabel
|
||||
├── ProgressTrack
|
||||
├── ProgressIndicator
|
||||
└── ProgressValue
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Progress API](https://base-ui.com/react/components/progress#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Progress API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Progress` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Track
|
||||
|
||||
`Progress.Track` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Indicator
|
||||
|
||||
`Progress.Indicator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Value
|
||||
|
||||
`Progress.Value` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Label
|
||||
|
||||
`Progress.Label` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,78 @@
|
||||
# Radio
|
||||
|
||||
Radio wraps Base UI's radio primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/radio/radio.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import { RadioGroup } from "@base-ui/react/radio-group"
|
||||
import Radio from "@/components/ui/radio"
|
||||
|
||||
function ExampleRadioGroup() {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<RadioGroup aria-labelledby={id} defaultValue="fuji-apple" className="flex flex-col items-start gap-1">
|
||||
<div id={id} className="text-sm font-bold">Best apple</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Radio.Root value="fuji-apple">
|
||||
<Radio.Indicator />
|
||||
</Radio.Root>
|
||||
Fuji
|
||||
</label>
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Radio, RadioIndicator } from "@/components/ui/radio"
|
||||
|
||||
<Radio value="fuji-apple">
|
||||
<RadioIndicator />
|
||||
</Radio>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Radio`:
|
||||
|
||||
```txt
|
||||
Radio
|
||||
└── RadioIndicator
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Radio API](https://base-ui.com/react/components/radio#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Radio API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Radio` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Indicator
|
||||
|
||||
`Radio.Indicator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,129 @@
|
||||
# Scroll Area
|
||||
|
||||
Scroll Area wraps Base UI's scroll area primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/scroll-area/scroll-area.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import ScrollArea from "@/components/ui/scroll-area"
|
||||
|
||||
<ScrollArea.Root>
|
||||
<ScrollArea.Viewport>
|
||||
<ScrollArea.Content>Scrollable content</ScrollArea.Content>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar>
|
||||
<ScrollArea.Thumb />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from "@/components/ui/scroll-area"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `ScrollArea`:
|
||||
|
||||
```txt
|
||||
ScrollArea
|
||||
├── ScrollAreaThumb
|
||||
├── ScrollAreaViewport
|
||||
├── ScrollAreaScrollbar
|
||||
├── ScrollAreaContent
|
||||
└── ScrollAreaCorner
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Both scrollbars
|
||||
|
||||
::example{src="components/scroll-area/both-scrollbars.tsx"}
|
||||
|
||||
### Gradient scroll fade
|
||||
|
||||
::example{src="components/scroll-area/gradient-scroll-fade.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Scroll Area API](https://base-ui.com/react/components/scroll-area#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Scroll Area API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`ScrollArea` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Viewport
|
||||
|
||||
`ScrollArea.Viewport` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Scrollbar
|
||||
|
||||
`ScrollArea.Scrollbar` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Content
|
||||
|
||||
`ScrollArea.Content` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Thumb
|
||||
|
||||
`ScrollArea.Thumb` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Corner
|
||||
|
||||
`ScrollArea.Corner` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,391 @@
|
||||
# Select
|
||||
|
||||
Select exposes every styled Base UI part and named exports for shadcn-style composition.
|
||||
|
||||
::example{src="components/select/select.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Select from "@/components/ui/select"
|
||||
import { Check, ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"
|
||||
|
||||
const apples = [
|
||||
{ label: "Gala", value: "gala" },
|
||||
{ label: "Fuji", value: "fuji" },
|
||||
{ label: "Honeycrisp", value: "honeycrisp" },
|
||||
]
|
||||
|
||||
<Select.Root items={apples}>
|
||||
<Select.Label>Apple</Select.Label>
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Select apple" />
|
||||
<Select.Icon>
|
||||
<ChevronsUpDown className="size-4" />
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
<Select.Portal>
|
||||
<Select.Positioner sideOffset={4}>
|
||||
<Select.Popup>
|
||||
<Select.ScrollUpArrow>
|
||||
<ChevronUp className="size-4" />
|
||||
</Select.ScrollUpArrow>
|
||||
<Select.List>
|
||||
{apples.map(({ label, value }) => (
|
||||
<Select.Item key={label} value={value}>
|
||||
<Select.ItemIndicator>
|
||||
<Check className="size-4" />
|
||||
</Select.ItemIndicator>
|
||||
<Select.ItemText>{label}</Select.ItemText>
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.List>
|
||||
<Select.ScrollDownArrow>
|
||||
<ChevronDown className="size-4" />
|
||||
</Select.ScrollDownArrow>
|
||||
</Select.Popup>
|
||||
</Select.Positioner>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Select,
|
||||
SelectIcon,
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectPopup,
|
||||
SelectPortal,
|
||||
SelectPositioner,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Select`:
|
||||
|
||||
```txt
|
||||
Select
|
||||
├── SelectTrigger
|
||||
├── SelectLabel
|
||||
├── SelectGroup
|
||||
├── SelectPortal
|
||||
│ ├── SelectBackdrop
|
||||
│ └── SelectPositioner
|
||||
│ └── SelectPopup
|
||||
│ ├── SelectArrow
|
||||
│ ├── SelectList
|
||||
│ ├── SelectGroup
|
||||
│ └── SelectItem
|
||||
├── SelectValue
|
||||
├── SelectIcon
|
||||
├── SelectItemIndicator
|
||||
├── SelectItemText
|
||||
├── SelectScrollDownArrow
|
||||
├── SelectScrollUpArrow
|
||||
├── SelectGroupLabel
|
||||
└── Select.Separator
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Select.
|
||||
|
||||
### Typed wrapper component
|
||||
|
||||
The following example shows a typed wrapper around the Select component with correct type inference and type safety:
|
||||
|
||||
```tsx
|
||||
import * as React from "react"
|
||||
import Select from "@/components/ui/select"
|
||||
|
||||
export function MySelect<Value, Multiple extends boolean | undefined = false>(
|
||||
props: Select.Root.Props<Value, Multiple>,
|
||||
): React.JSX.Element {
|
||||
return <Select.Root {...props}>{/* ... */}</Select.Root>
|
||||
}
|
||||
```
|
||||
|
||||
### Formatting the value
|
||||
|
||||
By default, the `<Select.Value>` component renders the raw `value`.
|
||||
|
||||
```jsx
|
||||
const items = [
|
||||
{ value: null, label: "Select theme" },
|
||||
{ value: "system", label: "System default" },
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
]
|
||||
|
||||
<Select.Root items={items}>
|
||||
<Select.Value />
|
||||
</Select.Root>;
|
||||
```
|
||||
|
||||
### Labeling a select
|
||||
|
||||
Use `<Select.Label>` to provide a visible label for the select trigger:
|
||||
|
||||
```tsx
|
||||
<Select.Root>
|
||||
|
||||
<Select.Label>Theme</Select.Label>
|
||||
{/* ... */}
|
||||
</Select.Root>
|
||||
```
|
||||
|
||||
### Placeholder values
|
||||
|
||||
To show a placeholder value, use the `placeholder` prop on `<Select.Value>`:
|
||||
|
||||
```jsx
|
||||
const items = [
|
||||
{ value: "system", label: "System default" },
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
]
|
||||
|
||||
<Select.Root items={items}>
|
||||
|
||||
<Select.Value placeholder="Select theme" />
|
||||
</Select.Root>;
|
||||
```
|
||||
|
||||
|
||||
### Multiple selection
|
||||
|
||||
::example{src="components/select/multiple-selection.tsx"}
|
||||
|
||||
### Object values
|
||||
|
||||
::example{src="components/select/object-values.tsx"}
|
||||
|
||||
### Grouped
|
||||
|
||||
::example{src="components/select/grouped.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Select API](https://base-ui.com/react/components/select#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Select API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Select` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Label
|
||||
|
||||
`Select.Label` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`Select.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Value
|
||||
|
||||
`Select.Value` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Icon
|
||||
|
||||
`Select.Icon` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Portal
|
||||
|
||||
`Select.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Backdrop
|
||||
|
||||
`Select.Backdrop` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Positioner
|
||||
|
||||
`Select.Positioner` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `Side` | - | Preferred side for floating content. |
|
||||
| `align` | `Align` | - | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance from the anchor. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flip and shift behavior. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement state. |
|
||||
|
||||
### Popup
|
||||
|
||||
`Select.Popup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### List
|
||||
|
||||
`Select.List` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Item
|
||||
|
||||
`Select.Item` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### ItemIndicator
|
||||
|
||||
`Select.ItemIndicator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### ItemText
|
||||
|
||||
`Select.ItemText` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Arrow
|
||||
|
||||
`Select.Arrow` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### ScrollDownArrow
|
||||
|
||||
`Select.ScrollDownArrow` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### ScrollUpArrow
|
||||
|
||||
`Select.ScrollUpArrow` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Group
|
||||
|
||||
`Select.Group` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### GroupLabel
|
||||
|
||||
`Select.GroupLabel` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Separator
|
||||
|
||||
`Select.Separator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,49 @@
|
||||
# Separator
|
||||
|
||||
Separator wraps Base UI's separator primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/separator/separator.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Separator from "@/components/ui/separator"
|
||||
|
||||
<Separator orientation="vertical" className="w-px bg-neutral-300 dark:bg-neutral-700" />
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
<Separator orientation="vertical" className="w-px bg-neutral-300" />
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Separator`:
|
||||
|
||||
```txt
|
||||
Separator
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Separator API](https://base-ui.com/react/components/separator#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Separator API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Separator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
@@ -0,0 +1,176 @@
|
||||
# Slider
|
||||
|
||||
Slider wraps Base UI's slider primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/slider/slider.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Slider from "@/components/ui/slider"
|
||||
|
||||
<Slider.Root defaultValue={25}>
|
||||
<Slider.Control>
|
||||
<Slider.Track>
|
||||
<Slider.Indicator />
|
||||
<Slider.Thumb aria-label="Volume" />
|
||||
</Slider.Track>
|
||||
</Slider.Control>
|
||||
</Slider.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Slider,
|
||||
SliderControl,
|
||||
SliderIndicator,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
} from "@/components/ui/slider"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Slider`:
|
||||
|
||||
```txt
|
||||
Slider
|
||||
├── SliderLabel
|
||||
├── SliderControl
|
||||
├── SliderTrack
|
||||
├── SliderThumb
|
||||
├── SliderValue
|
||||
└── SliderIndicator
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Slider.
|
||||
|
||||
### Range slider
|
||||
|
||||
To create a range slider:
|
||||
|
||||
### Thumb alignment
|
||||
|
||||
Set `thumbAlignment="edge"` to inset the thumb such that its edge aligns with the edge of the control when the value is at `min` or `max`, without overflowing the control like the default `"center"` alignment.
|
||||
|
||||
### Labeling a slider
|
||||
|
||||
A single-thumb slider without a visible label (such as a volume control) can be labeled using `aria-label` on `<Slider.Thumb>`:
|
||||
|
||||
```tsx
|
||||
<Slider.Root>
|
||||
<Slider.Control>
|
||||
<Slider.Track>
|
||||
<Slider.Indicator />
|
||||
|
||||
<Slider.Thumb aria-label="Volume" />
|
||||
</Slider.Track>
|
||||
</Slider.Control>
|
||||
</Slider.Root>
|
||||
```
|
||||
|
||||
### Vertical
|
||||
|
||||
Set `orientation="vertical"` on `<Slider.Root>` to build a vertical slider.
|
||||
|
||||
|
||||
### Range slider
|
||||
|
||||
::example{src="components/slider/range-slider.tsx"}
|
||||
|
||||
### Thumb alignment
|
||||
|
||||
::example{src="components/slider/thumb-alignment.tsx"}
|
||||
|
||||
### Vertical
|
||||
|
||||
::example{src="components/slider/vertical.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Slider API](https://base-ui.com/react/components/slider#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Slider API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Slider` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Label
|
||||
|
||||
`Slider.Label` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Value
|
||||
|
||||
`Slider.Value` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Control
|
||||
|
||||
`Slider.Control` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Track
|
||||
|
||||
`Slider.Track` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Thumb
|
||||
|
||||
`Slider.Thumb` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Indicator
|
||||
|
||||
`Slider.Indicator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,68 @@
|
||||
# Switch
|
||||
|
||||
Switch wraps Base UI's switch primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/switch/switch.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Switch from "@/components/ui/switch"
|
||||
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Switch.Root defaultChecked>
|
||||
<Switch.Thumb />
|
||||
</Switch.Root>
|
||||
Notifications
|
||||
</label>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Switch, SwitchThumb } from "@/components/ui/switch"
|
||||
|
||||
<Switch defaultChecked>
|
||||
<SwitchThumb />
|
||||
</Switch>
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Switch`:
|
||||
|
||||
```txt
|
||||
Switch
|
||||
└── SwitchThumb
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Switch API](https://base-ui.com/react/components/switch#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Switch API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Switch` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Thumb
|
||||
|
||||
`Switch.Thumb` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,108 @@
|
||||
# Tabs
|
||||
|
||||
Tabs wraps Base UI's tabs primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/tabs/tabs.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Tabs from "@/components/ui/tabs"
|
||||
|
||||
<Tabs.Root defaultValue="overview">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="overview">Overview</Tabs.Tab>
|
||||
<Tabs.Tab value="projects">Projects</Tabs.Tab>
|
||||
<Tabs.Indicator />
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="overview">Workspace stats and activity.</Tabs.Panel>
|
||||
</Tabs.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Tabs,
|
||||
TabsIndicator,
|
||||
TabsList,
|
||||
TabsPanel,
|
||||
TabsTab,
|
||||
} from "@/components/ui/tabs"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Tabs`:
|
||||
|
||||
```txt
|
||||
Tabs
|
||||
├── TabsTab
|
||||
├── TabsIndicator
|
||||
├── TabsPanel
|
||||
└── TabsList
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Tabs API](https://base-ui.com/react/components/tabs#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Tabs API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Tabs` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Tab
|
||||
|
||||
`Tabs.Tab` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Indicator
|
||||
|
||||
`Tabs.Indicator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Panel
|
||||
|
||||
`Tabs.Panel` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### List
|
||||
|
||||
`Tabs.List` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,278 @@
|
||||
# Toast
|
||||
|
||||
Toast wraps Base UI's toast primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/toast/toast.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Toast, { useToastManager } from "@/components/ui/toast"
|
||||
|
||||
function ToastButton() {
|
||||
const toastManager = useToastManager()
|
||||
|
||||
return (
|
||||
<button onClick={() => toastManager.add({ title: "Saved" })}>
|
||||
Create toast
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
<Toast.Provider>
|
||||
<ToastButton />
|
||||
<Toast.Portal>
|
||||
<Toast.Viewport>
|
||||
{toasts.map((toast) => (
|
||||
<Toast.Root toast={toast}>
|
||||
<Toast.Content>
|
||||
<Toast.Title />
|
||||
<Toast.Description />
|
||||
</Toast.Content>
|
||||
</Toast.Root>
|
||||
))}
|
||||
</Toast.Viewport>
|
||||
</Toast.Portal>
|
||||
</Toast.Provider>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Toast,
|
||||
ToastContent,
|
||||
ToastProvider,
|
||||
ToastRoot,
|
||||
ToastViewport,
|
||||
useToastManager,
|
||||
} from "@/components/ui/toast"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Toast`:
|
||||
|
||||
```txt
|
||||
Toast
|
||||
├── ToastPortal
|
||||
│ └── ToastPositioner
|
||||
│ ├── ToastArrow
|
||||
│ └── ToastViewport
|
||||
├── ToastProvider
|
||||
├── ToastContent
|
||||
├── ToastDescription
|
||||
├── ToastTitle
|
||||
├── ToastClose
|
||||
└── ToastAction
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Toast.
|
||||
|
||||
### Anchored toasts
|
||||
|
||||
Toasts can be anchored to a specific element using `<Toast.Positioner>` and the `positionerProps` option when adding a toast. This is useful for showing contextual feedback like transient "Copied" toasts that appear near the button that triggered the action.
|
||||
|
||||
```tsx
|
||||
const anchoredToastManager = Toast.createToastManager()
|
||||
const stackedToastManager = Toast.createToastManager()
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Toast.Provider toastManager={anchoredToastManager}>
|
||||
<AnchoredToasts />
|
||||
</Toast.Provider>
|
||||
<Toast.Provider toastManager={stackedToastManager}>
|
||||
<StackedToasts />
|
||||
</Toast.Provider>
|
||||
|
||||
{/* App content */}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
function AnchoredToasts() {
|
||||
const { toasts } = Toast.useToastManager()
|
||||
return (
|
||||
<Toast.Portal>
|
||||
<Toast.Viewport>
|
||||
{toasts.map((toast) => (
|
||||
<Toast.Positioner key={toast.id} toast={toast}>
|
||||
<Toast.Root toast={toast}>{/* ... */}</Toast.Root>
|
||||
</Toast.Positioner>
|
||||
))}
|
||||
</Toast.Viewport>
|
||||
</Toast.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function StackedToasts() {
|
||||
const { toasts } = Toast.useToastManager()
|
||||
return (
|
||||
<Toast.Portal>
|
||||
<Toast.Viewport>
|
||||
{toasts.map((toast) => (
|
||||
<Toast.Root key={toast.id} toast={toast}>
|
||||
{/* ... */}
|
||||
</Toast.Root>
|
||||
))}
|
||||
</Toast.Viewport>
|
||||
</Toast.Portal>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Custom position
|
||||
|
||||
The position of the toasts is controlled by your own CSS. To change the toasts' position, you can modify the `.Viewport` and `.Root` styles. A more general component could accept a `data-position` attribute, which the CSS handles for each variation. The following shows a top-center position:
|
||||
|
||||
### Undo action
|
||||
|
||||
When adding a toast, the `actionProps` option can be used to define props for an action button inside of it—this enables the ability to undo an action associated with the toast.
|
||||
|
||||
### Promise
|
||||
|
||||
An asynchronous toast can be created with three possible states: `loading`, `success`, and `error`. The `type` string matches these states to change the styling. Each of the states also accepts the method options object for more granular control.
|
||||
|
||||
|
||||
### Anchored toasts
|
||||
|
||||
::example{src="components/toast/anchored-toasts.tsx"}
|
||||
|
||||
### Custom position
|
||||
|
||||
::example{src="components/toast/custom-position.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Toast API](https://base-ui.com/react/components/toast#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Toast API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Provider
|
||||
|
||||
`Toast.Provider` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Viewport
|
||||
|
||||
`Toast.Viewport` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Root
|
||||
|
||||
`Toast` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Content
|
||||
|
||||
`Toast.Content` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Description
|
||||
|
||||
`Toast.Description` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Title
|
||||
|
||||
`Toast.Title` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Close
|
||||
|
||||
`Toast.Close` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Action
|
||||
|
||||
`Toast.Action` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Portal
|
||||
|
||||
`Toast.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Positioner
|
||||
|
||||
`Toast.Positioner` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `Side` | - | Preferred side for floating content. |
|
||||
| `align` | `Align` | - | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance from the anchor. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flip and shift behavior. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement state. |
|
||||
|
||||
### Arrow
|
||||
|
||||
`Toast.Arrow` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,63 @@
|
||||
# Toggle Group
|
||||
|
||||
Toggle Group wraps Base UI's toggle group primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/toggle-group/toggle-group.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Toggle from "@/components/ui/toggle"
|
||||
import ToggleGroup from "@/components/ui/toggle-group"
|
||||
|
||||
<ToggleGroup aria-label="Text alignment" defaultValue={["left"]}>
|
||||
<Toggle value="left">Left</Toggle>
|
||||
<Toggle value="center">Center</Toggle>
|
||||
</ToggleGroup>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { ToggleGroup } from "@/components/ui/toggle-group"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `ToggleGroup`:
|
||||
|
||||
```txt
|
||||
ToggleGroup
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Toggle Group.
|
||||
|
||||
### Multiple
|
||||
|
||||
Add the `multiple` prop to allow pressing more than one toggle at a time.
|
||||
|
||||
|
||||
### Multiple
|
||||
|
||||
::example{src="components/toggle-group/multiple.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Toggle Group API](https://base-ui.com/react/components/toggle-group#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Toggle Group API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`ToggleGroup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
@@ -0,0 +1,49 @@
|
||||
# Toggle
|
||||
|
||||
Toggle wraps Base UI's toggle primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/toggle/toggle.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Toggle from "@/components/ui/toggle"
|
||||
|
||||
<Toggle aria-label="Favorite">
|
||||
<Heart className="size-4" />
|
||||
</Toggle>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import { Toggle } from "@/components/ui/toggle"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Toggle`:
|
||||
|
||||
```txt
|
||||
Toggle
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Toggle API](https://base-ui.com/react/components/toggle#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Toggle API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Toggle` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
@@ -0,0 +1,146 @@
|
||||
# Toolbar
|
||||
|
||||
Toolbar wraps Base UI's toolbar primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/toolbar/toolbar.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Toolbar from "@/components/ui/toolbar"
|
||||
import Toggle from "@/components/ui/toggle"
|
||||
import ToggleGroup from "@/components/ui/toggle-group"
|
||||
import Select from "@/components/ui/select"
|
||||
|
||||
<Toolbar.Root>
|
||||
<ToggleGroup aria-label="Alignment">
|
||||
<Toolbar.Button render={<Toggle />} value="align-left">
|
||||
Align Left
|
||||
</Toolbar.Button>
|
||||
<Toolbar.Button render={<Toggle />} value="align-right">
|
||||
Align Right
|
||||
</Toolbar.Button>
|
||||
</ToggleGroup>
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Group>
|
||||
<Toolbar.Button>$</Toolbar.Button>
|
||||
<Toolbar.Button>%</Toolbar.Button>
|
||||
</Toolbar.Group>
|
||||
<Select.Root defaultValue="Helvetica">
|
||||
<Toolbar.Button render={<Select.Trigger />}>
|
||||
<Select.Value />
|
||||
<Select.Icon />
|
||||
</Toolbar.Button>
|
||||
<Select.Portal>
|
||||
<Select.Positioner>
|
||||
<Select.Popup />
|
||||
</Select.Positioner>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Link href="#">Edited 51m ago</Toolbar.Link>
|
||||
</Toolbar.Root>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
ToolbarGroup,
|
||||
ToolbarInput,
|
||||
ToolbarLink,
|
||||
ToolbarSeparator,
|
||||
} from "@/components/ui/toolbar"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Toolbar`:
|
||||
|
||||
```txt
|
||||
Toolbar
|
||||
├── ToolbarInput
|
||||
├── ToolbarGroup
|
||||
├── Toolbar.Separator
|
||||
├── ToolbarButton
|
||||
└── ToolbarLink
|
||||
```
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Toolbar API](https://base-ui.com/react/components/toolbar#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Toolbar API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Separator
|
||||
|
||||
`Toolbar.Separator` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Root
|
||||
|
||||
`Toolbar` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Group
|
||||
|
||||
`Toolbar.Group` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Button
|
||||
|
||||
`Toolbar.Button` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Link
|
||||
|
||||
`Toolbar.Link` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Input
|
||||
|
||||
`Toolbar.Input` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,201 @@
|
||||
# Tooltip
|
||||
|
||||
Tooltip wraps Base UI's tooltip primitive with the Tailwind classes from the official Base UI example.
|
||||
|
||||
::example{src="components/tooltip/tooltip.tsx"}
|
||||
|
||||
## Anatomy
|
||||
|
||||
### Base UI Style
|
||||
|
||||
```tsx
|
||||
import Tooltip from "@/components/ui/tooltip"
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger aria-label="Bold">B</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Positioner sideOffset={11}>
|
||||
<Tooltip.Popup>
|
||||
<Tooltip.Arrow />
|
||||
Bold
|
||||
</Tooltip.Popup>
|
||||
</Tooltip.Positioner>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
```
|
||||
|
||||
### Shadcn Style
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
TooltipPopup,
|
||||
TooltipPortal,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
```
|
||||
|
||||
## Composition
|
||||
|
||||
Use the following composition to build `Tooltip`:
|
||||
|
||||
```txt
|
||||
Tooltip
|
||||
├── TooltipTrigger
|
||||
├── TooltipPortal
|
||||
│ └── TooltipPositioner
|
||||
│ └── TooltipPopup
|
||||
│ ├── TooltipArrow
|
||||
│ └── TooltipViewport
|
||||
└── TooltipProvider
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
These examples mirror the usage patterns documented by Base UI for Tooltip.
|
||||
|
||||
### Detached triggers
|
||||
|
||||
A tooltip can be controlled by a trigger located either inside or outside the `<Tooltip.Root>` component. For simple, one-off interactions, place the `<Tooltip.Trigger>` inside `<Tooltip.Root>`, as shown in the example at the top of this page.
|
||||
|
||||
```jsx
|
||||
const demoTooltip = Tooltip.createHandle()
|
||||
|
||||
<Tooltip.Trigger handle={demoTooltip}>Button</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Root handle={demoTooltip}>
|
||||
...
|
||||
</Tooltip.Root>
|
||||
```
|
||||
|
||||
### Multiple triggers
|
||||
|
||||
A single tooltip can be opened by multiple trigger elements. You can achieve this by using the same `handle` for several detached triggers, or by placing multiple `<Tooltip.Trigger>` components inside a single `<Tooltip.Root>`.
|
||||
|
||||
```jsx
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>Trigger 1</Tooltip.Trigger>
|
||||
<Tooltip.Trigger>Trigger 2</Tooltip.Trigger>
|
||||
...
|
||||
</Tooltip.Root>
|
||||
```
|
||||
|
||||
### Controlled mode with multiple triggers
|
||||
|
||||
You can control the tooltip's open state externally using the `open` and `onOpenChange` props on `<Tooltip.Root>`. This allows you to manage the tooltip's visibility based on your application's state. When using multiple triggers, you have to manage which trigger is active with the `triggerId` prop on `<Tooltip.Root>` and the `id` prop on each `<Tooltip.Trigger>`.
|
||||
|
||||
### Animating the Tooltip
|
||||
|
||||
You can animate a tooltip as it moves between different trigger elements. This includes animating its position, size, and content.
|
||||
|
||||
|
||||
### Detached triggers
|
||||
|
||||
::example{src="components/tooltip/detached-triggers.tsx"}
|
||||
|
||||
### Controlled mode with multiple triggers
|
||||
|
||||
::example{src="components/tooltip/controlled-mode-with-multiple-triggers.tsx"}
|
||||
|
||||
### Animating the Tooltip
|
||||
|
||||
::example{src="components/tooltip/animating-the-tooltip.tsx"}
|
||||
|
||||
## API Reference
|
||||
|
||||
Official API: [Base UI Tooltip API](https://base-ui.com/react/components/tooltip#api-reference).
|
||||
|
||||
This wrapper preserves the underlying Base UI Tooltip API. Styled parts forward `className`, `style`, and `render` where Base UI supports them.
|
||||
|
||||
### Root
|
||||
|
||||
`Tooltip` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `defaultValue` / `defaultOpen` | `any` | - | Initial uncontrolled value or open state when supported. |
|
||||
| `value` / `open` | `any` | - | Controlled value or open state when supported. |
|
||||
| `onValueChange` / `onOpenChange` | `function` | - | Called when controlled state changes. |
|
||||
| `children` | `React.ReactNode` | - | Child parts rendered inside the root. |
|
||||
|
||||
### Trigger
|
||||
|
||||
`Tooltip.Trigger` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `disabled` | `boolean` | `false` | Prevents user interaction when supported. |
|
||||
| `onClick` | `function` | - | Called when the element is pressed. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for interactive state. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element. |
|
||||
| `children` | `React.ReactNode` | - | Visible content. |
|
||||
|
||||
### Portal
|
||||
|
||||
`Tooltip.Portal` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `container` | `HTMLElement \| RefObject<HTMLElement>` | `document.body` | Element that receives portal content. |
|
||||
| `keepMounted` | `boolean` | `false` | Keeps children mounted when closed. |
|
||||
| `children` | `React.ReactNode` | - | Floating content. |
|
||||
|
||||
### Positioner
|
||||
|
||||
`Tooltip.Positioner` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `side` | `Side` | - | Preferred side for floating content. |
|
||||
| `align` | `Align` | - | Alignment on the selected side. |
|
||||
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance from the anchor. |
|
||||
| `collisionAvoidance` | `CollisionAvoidance` | - | Controls flip and shift behavior. |
|
||||
| `className` | `string \| (state) => string` | - | Classes for placement state. |
|
||||
|
||||
### Popup
|
||||
|
||||
`Tooltip.Popup` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Arrow
|
||||
|
||||
`Tooltip.Arrow` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Provider
|
||||
|
||||
`Tooltip.Provider` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
|
||||
### Viewport
|
||||
|
||||
`Tooltip.Viewport` exposes the corresponding Base UI part with the wrapper's Tailwind styles.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `className` | `string \| (state) => string` | - | Classes for the part state. |
|
||||
| `style` | `React.CSSProperties \| (state) => React.CSSProperties` | - | Inline style or state-based style. |
|
||||
| `render` | `ReactElement \| (props, state) => ReactElement` | - | Replaces the rendered element when supported. |
|
||||
| `children` | `React.ReactNode` | - | Child content when the part accepts children. |
|
||||
@@ -0,0 +1,60 @@
|
||||
# Forms
|
||||
|
||||
A guide to using Base UI form primitives through these Tailwind wrappers.
|
||||
|
||||
## Naming form controls
|
||||
|
||||
Give form controls a `name` when their value should participate in submission or validation. Field wrappers should preserve the Base UI field structure so labels, descriptions, errors, and controls stay associated.
|
||||
|
||||
```tsx
|
||||
import Field from "@/components/ui/field"
|
||||
import Input from "@/components/ui/input"
|
||||
|
||||
<Field.Root name="serverName">
|
||||
<Field.Label>Server name</Field.Label>
|
||||
<Input required minLength={3} />
|
||||
<Field.Error />
|
||||
</Field.Root>
|
||||
```
|
||||
|
||||
## Building form fields
|
||||
|
||||
Prefer a field root around the control and its label or error text. This keeps the semantic relationship clear and gives styling a predictable container.
|
||||
|
||||
Use native labels for simple controls and Base UI field parts when you need validation messages, descriptions, or grouped form structure.
|
||||
|
||||
## Submitting data
|
||||
|
||||
Base UI form components integrate with native form submission. The `Form` component can also provide submitted values as a JavaScript object through `onFormSubmit`.
|
||||
|
||||
```tsx
|
||||
import Form from "@/components/ui/form"
|
||||
|
||||
<Form
|
||||
onFormSubmit={async (values) => {
|
||||
await save(values)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Constraint validation
|
||||
|
||||
Use native validation attributes such as `required`, `minLength`, `maxLength`, `pattern`, and `step` where possible. Base UI form controls extend the native constraint validation model.
|
||||
|
||||
For composite controls, make sure the hidden input can participate in submission and validation by giving the control a `name`.
|
||||
|
||||
## Custom validation
|
||||
|
||||
When native validation is not enough, use Base UI validation props on field primitives. Keep validation messages close to the field so `Field.Error` can render them consistently.
|
||||
|
||||
## Form libraries
|
||||
|
||||
Base UI integrates with React Hook Form and TanStack Form because the controls expose regular values, refs, and change handlers. Wrappers should forward props rather than hiding these integration points.
|
||||
|
||||
## Source
|
||||
|
||||
Adapted from the official Base UI Forms guide.
|
||||
|
||||
## API Reference
|
||||
|
||||
This page is conceptual documentation. Component-level APIs are documented on individual component pages, and utility APIs are documented under Utils.
|
||||
@@ -0,0 +1,24 @@
|
||||
# llms.txt
|
||||
|
||||
Placeholder for an LLM-oriented docs index.
|
||||
|
||||
## Goal
|
||||
|
||||
This page will become a concise map of the component library once the API, docs structure, and examples are stable.
|
||||
|
||||
## Planned contents
|
||||
|
||||
- Project purpose and installation
|
||||
- Component import conventions
|
||||
- Component list and route map
|
||||
- Utility list
|
||||
- Styling and accessibility rules
|
||||
- Links to source files and examples
|
||||
|
||||
## Status
|
||||
|
||||
Not final yet. Complete this after the component wrappers and docs pages settle.
|
||||
|
||||
## API Reference
|
||||
|
||||
This page is conceptual documentation. Component-level APIs are documented on individual component pages, and utility APIs are documented under Utils.
|
||||
@@ -0,0 +1,64 @@
|
||||
# TypeScript
|
||||
|
||||
A guide to typing wrappers around Base UI components.
|
||||
|
||||
## Namespaces
|
||||
|
||||
Base UI organizes types on component namespaces. Component parts commonly expose `Props` and `State` types.
|
||||
|
||||
```ts
|
||||
import { Tooltip } from "@base-ui/react/tooltip"
|
||||
|
||||
type TooltipRootProps = Tooltip.Root.Props
|
||||
type TooltipRootState = Tooltip.Root.State
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
Wrapper components should accept the underlying Base UI part props and add only styling-related conveniences when needed.
|
||||
|
||||
```tsx
|
||||
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
||||
|
||||
function TooltipRoot(props: TooltipPrimitive.Root.Props) {
|
||||
return <TooltipPrimitive.Root {...props} />
|
||||
}
|
||||
```
|
||||
|
||||
## State
|
||||
|
||||
Some render props receive a state object. Positioner parts, popup parts, and interactive controls may expose state such as `open`, `side`, `align`, or selected state.
|
||||
|
||||
```tsx
|
||||
function renderPositioner(
|
||||
props: Popover.Positioner.Props,
|
||||
state: Popover.Positioner.State,
|
||||
) {
|
||||
return <div data-side={state.side} {...props} />
|
||||
}
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
Custom event detail types are also exported through namespaces. Use these when typing controlled wrappers or callbacks.
|
||||
|
||||
```ts
|
||||
function onOpenChange(
|
||||
open: boolean,
|
||||
details: Combobox.Root.ChangeEventDetails,
|
||||
) {
|
||||
console.log(open, details)
|
||||
}
|
||||
```
|
||||
|
||||
## Other accessible types
|
||||
|
||||
Some components export additional types for imperative actions, complex values, or component-specific objects. Prefer importing those types from the Base UI namespace instead of re-declaring them locally.
|
||||
|
||||
## Source
|
||||
|
||||
Adapted from the official Base UI TypeScript guide.
|
||||
|
||||
## API Reference
|
||||
|
||||
This page is conceptual documentation. Component-level APIs are documented on individual component pages, and utility APIs are documented under Utils.
|
||||
@@ -0,0 +1,27 @@
|
||||
# About
|
||||
|
||||
Base UI is an unstyled React component library for building accessible user interfaces. This project wraps Base UI primitives with Tailwind classes based on the official examples.
|
||||
|
||||
## Headless foundation
|
||||
|
||||
Base UI does not ship CSS or prescribe a styling system. That makes it a good foundation for internal component ownership: behavior comes from Base UI, while styling lives in your app.
|
||||
|
||||
## Accessible primitives
|
||||
|
||||
The primitives are designed around accessibility, keyboard interaction, focus management, and common ARIA patterns. These wrappers keep the primitive parts exposed so accessibility behavior remains visible and configurable.
|
||||
|
||||
## Composable parts
|
||||
|
||||
Component APIs are open. You can assemble, omit, or wrap individual parts such as `Root`, `Trigger`, `Portal`, `Positioner`, `Popup`, `Item`, and `Arrow`.
|
||||
|
||||
## Why this wrapper exists
|
||||
|
||||
This library is intentionally source-first. It provides styled component folders that can be copied into an app, inspected, changed, and owned internally without waiting on a package release process.
|
||||
|
||||
## Source
|
||||
|
||||
Adapted from the official Base UI About page.
|
||||
|
||||
## API Reference
|
||||
|
||||
This page is conceptual documentation. Component-level APIs are documented on individual component pages, and utility APIs are documented under Utils.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Accessibility
|
||||
|
||||
Base UI focuses on accessible primitives. These wrappers keep the Base UI structure intact so keyboard behavior, ARIA attributes, focus management, and interaction state continue to come from the underlying library.
|
||||
|
||||
## Keyboard navigation
|
||||
|
||||
Base UI components follow WAI-ARIA authoring patterns where applicable. Components commonly handle arrow keys, typeahead, `Home`, `End`, `Enter`, and `Esc` depending on the interaction.
|
||||
|
||||
When wrapping a component, keep the Base UI part responsible for behavior in place. For example, keep `Trigger`, `Popup`, `Item`, and `Field` parts wired as documented instead of replacing them with inert elements.
|
||||
|
||||
## Focus management
|
||||
|
||||
Many overlay components manage focus when they open and close. Some parts expose focus-related props such as `initialFocus` or `finalFocus`.
|
||||
|
||||
Styling remains your responsibility. Make sure focused controls have a visible `focus-visible` state in both light and dark themes.
|
||||
|
||||
## Color contrast
|
||||
|
||||
These wrappers use the official Base UI example styling as the baseline. When adapting colors, check foreground and background contrast for all text, icons, borders, selected states, and disabled states.
|
||||
|
||||
## Accessible labels
|
||||
|
||||
Form controls need accessible names. Prefer visible labels through Base UI field parts, native `<label>` elements, or component-specific label parts such as `Select.Label`.
|
||||
|
||||
Use `aria-label` or `aria-labelledby` for icon-only controls and custom triggers when there is no visible label.
|
||||
|
||||
## Testing
|
||||
|
||||
Check keyboard navigation, focus order, screen-reader labels, and contrast after changing component structure or classes. The wrapper should preserve Base UI behavior while only changing presentation.
|
||||
|
||||
## Source
|
||||
|
||||
Adapted from the official Base UI Accessibility guide.
|
||||
|
||||
## API Reference
|
||||
|
||||
This page is conceptual documentation. Component-level APIs are documented on individual component pages, and utility APIs are documented under Utils.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Quick start
|
||||
|
||||
A quick guide to using these Base UI Tailwind components inside an app.
|
||||
|
||||
## Install the library
|
||||
|
||||
Install Base UI and the small styling utilities used by these wrappers.
|
||||
|
||||
```bash
|
||||
npm install @base-ui/react clsx tailwind-merge lucide-react
|
||||
```
|
||||
|
||||
## Set up Tailwind
|
||||
|
||||
Include copied component files in Tailwind's content paths so their classes are generated.
|
||||
|
||||
```ts
|
||||
import type { Config } from "tailwindcss"
|
||||
|
||||
export default {
|
||||
darkMode: "class",
|
||||
content: ["./src/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config
|
||||
```
|
||||
|
||||
## Portals
|
||||
|
||||
Base UI renders floating UI like dialogs, popups, menus, and tooltips in portals. Add `isolation: isolate` to the app root so portaled content can stack above the rest of the application without local z-index conflicts.
|
||||
|
||||
```css
|
||||
#root {
|
||||
isolation: isolate;
|
||||
}
|
||||
```
|
||||
|
||||
Use `CSPProvider` only when your Content Security Policy needs a nonce for Base UI's injected style or script elements. It does not replace the root isolation rule.
|
||||
|
||||
## Assemble a component
|
||||
|
||||
Each component exposes styled Base UI parts. Import the namespace and compose the same parts you would use from Base UI.
|
||||
|
||||
```tsx
|
||||
import Dialog from "@/components/ui/dialog"
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>Open</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Popup>
|
||||
<Dialog.Title>Notifications</Dialog.Title>
|
||||
<Dialog.Description>You are all caught up. Good job!</Dialog.Description>
|
||||
<Dialog.Close>Close</Dialog.Close>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
```
|
||||
|
||||
## Import styles
|
||||
|
||||
The default export follows the Base UI namespace style, while named exports support a flatter shadcn-style composition.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger>Open</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>Title</DialogTitle>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
## Source ownership
|
||||
|
||||
The component folders are intended to be copied into an app and owned there. Every styled part accepts `className`, and composite components are split by Base UI part so you can replace or remove pieces without fighting an abstraction.
|
||||
|
||||
## Working with LLMs
|
||||
|
||||
Base UI publishes Markdown versions of its docs and an `llms.txt` entry for AI tools. This project keeps the same idea: docs pages should be readable as source and easy to share with an assistant while you adapt components internally.
|
||||
|
||||
## Next steps
|
||||
|
||||
Read Accessibility for interaction requirements, Forms for validation patterns, and TypeScript for wrapper typing conventions.
|
||||
|
||||
## API Reference
|
||||
|
||||
This page is conceptual documentation. Component-level APIs are documented on individual component pages, and utility APIs are documented under Utils.
|
||||
@@ -0,0 +1,28 @@
|
||||
# cn
|
||||
|
||||
Utility for merging conditional class names with Tailwind conflict resolution.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
cn("inline-flex items-center", isActive && "bg-neutral-100")
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
```ts
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### cn
|
||||
|
||||
`cn(...inputs)` accepts any `clsx` class value and returns a Tailwind-merged class string. Later conflicting Tailwind utilities take precedence through `tailwind-merge`.
|
||||
@@ -0,0 +1,69 @@
|
||||
# CSP Provider
|
||||
|
||||
Configures CSP-related behavior for inline tags rendered by Base UI components.
|
||||
|
||||
## Anatomy
|
||||
|
||||
Import the provider and wrap it around the app or a subtree of Base UI components.
|
||||
|
||||
```tsx
|
||||
import { CSPProvider } from "@base-ui/react/csp-provider"
|
||||
|
||||
<CSPProvider nonce="...">
|
||||
<App />
|
||||
</CSPProvider>
|
||||
```
|
||||
|
||||
Some Base UI components render inline `<style>` or `<script>` tags for behavior such as scrollbar handling or pre-hydration behavior. Under a strict Content Security Policy, those tags may need a matching nonce.
|
||||
|
||||
## Supplying a Nonce
|
||||
|
||||
Generate a nonce per request, include it in the CSP header, and pass the same value to `CSPProvider`.
|
||||
|
||||
```ts
|
||||
const nonce = crypto.randomUUID()
|
||||
|
||||
const csp = [
|
||||
"default-src 'self'",
|
||||
`script-src 'self' 'nonce-${nonce}'`,
|
||||
`style-src-elem 'self' 'nonce-${nonce}'`,
|
||||
].join("; ")
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { CSPProvider } from "@base-ui/react/csp-provider"
|
||||
|
||||
function App({ nonce }: { nonce: string }) {
|
||||
return <CSPProvider nonce={nonce}>{/* app */}</CSPProvider>
|
||||
}
|
||||
```
|
||||
|
||||
## Disable Inline Style Elements
|
||||
|
||||
If your app provides the required scrollbar CSS globally, `disableStyleElements` can prevent Base UI from rendering inline style elements.
|
||||
|
||||
```tsx
|
||||
<CSPProvider disableStyleElements>
|
||||
<App />
|
||||
</CSPProvider>
|
||||
```
|
||||
|
||||
## Inline Style Attributes
|
||||
|
||||
`CSPProvider` covers inline `<style>` and `<script>` elements rendered by Base UI. It does not remove inline `style` attributes. If your CSP blocks style attributes, prefer external CSS, client-only rendering for affected parts, or explicitly unset inline styles where appropriate.
|
||||
|
||||
## API Reference
|
||||
|
||||
### CSPProvider
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `nonce` | `string` | - | Nonce applied to inline `<style>` and `<script>` tags rendered by Base UI. |
|
||||
| `disableStyleElements` | `boolean` | `false` | Prevents Base UI from rendering inline `<style>` elements. |
|
||||
| `children` | `React.ReactNode` | - | The subtree that receives the CSP configuration. |
|
||||
|
||||
Exported types include `CSPProvider.Props` and `CSPProvider.State`.
|
||||
|
||||
## Source
|
||||
|
||||
Adapted from the official Base UI CSP Provider docs.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Direction Provider
|
||||
|
||||
Enables RTL-aware behavior for Base UI components.
|
||||
|
||||
## Anatomy
|
||||
|
||||
Wrap the app or a subtree with `DirectionProvider`.
|
||||
|
||||
```tsx
|
||||
import { DirectionProvider } from "@base-ui/react/direction-provider"
|
||||
|
||||
<DirectionProvider direction="rtl">
|
||||
<App />
|
||||
</DirectionProvider>
|
||||
```
|
||||
|
||||
`DirectionProvider` affects Base UI component behavior, but it does not set HTML or CSS direction by itself. Set `dir="rtl"` or CSS `direction: rtl` in your application as well.
|
||||
|
||||
```tsx
|
||||
<div dir="rtl">
|
||||
<DirectionProvider direction="rtl">
|
||||
<Slider.Root defaultValue={25} />
|
||||
</DirectionProvider>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Reading Direction
|
||||
|
||||
Use `useDirection` when a component needs to read the current text direction, especially when working with portaled content that may render outside a DOM subtree with `dir`.
|
||||
|
||||
```tsx
|
||||
import { useDirection } from "@base-ui/react/direction-provider"
|
||||
|
||||
const direction = useDirection()
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### DirectionProvider
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `direction` | `"ltr" \| "rtl"` | `"ltr"` | Reading direction for Base UI component behavior. |
|
||||
| `children` | `React.ReactNode` | - | The subtree that receives the direction configuration. |
|
||||
|
||||
Exported types include `DirectionProvider.Props`, `DirectionProvider.State`, and `TextDirection`.
|
||||
|
||||
### useDirection
|
||||
|
||||
Returns the current `TextDirection`.
|
||||
|
||||
```ts
|
||||
type TextDirection = "ltr" | "rtl"
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
Adapted from the official Base UI Direction Provider docs.
|
||||
@@ -0,0 +1,68 @@
|
||||
# mergeProps
|
||||
|
||||
A utility for merging multiple sets of React props.
|
||||
|
||||
## How Merging Works
|
||||
|
||||
`mergeProps` combines prop objects so wrapper components can safely combine internal behavior with user-provided props.
|
||||
|
||||
```tsx
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
|
||||
const props = mergeProps(
|
||||
{ className: "Button", onClick: handleInternalClick },
|
||||
{ className: "primary", onClick: handleUserClick },
|
||||
)
|
||||
```
|
||||
|
||||
For most props, the rightmost value wins. Special handling is applied for `className`, `style`, and event handlers.
|
||||
|
||||
## Event Handlers
|
||||
|
||||
Event handlers are composed. Base UI also supports `event.preventBaseUIHandler()` for React synthetic events, which prevents Base UI's internal handler from running without calling `preventDefault()` or `stopPropagation()`.
|
||||
|
||||
```tsx
|
||||
mergeProps(props, {
|
||||
onClick(event) {
|
||||
event.preventBaseUIHandler()
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Class Names and Styles
|
||||
|
||||
`className` values are concatenated. `style` objects are merged, with later values taking precedence for the same property.
|
||||
|
||||
## Function Form
|
||||
|
||||
When using Base UI's `render` callback, props are not merged automatically. Use `mergeProps` to combine the received props with your own element props.
|
||||
|
||||
```tsx
|
||||
render={(props) => (
|
||||
<button
|
||||
{...mergeProps(props, {
|
||||
className: "Button",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### mergeProps
|
||||
|
||||
Merges two or more prop objects.
|
||||
|
||||
```ts
|
||||
mergeProps<TElement>(...props: Array<Record<string, unknown>>)
|
||||
```
|
||||
|
||||
### mergePropsN
|
||||
|
||||
Lower-level helper used when merging a known number of prop objects.
|
||||
|
||||
Use `mergeProps` for most wrapper code.
|
||||
|
||||
## Source
|
||||
|
||||
Adapted from the official Base UI mergeProps docs.
|
||||
@@ -0,0 +1,87 @@
|
||||
# useRender
|
||||
|
||||
Hook for enabling a `render` prop in custom components.
|
||||
|
||||
## Examples
|
||||
|
||||
`useRender` lets a component expose a Base UI-style `render` prop so consumers can override the default rendered element.
|
||||
|
||||
```tsx
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
|
||||
interface TextProps extends useRender.ComponentProps<"p"> {}
|
||||
|
||||
function Text({ render, ...props }: TextProps) {
|
||||
return useRender({
|
||||
defaultTagName: "p",
|
||||
render,
|
||||
props: mergeProps<"p">({ className: "Text" }, props),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Merging Props
|
||||
|
||||
Use `mergeProps` with `useRender` so internal props and consumer props compose correctly.
|
||||
|
||||
```tsx
|
||||
const element = useRender({
|
||||
defaultTagName: "button",
|
||||
render,
|
||||
props: mergeProps<"button">(
|
||||
{ type: "button", className: "Button" },
|
||||
props,
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
## Merging Refs
|
||||
|
||||
Pass `ref` to `useRender` when the component needs to merge internal refs with a consumer ref.
|
||||
|
||||
## TypeScript
|
||||
|
||||
Use `useRender.ComponentProps` for public props and `useRender.ElementProps` for internal element props.
|
||||
|
||||
```ts
|
||||
interface ButtonProps extends useRender.ComponentProps<"button"> {}
|
||||
|
||||
const defaultProps: useRender.ElementProps<"button"> = {
|
||||
type: "button",
|
||||
}
|
||||
```
|
||||
|
||||
## Render Prop and Polymorphism
|
||||
|
||||
The `render` prop is primarily for composing behavior and props. Rendering a different tag can require extra care because some default props only make sense for the default element.
|
||||
|
||||
## API Reference
|
||||
|
||||
### useRender
|
||||
|
||||
```ts
|
||||
const element = useRender({
|
||||
defaultTagName,
|
||||
render,
|
||||
props,
|
||||
state,
|
||||
ref,
|
||||
enabled,
|
||||
stateAttributesMapping,
|
||||
})
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `defaultTagName` | intrinsic element name | Default element used when `render` is not provided. |
|
||||
| `render` | `ReactElement | function` | Overrides the rendered element. |
|
||||
| `props` | object | Props spread onto the rendered element and merged with render props. |
|
||||
| `state` | object | State passed to render callbacks and converted to data attributes. |
|
||||
| `ref` | React ref or ref array | Ref or refs applied to the rendered element. |
|
||||
| `enabled` | `boolean` | When false, skips rendering logic and returns `null`. |
|
||||
| `stateAttributesMapping` | object | Custom mapping from state values to data attributes. |
|
||||
|
||||
## Source
|
||||
|
||||
Adapted from the official Base UI useRender docs.
|
||||
@@ -0,0 +1,69 @@
|
||||
// @ts-nocheck
|
||||
import * as React from 'react';
|
||||
import Accordion from '@/components/ui/accordion';
|
||||
|
||||
export default function ExampleAccordion() {
|
||||
return (
|
||||
<Accordion.Root className="flex w-full max-w-80 flex-col border border-neutral-950 text-neutral-950 dark:border-white dark:text-white">
|
||||
<Accordion.Item>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger>
|
||||
What is Base UI?
|
||||
<PlusIcon className="shrink-0 transition-transform duration-100 ease-[ease-out] group-data-panel-open:rotate-45" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Panel>
|
||||
<div className="px-3 py-2">
|
||||
Base UI is a library of high-quality unstyled React components for design systems and
|
||||
web apps.
|
||||
</div>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item className="border-t border-neutral-950 dark:border-white">
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger>
|
||||
How do I get started?
|
||||
<PlusIcon className="shrink-0 transition-transform duration-100 ease-[ease-out] group-data-panel-open:rotate-45" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Panel>
|
||||
<div className="px-3 py-2">
|
||||
Head to the “Quick start” guide in the docs. If you’ve used unstyled libraries before,
|
||||
you’ll feel at home.
|
||||
</div>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item className="border-t border-neutral-950 dark:border-white">
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger>
|
||||
Can I use it for my project?
|
||||
<PlusIcon className="shrink-0 transition-transform duration-100 ease-[ease-out] group-data-panel-open:rotate-45" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Panel>
|
||||
<div className="px-3 py-2">Of course! Base UI is free and open source.</div>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function PlusIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="M1.5 8h13M8 14.5v-13" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// @ts-nocheck
|
||||
import * as React from 'react';
|
||||
import Accordion from '@/components/ui/accordion';
|
||||
|
||||
export default function ExampleAccordion() {
|
||||
return (
|
||||
<Accordion.Root
|
||||
multiple
|
||||
className="flex w-full max-w-80 flex-col border border-neutral-950 text-neutral-950 dark:border-white dark:text-white"
|
||||
>
|
||||
<Accordion.Item>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger>
|
||||
What is Base UI?
|
||||
<PlusIcon className="shrink-0 transition-transform duration-100 ease-[ease-out] group-data-panel-open:rotate-45" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Panel>
|
||||
<div className="px-3 py-2">
|
||||
Base UI is a library of high-quality unstyled React components for design systems and
|
||||
web apps.
|
||||
</div>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item className="border-t border-neutral-950 dark:border-white">
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger>
|
||||
How do I get started?
|
||||
<PlusIcon className="shrink-0 transition-transform duration-100 ease-[ease-out] group-data-panel-open:rotate-45" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Panel>
|
||||
<div className="px-3 py-2">
|
||||
Head to the “Quick start” guide in the docs. If you’ve used unstyled libraries before,
|
||||
you’ll feel at home.
|
||||
</div>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item className="border-t border-neutral-950 dark:border-white">
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger>
|
||||
Can I use it for my project?
|
||||
<PlusIcon className="shrink-0 transition-transform duration-100 ease-[ease-out] group-data-panel-open:rotate-45" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Panel>
|
||||
<div className="px-3 py-2">Of course! Base UI is free and open source.</div>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function PlusIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="M1.5 8h13M8 14.5v-13" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// @ts-nocheck
|
||||
import { Plus } from "lucide-react"
|
||||
import Accordion from "@/components/ui/accordion"
|
||||
|
||||
const accordionItems = [
|
||||
{
|
||||
title: "What is Base UI?",
|
||||
content:
|
||||
"Base UI is a library of high-quality unstyled React components for design systems and web apps.",
|
||||
},
|
||||
{
|
||||
title: "How do I get started?",
|
||||
content:
|
||||
"Head to the Quick start guide in the docs. If you have used unstyled libraries before, you will feel at home.",
|
||||
},
|
||||
{
|
||||
title: "Can I use it for my project?",
|
||||
content: "Of course. Base UI is free and open source.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function AccordionSinglePanelExample() {
|
||||
return (
|
||||
<Accordion.Root>
|
||||
{accordionItems.map((item, index) => (
|
||||
<Accordion.Item
|
||||
key={item.title}
|
||||
className={index === 0 ? "" : "border-t border-neutral-950 dark:border-white"}
|
||||
>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger>
|
||||
{item.title}
|
||||
<Plus className="size-4 shrink-0 transition-transform duration-100 ease-[ease-out] group-data-[panel-open]:rotate-45" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Panel>
|
||||
<div className="px-3 py-2">{item.content}</div>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion.Root>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// @ts-nocheck
|
||||
import AlertDialog from '@/components/ui/alert-dialog';
|
||||
|
||||
const dangerButtonClasses = 'text-red-700 dark:text-red-400';
|
||||
|
||||
export default function ExampleAlertDialog() {
|
||||
return (
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger className={dangerButtonClasses}>Discard draft</AlertDialog.Trigger>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Backdrop />
|
||||
<AlertDialog.Popup>
|
||||
<div className="flex flex-col gap-1">
|
||||
<AlertDialog.Title>Discard draft?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
You can’t undo this action.
|
||||
</AlertDialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<AlertDialog.Close>Cancel</AlertDialog.Close>
|
||||
<AlertDialog.Close className={dangerButtonClasses}>Discard</AlertDialog.Close>
|
||||
</div>
|
||||
</AlertDialog.Popup>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import AlertDialog from '@/components/ui/alert-dialog';
|
||||
import Button from '@/components/ui/button';
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
|
||||
export default function ExampleDialog() {
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const [confirmationOpen, setConfirmationOpen] = React.useState(false);
|
||||
const [textareaValue, setTextareaValue] = React.useState('');
|
||||
const titleId = React.useId();
|
||||
|
||||
return (
|
||||
<Dialog.Root
|
||||
open={dialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
// Show the close confirmation if there’s text in the textarea
|
||||
if (!open && textareaValue) {
|
||||
setConfirmationOpen(true);
|
||||
} else {
|
||||
// Reset the text area value
|
||||
setTextareaValue('');
|
||||
// Open or close the dialog normally
|
||||
setDialogOpen(open);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog.Trigger>Tweet</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Popup className="fixed top-[calc(50%+1.25rem*var(--nested-dialogs))] left-1/2 -mt-8 flex w-96 max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-1 scale-[calc(1-0.1*var(--nested-dialogs))] bg-white dark:bg-neutral-950 p-4 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-[top,transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out after:absolute after:inset-0 after:bg-black/5 after:opacity-0 after:transition-opacity after:duration-100 after:ease-out after:pointer-events-none data-ending-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-ending-style:scale-[0.96] data-ending-style:opacity-0 data-nested-dialog-open:after:opacity-100 data-starting-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-starting-style:scale-[0.96] data-starting-style:opacity-0">
|
||||
<Dialog.Title id={titleId}>New tweet</Dialog.Title>
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
// Close the dialog when submitting
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
aria-labelledby={titleId}
|
||||
required
|
||||
className="min-h-32 w-full border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 p-2 text-sm any-pointer-coarse:text-base font-normal text-neutral-950 dark:text-white placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white"
|
||||
placeholder="What’s on your mind?"
|
||||
value={textareaValue}
|
||||
onChange={(event) => setTextareaValue(event.target.value)}
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Dialog.Close>Cancel</Dialog.Close>
|
||||
<Button type="submit">Tweet</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
|
||||
{/* Confirmation dialog */}
|
||||
<AlertDialog.Root open={confirmationOpen} onOpenChange={setConfirmationOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Popup className="fixed top-[calc(50%+1.25rem*var(--nested-dialogs))] left-1/2 -mt-8 flex w-96 max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 scale-[calc(1-0.1*var(--nested-dialogs))] bg-white dark:bg-neutral-950 p-4 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-[top,transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out after:absolute after:inset-0 after:bg-black/5 after:opacity-0 after:transition-opacity after:duration-100 after:ease-out after:pointer-events-none data-ending-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-ending-style:scale-[0.96] data-ending-style:opacity-0 data-nested-dialog-open:after:opacity-100 data-starting-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-starting-style:scale-[0.96] data-starting-style:opacity-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<AlertDialog.Title>Discard tweet?</AlertDialog.Title>
|
||||
<AlertDialog.Description>Your tweet will be lost.</AlertDialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<AlertDialog.Close>Go back</AlertDialog.Close>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setConfirmationOpen(false);
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDialog.Popup>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import AlertDialog from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
type AlertPayload = { message: string };
|
||||
|
||||
const demoAlertDialog = AlertDialog.createHandle<AlertPayload>();
|
||||
|
||||
const dangerButtonClasses = 'text-red-700 dark:text-red-400';
|
||||
|
||||
export default function AlertDialogDetachedTriggersControlledDemo() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [triggerId, setTriggerId] = React.useState<string | null>(null);
|
||||
|
||||
const handleOpenChange = (isOpen: boolean, eventDetails: AlertDialog.Root.ChangeEventDetails) => {
|
||||
setOpen(isOpen);
|
||||
setTriggerId(eventDetails.trigger?.id ?? null);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
<AlertDialog.Trigger
|
||||
className={dangerButtonClasses}
|
||||
handle={demoAlertDialog}
|
||||
id="alert-trigger-1"
|
||||
payload={{ message: 'Discard draft?' }}
|
||||
>
|
||||
Discard
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
<AlertDialog.Trigger
|
||||
className={dangerButtonClasses}
|
||||
handle={demoAlertDialog}
|
||||
id="alert-trigger-2"
|
||||
payload={{ message: 'Delete project?' }}
|
||||
>
|
||||
Delete
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
<AlertDialog.Trigger
|
||||
handle={demoAlertDialog}
|
||||
id="alert-trigger-3"
|
||||
payload={{ message: 'Sign out?' }}
|
||||
>
|
||||
Sign out
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTriggerId('alert-trigger-2');
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
Open programmatically
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AlertDialog.Root<AlertPayload>
|
||||
handle={demoAlertDialog}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
triggerId={triggerId}
|
||||
>
|
||||
{({ payload }) => (
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Backdrop />
|
||||
<AlertDialog.Popup>
|
||||
<div className="flex flex-col gap-1">
|
||||
<AlertDialog.Title>
|
||||
{payload?.message ?? 'Are you sure?'}
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This action cannot be undone.
|
||||
</AlertDialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<AlertDialog.Close>Cancel</AlertDialog.Close>
|
||||
<AlertDialog.Close className={dangerButtonClasses}>Confirm</AlertDialog.Close>
|
||||
</div>
|
||||
</AlertDialog.Popup>
|
||||
</AlertDialog.Portal>
|
||||
)}
|
||||
</AlertDialog.Root>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import AlertDialog from '@/components/ui/alert-dialog';
|
||||
|
||||
const demoAlertDialog = AlertDialog.createHandle();
|
||||
|
||||
const dangerButtonClasses = 'text-red-700 dark:text-red-400';
|
||||
|
||||
export default function AlertDialogDetachedTriggersSimpleDemo() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AlertDialog.Trigger className={dangerButtonClasses} handle={demoAlertDialog}>
|
||||
Discard draft
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
<AlertDialog.Root handle={demoAlertDialog}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Backdrop />
|
||||
<AlertDialog.Popup>
|
||||
<div className="flex flex-col gap-1">
|
||||
<AlertDialog.Title>Discard draft?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This action cannot be undone.
|
||||
</AlertDialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<AlertDialog.Close>Cancel</AlertDialog.Close>
|
||||
<AlertDialog.Close className={dangerButtonClasses}>Discard</AlertDialog.Close>
|
||||
</div>
|
||||
</AlertDialog.Popup>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
|
||||
export default function ExampleAsyncAutocomplete() {
|
||||
const [searchValue, setSearchValue] = React.useState('');
|
||||
const [searchResults, setSearchResults] = React.useState<Movie[]>([]);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
|
||||
const { contains } = Autocomplete.useFilter();
|
||||
|
||||
const abortControllerRef = React.useRef<AbortController | null>(null);
|
||||
|
||||
function getStatus(): React.ReactNode | null {
|
||||
if (isPending) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span
|
||||
className="inline-block size-3 animate-spin rounded-full border border-current border-r-transparent rtl:border-r-current rtl:border-l-transparent"
|
||||
aria-hidden
|
||||
/>
|
||||
Searching…
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (searchValue === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (searchResults.length === 0) {
|
||||
return `Movie or year "${searchValue}" does not exist in the Top 100 IMDb movies`;
|
||||
}
|
||||
|
||||
return `${searchResults.length} result${searchResults.length === 1 ? '' : 's'} found`;
|
||||
}
|
||||
|
||||
const status = getStatus();
|
||||
|
||||
return (
|
||||
<Autocomplete.Root
|
||||
items={searchResults}
|
||||
value={searchValue}
|
||||
onValueChange={(nextSearchValue) => {
|
||||
setSearchValue(nextSearchValue);
|
||||
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current?.abort();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
if (nextSearchValue === '') {
|
||||
setSearchResults([]);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
setError(null);
|
||||
|
||||
const result = await searchMovies(nextSearchValue, contains);
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
setSearchResults(result.movies);
|
||||
setError(result.error);
|
||||
});
|
||||
});
|
||||
}}
|
||||
itemToStringValue={(item) => item.title}
|
||||
filter={null}
|
||||
>
|
||||
<label className="flex flex-col gap-1 text-sm font-bold text-neutral-950 dark:text-white">
|
||||
Search movies by name or year
|
||||
<Autocomplete.Input placeholder="e.g. Pulp Fiction or 1994" />
|
||||
</label>
|
||||
|
||||
<Autocomplete.Portal hidden={!status}>
|
||||
<Autocomplete.Positioner sideOffset={4} align="start">
|
||||
<Autocomplete.Popup aria-busy={isPending || undefined}>
|
||||
<div className="max-h-[min(var(--available-height),22.5rem)] overflow-y-auto overscroll-contain py-1 scroll-pt-1 scroll-pb-1">
|
||||
<Autocomplete.Status>
|
||||
{status && (
|
||||
<div className="flex items-center gap-2 py-1 pr-8 pl-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{status}
|
||||
</div>
|
||||
)}
|
||||
</Autocomplete.Status>
|
||||
<Autocomplete.List>
|
||||
{(movie: Movie) => (
|
||||
<Autocomplete.Item key={movie.id} className="group" value={movie}>
|
||||
<span className="flex w-full flex-col gap-1">
|
||||
<span className="font-bold leading-5">{movie.title}</span>
|
||||
<span className="text-sm leading-4 text-neutral-500 dark:text-neutral-400 group-data-highlighted:text-neutral-300 dark:group-data-highlighted:text-neutral-500">
|
||||
{movie.year}
|
||||
</span>
|
||||
</span>
|
||||
</Autocomplete.Item>
|
||||
)}
|
||||
</Autocomplete.List>
|
||||
</div>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
);
|
||||
}
|
||||
|
||||
async function searchMovies(
|
||||
query: string,
|
||||
filter: (item: string, query: string) => boolean,
|
||||
): Promise<{ movies: Movie[]; error: string | null }> {
|
||||
// Simulate network delay
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, Math.random() * 500 + 100);
|
||||
});
|
||||
|
||||
// Simulate occasional network errors (1% chance)
|
||||
if (Math.random() < 0.01 || query === 'will_error') {
|
||||
return {
|
||||
movies: [],
|
||||
error: 'Failed to fetch movies. Please try again.',
|
||||
};
|
||||
}
|
||||
|
||||
const movies = top100Movies.filter(
|
||||
(movie) => filter(movie.title, query) || filter(movie.year.toString(), query),
|
||||
);
|
||||
|
||||
return {
|
||||
movies,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
interface Movie {
|
||||
id: string;
|
||||
title: string;
|
||||
year: number;
|
||||
}
|
||||
|
||||
const top100Movies: Movie[] = [
|
||||
{ id: '1', title: 'The Shawshank Redemption', year: 1994 },
|
||||
{ id: '2', title: 'The Godfather', year: 1972 },
|
||||
{ id: '3', title: 'The Dark Knight', year: 2008 },
|
||||
{ id: '4', title: 'The Godfather Part II', year: 1974 },
|
||||
{ id: '5', title: '12 Angry Men', year: 1957 },
|
||||
{ id: '6', title: 'The Lord of the Rings: The Return of the King', year: 2003 },
|
||||
{ id: '7', title: "Schindler's List", year: 1993 },
|
||||
{ id: '8', title: 'Pulp Fiction', year: 1994 },
|
||||
{ id: '9', title: 'The Lord of the Rings: The Fellowship of the Ring', year: 2001 },
|
||||
{ id: '10', title: 'The Good, the Bad and the Ugly', year: 1966 },
|
||||
{ id: '11', title: 'Forrest Gump', year: 1994 },
|
||||
{ id: '12', title: 'The Lord of the Rings: The Two Towers', year: 2002 },
|
||||
{ id: '13', title: 'Fight Club', year: 1999 },
|
||||
{ id: '14', title: 'Inception', year: 2010 },
|
||||
{ id: '15', title: 'Star Wars: Episode V – The Empire Strikes Back', year: 1980 },
|
||||
{ id: '16', title: 'The Matrix', year: 1999 },
|
||||
{ id: '17', title: 'Goodfellas', year: 1990 },
|
||||
{ id: '18', title: 'Interstellar', year: 2014 },
|
||||
{ id: '19', title: "One Flew Over the Cuckoo's Nest", year: 1975 },
|
||||
{ id: '20', title: 'Se7en', year: 1995 },
|
||||
{ id: '21', title: "It's a Wonderful Life", year: 1946 },
|
||||
{ id: '22', title: 'The Silence of the Lambs', year: 1991 },
|
||||
{ id: '23', title: 'Seven Samurai', year: 1954 },
|
||||
{ id: '24', title: 'Saving Private Ryan', year: 1998 },
|
||||
{ id: '25', title: 'City of God', year: 2002 },
|
||||
{ id: '26', title: 'Life Is Beautiful', year: 1997 },
|
||||
{ id: '27', title: 'The Green Mile', year: 1999 },
|
||||
{ id: '28', title: 'Star Wars: Episode IV – A New Hope', year: 1977 },
|
||||
{ id: '29', title: 'Terminator 2: Judgment Day', year: 1991 },
|
||||
{ id: '30', title: 'Back to the Future', year: 1985 },
|
||||
{ id: '31', title: 'Spirited Away', year: 2001 },
|
||||
{ id: '32', title: 'The Pianist', year: 2002 },
|
||||
{ id: '33', title: 'Psycho', year: 1960 },
|
||||
{ id: '34', title: 'Parasite', year: 2019 },
|
||||
{ id: '35', title: 'Gladiator', year: 2000 },
|
||||
{ id: '36', title: 'Léon: The Professional', year: 1994 },
|
||||
{ id: '37', title: 'American History X', year: 1998 },
|
||||
{ id: '38', title: 'The Departed', year: 2006 },
|
||||
{ id: '39', title: 'Whiplash', year: 2014 },
|
||||
{ id: '40', title: 'The Prestige', year: 2006 },
|
||||
{ id: '41', title: 'Grave of the Fireflies', year: 1988 },
|
||||
{ id: '42', title: 'The Usual Suspects', year: 1995 },
|
||||
{ id: '43', title: 'Casablanca', year: 1942 },
|
||||
{ id: '44', title: 'Harakiri', year: 1962 },
|
||||
{ id: '45', title: 'The Lion King', year: 1994 },
|
||||
{ id: '46', title: 'The Intouchables', year: 2011 },
|
||||
{ id: '47', title: 'Modern Times', year: 1936 },
|
||||
{ id: '48', title: 'The Lives of Others', year: 2006 },
|
||||
{ id: '49', title: 'Once Upon a Time in the West', year: 1968 },
|
||||
{ id: '50', title: 'Rear Window', year: 1954 },
|
||||
{ id: '51', title: 'Alien', year: 1979 },
|
||||
{ id: '52', title: 'City Lights', year: 1931 },
|
||||
{ id: '53', title: 'The Shining', year: 1980 },
|
||||
{ id: '54', title: 'Cinema Paradiso', year: 1988 },
|
||||
{ id: '55', title: 'Avengers: Infinity War', year: 2018 },
|
||||
{ id: '56', title: 'Paths of Glory', year: 1957 },
|
||||
{ id: '57', title: 'Django Unchained', year: 2012 },
|
||||
{ id: '58', title: 'WALL·E', year: 2008 },
|
||||
{ id: '59', title: 'Sunset Boulevard', year: 1950 },
|
||||
{ id: '60', title: 'The Great Dictator', year: 1940 },
|
||||
{ id: '61', title: 'The Dark Knight Rises', year: 2012 },
|
||||
{ id: '62', title: 'Princess Mononoke', year: 1997 },
|
||||
{ id: '63', title: 'Witness for the Prosecution', year: 1957 },
|
||||
{ id: '64', title: 'Oldboy', year: 2003 },
|
||||
{ id: '65', title: 'Aliens', year: 1986 },
|
||||
{ id: '66', title: 'Once Upon a Time in America', year: 1984 },
|
||||
{ id: '67', title: 'Coco', year: 2017 },
|
||||
{ id: '68', title: 'Your Name.', year: 2016 },
|
||||
{ id: '69', title: 'American Beauty', year: 1999 },
|
||||
{ id: '70', title: 'Braveheart', year: 1995 },
|
||||
{ id: '71', title: 'Das Boot', year: 1981 },
|
||||
{ id: '72', title: '3 Idiots', year: 2009 },
|
||||
{ id: '73', title: 'Toy Story', year: 1995 },
|
||||
{ id: '74', title: 'Inglourious Basterds', year: 2009 },
|
||||
{ id: '75', title: 'High and Low', year: 1963 },
|
||||
{ id: '76', title: 'Amadeus', year: 1984 },
|
||||
{ id: '77', title: 'Good Will Hunting', year: 1997 },
|
||||
{ id: '78', title: 'Star Wars: Episode VI – Return of the Jedi', year: 1983 },
|
||||
{ id: '79', title: 'The Hunt', year: 2012 },
|
||||
{ id: '80', title: 'Capharnaüm', year: 2018 },
|
||||
{ id: '81', title: 'Reservoir Dogs', year: 1992 },
|
||||
{ id: '82', title: 'Eternal Sunshine of the Spotless Mind', year: 2004 },
|
||||
{ id: '83', title: 'Requiem for a Dream', year: 2000 },
|
||||
{ id: '84', title: 'Come and See', year: 1985 },
|
||||
{ id: '85', title: 'Ikiru', year: 1952 },
|
||||
{ id: '86', title: 'Vertigo', year: 1958 },
|
||||
{ id: '87', title: 'Lawrence of Arabia', year: 1962 },
|
||||
{ id: '88', title: 'Citizen Kane', year: 1941 },
|
||||
{ id: '89', title: 'Memento', year: 2000 },
|
||||
{ id: '90', title: 'North by Northwest', year: 1959 },
|
||||
{ id: '91', title: 'Star Wars: Episode III – Revenge of the Sith', year: 2005 },
|
||||
{ id: '92', title: '2001: A Space Odyssey', year: 1968 },
|
||||
{ id: '93', title: 'Amélie', year: 2001 },
|
||||
{ id: '94', title: "Singin' in the Rain", year: 1952 },
|
||||
{ id: '95', title: 'Apocalypse Now', year: 1979 },
|
||||
{ id: '96', title: 'Taxi Driver', year: 1976 },
|
||||
{ id: '97', title: 'Downfall', year: 2004 },
|
||||
{ id: '98', title: 'The Wolf of Wall Street', year: 2013 },
|
||||
{ id: '99', title: 'A Clockwork Orange', year: 1971 },
|
||||
{ id: '100', title: 'Double Indemnity', year: 1944 },
|
||||
];
|
||||
@@ -0,0 +1,82 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
|
||||
export default function ExampleAutocompleteAutoHighlight() {
|
||||
return (
|
||||
<Autocomplete.Root items={tags} autoHighlight>
|
||||
<label className="flex flex-col gap-1 text-sm font-bold text-neutral-950 dark:text-white">
|
||||
Auto highlight on type
|
||||
<Autocomplete.Input placeholder="e.g. feature" />
|
||||
</label>
|
||||
|
||||
<Autocomplete.Portal>
|
||||
<Autocomplete.Positioner sideOffset={4}>
|
||||
<Autocomplete.Popup>
|
||||
<Autocomplete.Empty>
|
||||
<div className="py-4 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No tags found.
|
||||
</div>
|
||||
</Autocomplete.Empty>
|
||||
<Autocomplete.List>
|
||||
{(tag: Tag) => (
|
||||
<Autocomplete.Item key={tag.id} value={tag}>
|
||||
{tag.value}
|
||||
</Autocomplete.Item>
|
||||
)}
|
||||
</Autocomplete.List>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const tags: Tag[] = [
|
||||
{ id: 't1', value: 'feature' },
|
||||
{ id: 't2', value: 'fix' },
|
||||
{ id: 't3', value: 'bug' },
|
||||
{ id: 't4', value: 'docs' },
|
||||
{ id: 't5', value: 'internal' },
|
||||
{ id: 't6', value: 'mobile' },
|
||||
{ id: 'c-accordion', value: 'component: accordion' },
|
||||
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
|
||||
{ id: 'c-autocomplete', value: 'component: autocomplete' },
|
||||
{ id: 'c-avatar', value: 'component: avatar' },
|
||||
{ id: 'c-checkbox', value: 'component: checkbox' },
|
||||
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
|
||||
{ id: 'c-collapsible', value: 'component: collapsible' },
|
||||
{ id: 'c-combobox', value: 'component: combobox' },
|
||||
{ id: 'c-context-menu', value: 'component: context menu' },
|
||||
{ id: 'c-dialog', value: 'component: dialog' },
|
||||
{ id: 'c-field', value: 'component: field' },
|
||||
{ id: 'c-fieldset', value: 'component: fieldset' },
|
||||
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
|
||||
{ id: 'c-form', value: 'component: form' },
|
||||
{ id: 'c-input', value: 'component: input' },
|
||||
{ id: 'c-menu', value: 'component: menu' },
|
||||
{ id: 'c-menubar', value: 'component: menubar' },
|
||||
{ id: 'c-meter', value: 'component: meter' },
|
||||
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
|
||||
{ id: 'c-number-field', value: 'component: number field' },
|
||||
{ id: 'c-popover', value: 'component: popover' },
|
||||
{ id: 'c-preview-card', value: 'component: preview card' },
|
||||
{ id: 'c-progress', value: 'component: progress' },
|
||||
{ id: 'c-radio', value: 'component: radio' },
|
||||
{ id: 'c-scroll-area', value: 'component: scroll area' },
|
||||
{ id: 'c-select', value: 'component: select' },
|
||||
{ id: 'c-separator', value: 'component: separator' },
|
||||
{ id: 'c-slider', value: 'component: slider' },
|
||||
{ id: 'c-switch', value: 'component: switch' },
|
||||
{ id: 'c-tabs', value: 'component: tabs' },
|
||||
{ id: 'c-toast', value: 'component: toast' },
|
||||
{ id: 'c-toggle', value: 'component: toggle' },
|
||||
{ id: 'c-toggle-group', value: 'component: toggle group' },
|
||||
{ id: 'c-toolbar', value: 'component: toolbar' },
|
||||
{ id: 'c-tooltip', value: 'component: tooltip' },
|
||||
];
|
||||
@@ -0,0 +1,82 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
|
||||
export default function ExampleAutocomplete() {
|
||||
return (
|
||||
<Autocomplete.Root items={tags}>
|
||||
<label className="flex flex-col gap-1 text-sm font-bold text-neutral-950 dark:text-white">
|
||||
Search tags
|
||||
<Autocomplete.Input placeholder="e.g. feature" />
|
||||
</label>
|
||||
|
||||
<Autocomplete.Portal>
|
||||
<Autocomplete.Positioner sideOffset={4}>
|
||||
<Autocomplete.Popup>
|
||||
<Autocomplete.Empty>
|
||||
<div className="py-4 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No tags found.
|
||||
</div>
|
||||
</Autocomplete.Empty>
|
||||
<Autocomplete.List>
|
||||
{(tag: Tag) => (
|
||||
<Autocomplete.Item key={tag.id} value={tag}>
|
||||
{tag.value}
|
||||
</Autocomplete.Item>
|
||||
)}
|
||||
</Autocomplete.List>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const tags: Tag[] = [
|
||||
{ id: 't1', value: 'feature' },
|
||||
{ id: 't2', value: 'fix' },
|
||||
{ id: 't3', value: 'bug' },
|
||||
{ id: 't4', value: 'docs' },
|
||||
{ id: 't5', value: 'internal' },
|
||||
{ id: 't6', value: 'mobile' },
|
||||
{ id: 'c-accordion', value: 'component: accordion' },
|
||||
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
|
||||
{ id: 'c-autocomplete', value: 'component: autocomplete' },
|
||||
{ id: 'c-avatar', value: 'component: avatar' },
|
||||
{ id: 'c-checkbox', value: 'component: checkbox' },
|
||||
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
|
||||
{ id: 'c-collapsible', value: 'component: collapsible' },
|
||||
{ id: 'c-combobox', value: 'component: combobox' },
|
||||
{ id: 'c-context-menu', value: 'component: context menu' },
|
||||
{ id: 'c-dialog', value: 'component: dialog' },
|
||||
{ id: 'c-field', value: 'component: field' },
|
||||
{ id: 'c-fieldset', value: 'component: fieldset' },
|
||||
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
|
||||
{ id: 'c-form', value: 'component: form' },
|
||||
{ id: 'c-input', value: 'component: input' },
|
||||
{ id: 'c-menu', value: 'component: menu' },
|
||||
{ id: 'c-menubar', value: 'component: menubar' },
|
||||
{ id: 'c-meter', value: 'component: meter' },
|
||||
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
|
||||
{ id: 'c-number-field', value: 'component: number field' },
|
||||
{ id: 'c-popover', value: 'component: popover' },
|
||||
{ id: 'c-preview-card', value: 'component: preview card' },
|
||||
{ id: 'c-progress', value: 'component: progress' },
|
||||
{ id: 'c-radio', value: 'component: radio' },
|
||||
{ id: 'c-scroll-area', value: 'component: scroll area' },
|
||||
{ id: 'c-select', value: 'component: select' },
|
||||
{ id: 'c-separator', value: 'component: separator' },
|
||||
{ id: 'c-slider', value: 'component: slider' },
|
||||
{ id: 'c-switch', value: 'component: switch' },
|
||||
{ id: 'c-tabs', value: 'component: tabs' },
|
||||
{ id: 'c-toast', value: 'component: toast' },
|
||||
{ id: 'c-toggle', value: 'component: toggle' },
|
||||
{ id: 'c-toggle-group', value: 'component: toggle group' },
|
||||
{ id: 'c-toolbar', value: 'component: toolbar' },
|
||||
{ id: 'c-tooltip', value: 'component: tooltip' },
|
||||
];
|
||||
@@ -0,0 +1,150 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
import ScrollArea from '@/components/ui/scroll-area';
|
||||
|
||||
export default function ExampleAutocompleteCommandPalette() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const shortcutsDescriptionId = React.useId();
|
||||
|
||||
function handleItemClick() {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger className="flex h-8 cursor-default items-center justify-center gap-2 border border-neutral-950 bg-white px-3 text-sm leading-none whitespace-nowrap font-normal text-neutral-950 select-none hover:bg-neutral-100 active:bg-neutral-200 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700">
|
||||
Open command palette
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className="fixed inset-0 bg-black opacity-20 transition-opacity duration-150 ease-[cubic-bezier(0.45,1.005,0,1.005)] data-ending-style:opacity-0 data-starting-style:opacity-0 dark:opacity-70 supports-[-webkit-touch-callout:none]:absolute" />
|
||||
<Dialog.Viewport className="fixed inset-0 flex items-start justify-center overflow-hidden px-2 pt-18 pb-2">
|
||||
<Dialog.Popup
|
||||
className="relative flex max-h-[min(36rem,calc(100dvh-5rem))] w-[calc(100vw-1rem)] max-w-md flex-col border border-neutral-950 bg-white text-neutral-950 shadow-[0.25rem_0.25rem_0] shadow-black/12 transition-[transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out data-ending-style:-translate-y-4 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:-translate-y-4 data-starting-style:scale-95 data-starting-style:opacity-0 dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none"
|
||||
aria-label="Command palette"
|
||||
>
|
||||
<Autocomplete.Root
|
||||
open
|
||||
inline
|
||||
items={groupedItems}
|
||||
autoHighlight="always"
|
||||
keepHighlight
|
||||
>
|
||||
<Autocomplete.Input
|
||||
className="relative z-1 h-10 w-full border-0 bg-white px-3 text-sm any-pointer-coarse:text-base font-normal text-neutral-950 placeholder:text-neutral-500 focus:outline-2 focus:outline-solid focus:outline-neutral-950 dark:focus:outline-white dark:bg-neutral-950 dark:text-white dark:placeholder:text-neutral-400"
|
||||
aria-label="Search commands"
|
||||
aria-describedby={shortcutsDescriptionId}
|
||||
placeholder="Search for apps and commands…"
|
||||
/>
|
||||
<Dialog.Close className="sr-only">Close command palette</Dialog.Close>
|
||||
|
||||
<ScrollArea.Root className="relative flex max-h-[min(60dvh,24rem)] min-h-0 flex-[0_1_auto] overflow-hidden border-t border-neutral-950 dark:border-t-white">
|
||||
<ScrollArea.Viewport className="min-h-0 flex-1 overscroll-contain [scroll-padding-block:0.25rem] focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white">
|
||||
<ScrollArea.Content style={{ minWidth: '100%' }}>
|
||||
<Autocomplete.Empty>
|
||||
<div className="flex min-h-32 items-center justify-start py-4 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No results found.
|
||||
</div>
|
||||
</Autocomplete.Empty>
|
||||
|
||||
<Autocomplete.List className="py-1">
|
||||
{(group: Group) => (
|
||||
<Autocomplete.Group
|
||||
key={group.value}
|
||||
items={group.items}
|
||||
className="not-last:mb-1"
|
||||
>
|
||||
<Autocomplete.GroupLabel className="flex min-h-8 items-center pr-6 pl-3 text-sm leading-none font-normal text-neutral-500 select-none outline-none dark:text-neutral-400">
|
||||
{group.value}
|
||||
</Autocomplete.GroupLabel>
|
||||
<Autocomplete.Collection>
|
||||
{(item: Item) => (
|
||||
<Autocomplete.Item
|
||||
key={item.value}
|
||||
value={item}
|
||||
onClick={handleItemClick}
|
||||
className="group grid min-h-8 cursor-default grid-cols-[minmax(0,1fr)_auto] items-center gap-2 px-6 text-sm font-normal leading-[1.25] outline-none select-none [scroll-margin-block:0.25rem] data-highlighted:bg-neutral-200 dark:data-highlighted:bg-neutral-700"
|
||||
>
|
||||
<span className="min-w-0 truncate font-normal">{item.label}</span>
|
||||
<span className="shrink-0 whitespace-nowrap text-sm text-neutral-500 group-data-highlighted:text-neutral-700 dark:text-neutral-400 dark:group-data-highlighted:text-neutral-300">
|
||||
{group.value === 'Suggestions' ? 'Application' : 'Command'}
|
||||
</span>
|
||||
</Autocomplete.Item>
|
||||
)}
|
||||
</Autocomplete.Collection>
|
||||
</Autocomplete.Group>
|
||||
)}
|
||||
</Autocomplete.List>
|
||||
</ScrollArea.Content>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar className="flex w-4 justify-center bg-black/[.12] dark:bg-white/[.12]">
|
||||
<ScrollArea.Thumb className="w-full bg-neutral-950 dark:bg-white" />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-neutral-950 bg-white px-3 py-2.5 text-xs text-neutral-600 dark:border-white dark:bg-neutral-950 dark:text-neutral-400">
|
||||
<span id={shortcutsDescriptionId} className="sr-only">
|
||||
Use Enter to activate the highlighted item.
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Activate</span>
|
||||
<kbd className="inline-flex h-5 min-w-5 items-center justify-center border border-neutral-400 bg-neutral-100 px-1 font-mono text-[0.625rem] leading-none font-normal text-neutral-600 dark:border-neutral-600 dark:bg-neutral-900 dark:text-neutral-400">
|
||||
Enter
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</Autocomplete.Root>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Viewport>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface Item {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Group {
|
||||
value: string;
|
||||
items: Item[];
|
||||
}
|
||||
|
||||
const suggestions: Item[] = [
|
||||
{ value: 'linear', label: 'Linear' },
|
||||
{ value: 'figma', label: 'Figma' },
|
||||
{ value: 'slack', label: 'Slack' },
|
||||
{ value: 'youtube', label: 'YouTube' },
|
||||
{ value: 'raycast', label: 'Raycast' },
|
||||
{ value: 'notion', label: 'Notion' },
|
||||
{ value: 'github', label: 'GitHub' },
|
||||
{ value: 'jira', label: 'Jira' },
|
||||
{ value: 'calendar', label: 'Google Calendar' },
|
||||
{ value: 'chrome', label: 'Google Chrome' },
|
||||
{ value: 'mail', label: 'Apple Mail' },
|
||||
{ value: 'terminal', label: 'Terminal' },
|
||||
];
|
||||
|
||||
const commands: Item[] = [
|
||||
{ value: 'clipboard-history', label: 'Clipboard History' },
|
||||
{ value: 'import-extension', label: 'Import Extension' },
|
||||
{ value: 'create-snippet', label: 'Create Snippet' },
|
||||
{ value: 'system-preferences', label: 'System Preferences' },
|
||||
{ value: 'window-management', label: 'Window Management' },
|
||||
{ value: 'toggle-dark-mode', label: 'Toggle Dark Mode' },
|
||||
{ value: 'new-window', label: 'New Window' },
|
||||
{ value: 'new-tab', label: 'New Tab' },
|
||||
{ value: 'search-docs', label: 'Search Documentation' },
|
||||
{ value: 'capture-screen', label: 'Capture Screenshot' },
|
||||
{ value: 'close-sidebar', label: 'Toggle Sidebar' },
|
||||
{ value: 'toggle-terminal', label: 'Toggle Integrated Terminal' },
|
||||
{ value: 'run-script', label: 'Run Script' },
|
||||
];
|
||||
|
||||
const groupedItems: Group[] = [
|
||||
{ value: 'Suggestions', items: suggestions },
|
||||
{ value: 'Commands', items: commands },
|
||||
];
|
||||
@@ -0,0 +1,180 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
import { matchSorter } from 'match-sorter';
|
||||
|
||||
export default function ExampleFuzzyMatchingAutocomplete() {
|
||||
return (
|
||||
<Autocomplete.Root
|
||||
items={fuzzyItems}
|
||||
filter={fuzzyFilter}
|
||||
itemToStringValue={(item) => item.title}
|
||||
>
|
||||
<label className="flex flex-col gap-1 text-sm font-bold text-neutral-950 dark:text-white">
|
||||
Fuzzy search documentation
|
||||
<Autocomplete.Input placeholder="e.g. React" />
|
||||
</label>
|
||||
|
||||
<Autocomplete.Portal>
|
||||
<Autocomplete.Positioner sideOffset={4}>
|
||||
<Autocomplete.Popup>
|
||||
<Autocomplete.Empty>
|
||||
<div className="py-3 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No results found for "{<Autocomplete.Value />}"
|
||||
</div>
|
||||
</Autocomplete.Empty>
|
||||
|
||||
<Autocomplete.List className="max-h-[min(var(--available-height),28rem)]">
|
||||
{(item: FuzzyItem) => (
|
||||
<Autocomplete.Item
|
||||
key={item.title}
|
||||
value={item}
|
||||
className="py-3 leading-6 data-highlighted:text-neutral-950 data-highlighted:before:bg-neutral-100 dark:data-highlighted:text-white dark:data-highlighted:before:bg-neutral-800"
|
||||
>
|
||||
<Autocomplete.Value>
|
||||
{(value) => (
|
||||
<span className="flex w-full flex-col gap-1">
|
||||
<span className="flex items-center justify-between gap-3">
|
||||
<span className="flex-1 font-bold leading-5">
|
||||
{highlightText(item.title, value)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{highlightText(item.description, value)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</Autocomplete.Value>
|
||||
</Autocomplete.Item>
|
||||
)}
|
||||
</Autocomplete.List>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function highlightText(text: string, query: string): React.ReactNode {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const limited = trimmed.slice(0, 100);
|
||||
const escaped = limited.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(`(${escaped})`, 'gi');
|
||||
|
||||
return text.split(regex).map((part, idx) =>
|
||||
regex.test(part) ? (
|
||||
<mark key={idx} className="bg-transparent font-bold text-blue-800 dark:text-blue-500">
|
||||
{part}
|
||||
</mark>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function fuzzyFilter(item: FuzzyItem, query: string): boolean {
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const results = matchSorter([item], query, {
|
||||
keys: [
|
||||
'title',
|
||||
'description',
|
||||
'category',
|
||||
{ key: 'title', threshold: matchSorter.rankings.CONTAINS },
|
||||
{ key: 'description', threshold: matchSorter.rankings.WORD_STARTS_WITH },
|
||||
],
|
||||
});
|
||||
|
||||
return results.length > 0;
|
||||
}
|
||||
|
||||
interface FuzzyItem {
|
||||
title: string;
|
||||
description: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
const fuzzyItems: FuzzyItem[] = [
|
||||
{
|
||||
title: 'React Hooks Guide',
|
||||
description: 'Learn how to use React Hooks like useState, useEffect, and custom hooks',
|
||||
category: 'React',
|
||||
},
|
||||
{
|
||||
title: 'JavaScript Array Methods',
|
||||
description: 'Master array methods like map, filter, reduce, and forEach in JavaScript',
|
||||
category: 'JavaScript',
|
||||
},
|
||||
{
|
||||
title: 'CSS Flexbox Layout',
|
||||
description: 'Complete guide to CSS Flexbox for responsive web design',
|
||||
category: 'CSS',
|
||||
},
|
||||
{
|
||||
title: 'TypeScript Interfaces',
|
||||
description: 'Understanding TypeScript interfaces and type definitions',
|
||||
category: 'TypeScript',
|
||||
},
|
||||
{
|
||||
title: 'React Performance Optimization',
|
||||
description: 'Tips and techniques for optimizing React application performance',
|
||||
category: 'React',
|
||||
},
|
||||
{
|
||||
title: 'HTML Semantic Elements',
|
||||
description: 'Using semantic HTML elements for better accessibility and SEO',
|
||||
category: 'HTML',
|
||||
},
|
||||
{
|
||||
title: 'Node.js Express Server',
|
||||
description: 'Building RESTful APIs with Node.js and Express framework',
|
||||
category: 'Node.js',
|
||||
},
|
||||
{
|
||||
title: 'Vue Composition API',
|
||||
description: 'Modern Vue.js development using the Composition API',
|
||||
category: 'Vue.js',
|
||||
},
|
||||
{
|
||||
title: 'Angular Components',
|
||||
description: 'Creating reusable Angular components with TypeScript',
|
||||
category: 'Angular',
|
||||
},
|
||||
{
|
||||
title: 'Python Django Framework',
|
||||
description: 'Web development with Python Django framework',
|
||||
category: 'Python',
|
||||
},
|
||||
{
|
||||
title: 'CSS Grid Layout',
|
||||
description: 'Advanced CSS Grid techniques for complex layouts',
|
||||
category: 'CSS',
|
||||
},
|
||||
{
|
||||
title: 'React Testing Library',
|
||||
description: 'Testing React components with React Testing Library',
|
||||
category: 'React',
|
||||
},
|
||||
{
|
||||
title: 'MongoDB Queries',
|
||||
description: 'Advanced MongoDB queries and aggregation pipelines',
|
||||
category: 'Database',
|
||||
},
|
||||
{
|
||||
title: 'Webpack Configuration',
|
||||
description: 'Optimizing webpack configuration for production builds',
|
||||
category: 'Build Tools',
|
||||
},
|
||||
{
|
||||
title: 'SASS/SCSS Guide',
|
||||
description: 'Writing maintainable CSS with SASS and SCSS',
|
||||
category: 'CSS',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,255 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
|
||||
export default function ExampleEmojiPicker() {
|
||||
const [pickerOpen, setPickerOpen] = React.useState(false);
|
||||
const [textValue, setTextValue] = React.useState('');
|
||||
const [searchValue, setSearchValue] = React.useState('');
|
||||
|
||||
const textInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
|
||||
function handleInsertEmoji(value: string | null) {
|
||||
if (!value || !textInputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emoji = value;
|
||||
const start = textInputRef.current.selectionStart ?? textInputRef.current.value.length ?? 0;
|
||||
const end = textInputRef.current.selectionEnd ?? textInputRef.current.value.length ?? 0;
|
||||
|
||||
setTextValue((prev) => prev.slice(0, start) + emoji + prev.slice(end));
|
||||
setPickerOpen(false);
|
||||
|
||||
const input = textInputRef.current;
|
||||
if (input) {
|
||||
input.focus();
|
||||
const caretPos = start + emoji.length;
|
||||
input.setSelectionRange(caretPos, caretPos);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-[16rem]">
|
||||
<div className="relative flex w-full">
|
||||
<input
|
||||
ref={textInputRef}
|
||||
type="text"
|
||||
className="-mr-px h-8 flex-1 border border-r-0 border-neutral-950 px-2 text-sm any-pointer-coarse:text-base font-normal text-neutral-950 placeholder:text-neutral-500 dark:placeholder:text-neutral-400 bg-white dark:bg-neutral-950 focus:relative focus:outline-2 focus:-outline-offset-1 focus:outline-solid focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white"
|
||||
placeholder="iMessage"
|
||||
value={textValue}
|
||||
onChange={(event) => setTextValue(event.target.value)}
|
||||
/>
|
||||
|
||||
<Autocomplete.Root
|
||||
items={emojiGroups}
|
||||
grid
|
||||
open={pickerOpen}
|
||||
onOpenChange={setPickerOpen}
|
||||
onOpenChangeComplete={() => setSearchValue('')}
|
||||
value={searchValue}
|
||||
onValueChange={(value, details) => {
|
||||
if (details.reason !== 'item-press') {
|
||||
setSearchValue(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Autocomplete.Trigger
|
||||
className="flex size-8 items-center justify-center border border-neutral-950 bg-transparent text-xl leading-none text-neutral-950 outline-none hover:bg-neutral-100 active:bg-neutral-200 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-solid focus-visible:outline-neutral-950 dark:focus-visible:outline-white data-popup-open:bg-neutral-100 dark:border-white dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700 dark:data-popup-open:bg-neutral-800"
|
||||
aria-label="Choose emoji"
|
||||
>
|
||||
😀
|
||||
</Autocomplete.Trigger>
|
||||
<Autocomplete.Portal>
|
||||
<Autocomplete.Positioner className="outline-0" sideOffset={4} align="end">
|
||||
<Autocomplete.Popup
|
||||
className="[--input-container-height:2rem] max-h-[20.5rem] max-w-[var(--available-width)] origin-[var(--transform-origin)] bg-white text-neutral-950 shadow-[0.25rem_0.25rem_0] shadow-black/12 transition-[transform,scale,opacity] data-ending-style:scale-90 data-ending-style:opacity-0 data-starting-style:scale-90 data-starting-style:opacity-0 dark:bg-neutral-950 dark:text-white dark:shadow-none"
|
||||
aria-label="Select emoji"
|
||||
>
|
||||
<Autocomplete.Input
|
||||
placeholder="Search emojis…"
|
||||
className="h-8 w-64 max-w-full border border-neutral-950 bg-white px-2 text-sm any-pointer-coarse:text-base font-normal text-neutral-950 placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus:outline-2 focus:-outline-offset-2 focus:outline-solid focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:bg-neutral-950 dark:text-white"
|
||||
/>
|
||||
<div className="border border-t-0 border-neutral-950 dark:border-white">
|
||||
<Autocomplete.Empty>
|
||||
<div className="px-2 py-3 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No emojis found
|
||||
</div>
|
||||
</Autocomplete.Empty>
|
||||
<Autocomplete.List className="max-h-[min(calc(20.5rem-var(--input-container-height)-2px),calc(var(--available-height)-var(--input-container-height)-2px))] overflow-auto scroll-pt-1 scroll-pb-[0.35rem] overscroll-contain py-2 empty:p-0">
|
||||
{(group: EmojiGroup) => (
|
||||
<Autocomplete.Group key={group.value} items={group.items} className="block">
|
||||
<Autocomplete.GroupLabel className="p-2 text-sm leading-4 text-neutral-500 select-none dark:text-neutral-400">
|
||||
{group.label}
|
||||
</Autocomplete.GroupLabel>
|
||||
<div className="px-2 pb-1 pt-0" role="presentation">
|
||||
{chunkArray(group.items, COLUMNS).map((row, rowIdx) => (
|
||||
<Autocomplete.Row key={rowIdx} className="grid grid-cols-5">
|
||||
{row.map((rowItem) => (
|
||||
<Autocomplete.Item
|
||||
key={rowItem.emoji}
|
||||
value={rowItem}
|
||||
className="group flex h-10 min-w-[var(--anchor-width)] cursor-default flex-col items-center justify-center bg-transparent px-0.5 py-2 text-neutral-950 outline-0 select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-white data-highlighted:before:absolute data-highlighted:before:inset-0 data-highlighted:before:z-[-1] data-highlighted:before:bg-neutral-100 dark:text-white dark:data-highlighted:text-neutral-950 dark:data-highlighted:before:bg-neutral-800"
|
||||
onClick={() => {
|
||||
handleInsertEmoji(rowItem.emoji);
|
||||
setPickerOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="text-2xl leading-none">{rowItem.emoji}</span>
|
||||
</Autocomplete.Item>
|
||||
))}
|
||||
</Autocomplete.Row>
|
||||
))}
|
||||
</div>
|
||||
</Autocomplete.Group>
|
||||
)}
|
||||
</Autocomplete.List>
|
||||
</div>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const COLUMNS = 5;
|
||||
|
||||
function chunkArray<T>(array: T[], size: number): T[][] {
|
||||
const result: T[][] = [];
|
||||
for (let i = 0; i < array.length; i += size) {
|
||||
result.push(array.slice(i, i + size));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface EmojiItem {
|
||||
emoji: string;
|
||||
value: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface EmojiGroup {
|
||||
value: string;
|
||||
label: string;
|
||||
items: EmojiItem[];
|
||||
}
|
||||
|
||||
export const emojiCategories = [
|
||||
{
|
||||
label: 'Smileys & Emotion',
|
||||
emojis: [
|
||||
{ emoji: '😀', name: 'grinning face' },
|
||||
{ emoji: '😃', name: 'grinning face with big eyes' },
|
||||
{ emoji: '😄', name: 'grinning face with smiling eyes' },
|
||||
{ emoji: '😁', name: 'beaming face with smiling eyes' },
|
||||
{ emoji: '😆', name: 'grinning squinting face' },
|
||||
{ emoji: '😅', name: 'grinning face with sweat' },
|
||||
{ emoji: '🤣', name: 'rolling on the floor laughing' },
|
||||
{ emoji: '😂', name: 'face with tears of joy' },
|
||||
{ emoji: '🙂', name: 'slightly smiling face' },
|
||||
{ emoji: '🙃', name: 'upside-down face' },
|
||||
{ emoji: '😉', name: 'winking face' },
|
||||
{ emoji: '😊', name: 'smiling face with smiling eyes' },
|
||||
{ emoji: '😇', name: 'smiling face with halo' },
|
||||
{ emoji: '🥰', name: 'smiling face with hearts' },
|
||||
{ emoji: '😍', name: 'smiling face with heart-eyes' },
|
||||
{ emoji: '🤩', name: 'star-struck' },
|
||||
{ emoji: '😘', name: 'face blowing a kiss' },
|
||||
{ emoji: '😗', name: 'kissing face' },
|
||||
{ emoji: '☺️', name: 'smiling face' },
|
||||
{ emoji: '😚', name: 'kissing face with closed eyes' },
|
||||
{ emoji: '😙', name: 'kissing face with smiling eyes' },
|
||||
{ emoji: '🥲', name: 'smiling face with tear' },
|
||||
{ emoji: '😋', name: 'face savoring food' },
|
||||
{ emoji: '😛', name: 'face with tongue' },
|
||||
{ emoji: '😜', name: 'winking face with tongue' },
|
||||
{ emoji: '🤪', name: 'zany face' },
|
||||
{ emoji: '😝', name: 'squinting face with tongue' },
|
||||
{ emoji: '🤑', name: 'money-mouth face' },
|
||||
{ emoji: '🤗', name: 'hugging face' },
|
||||
{ emoji: '🤭', name: 'face with hand over mouth' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Animals & Nature',
|
||||
emojis: [
|
||||
{ emoji: '🐶', name: 'dog face' },
|
||||
{ emoji: '🐱', name: 'cat face' },
|
||||
{ emoji: '🐭', name: 'mouse face' },
|
||||
{ emoji: '🐹', name: 'hamster' },
|
||||
{ emoji: '🐰', name: 'rabbit face' },
|
||||
{ emoji: '🦊', name: 'fox' },
|
||||
{ emoji: '🐻', name: 'bear' },
|
||||
{ emoji: '🐼', name: 'panda' },
|
||||
{ emoji: '🐨', name: 'koala' },
|
||||
{ emoji: '🐯', name: 'tiger face' },
|
||||
{ emoji: '🦁', name: 'lion' },
|
||||
{ emoji: '🐮', name: 'cow face' },
|
||||
{ emoji: '🐷', name: 'pig face' },
|
||||
{ emoji: '🐽', name: 'pig nose' },
|
||||
{ emoji: '🐸', name: 'frog' },
|
||||
{ emoji: '🐵', name: 'monkey face' },
|
||||
{ emoji: '🙈', name: 'see-no-evil monkey' },
|
||||
{ emoji: '🙉', name: 'hear-no-evil monkey' },
|
||||
{ emoji: '🙊', name: 'speak-no-evil monkey' },
|
||||
{ emoji: '🐒', name: 'monkey' },
|
||||
{ emoji: '🐔', name: 'chicken' },
|
||||
{ emoji: '🐧', name: 'penguin' },
|
||||
{ emoji: '🐦', name: 'bird' },
|
||||
{ emoji: '🐤', name: 'baby chick' },
|
||||
{ emoji: '🐣', name: 'hatching chick' },
|
||||
{ emoji: '🐥', name: 'front-facing baby chick' },
|
||||
{ emoji: '🦆', name: 'duck' },
|
||||
{ emoji: '🦅', name: 'eagle' },
|
||||
{ emoji: '🦉', name: 'owl' },
|
||||
{ emoji: '🦇', name: 'bat' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Food & Drink',
|
||||
emojis: [
|
||||
{ emoji: '🍎', name: 'red apple' },
|
||||
{ emoji: '🍏', name: 'green apple' },
|
||||
{ emoji: '🍊', name: 'tangerine' },
|
||||
{ emoji: '🍋', name: 'lemon' },
|
||||
{ emoji: '🍌', name: 'banana' },
|
||||
{ emoji: '🍉', name: 'watermelon' },
|
||||
{ emoji: '🍇', name: 'grapes' },
|
||||
{ emoji: '🍓', name: 'strawberry' },
|
||||
{ emoji: '🫐', name: 'blueberries' },
|
||||
{ emoji: '🍈', name: 'melon' },
|
||||
{ emoji: '🍒', name: 'cherries' },
|
||||
{ emoji: '🍑', name: 'peach' },
|
||||
{ emoji: '🥭', name: 'mango' },
|
||||
{ emoji: '🍍', name: 'pineapple' },
|
||||
{ emoji: '🥥', name: 'coconut' },
|
||||
{ emoji: '🥝', name: 'kiwi fruit' },
|
||||
{ emoji: '🍅', name: 'tomato' },
|
||||
{ emoji: '🍆', name: 'eggplant' },
|
||||
{ emoji: '🥑', name: 'avocado' },
|
||||
{ emoji: '🥦', name: 'broccoli' },
|
||||
{ emoji: '🥬', name: 'leafy greens' },
|
||||
{ emoji: '🥒', name: 'cucumber' },
|
||||
{ emoji: '🌶️', name: 'hot pepper' },
|
||||
{ emoji: '🫑', name: 'bell pepper' },
|
||||
{ emoji: '🌽', name: 'ear of corn' },
|
||||
{ emoji: '🥕', name: 'carrot' },
|
||||
{ emoji: '🫒', name: 'olive' },
|
||||
{ emoji: '🧄', name: 'garlic' },
|
||||
{ emoji: '🧅', name: 'onion' },
|
||||
{ emoji: '🥔', name: 'potato' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const emojiGroups: EmojiGroup[] = emojiCategories.map((category) => ({
|
||||
value: category.label,
|
||||
label: category.label,
|
||||
items: category.emojis.map((emoji) => ({
|
||||
...emoji,
|
||||
value: emoji.name.toLowerCase(),
|
||||
})),
|
||||
}));
|
||||
@@ -0,0 +1,112 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
|
||||
export default function ExampleGroupAutocomplete() {
|
||||
return (
|
||||
<Autocomplete.Root items={groupedTags}>
|
||||
<label className="flex flex-col gap-1 text-sm font-bold text-neutral-950 dark:text-white">
|
||||
Select a tag
|
||||
<Autocomplete.Input placeholder="e.g. feature" />
|
||||
</label>
|
||||
|
||||
<Autocomplete.Portal>
|
||||
<Autocomplete.Positioner sideOffset={4}>
|
||||
<Autocomplete.Popup>
|
||||
<Autocomplete.Empty>
|
||||
<div className="py-4 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No tags found.
|
||||
</div>
|
||||
</Autocomplete.Empty>
|
||||
<Autocomplete.List>
|
||||
{(group: TagGroup) => (
|
||||
<Autocomplete.Group
|
||||
key={group.value}
|
||||
items={group.items}
|
||||
className="block pb-2 last:pb-0"
|
||||
>
|
||||
<Autocomplete.GroupLabel className="p-2 leading-4">
|
||||
{group.value}
|
||||
</Autocomplete.GroupLabel>
|
||||
<Autocomplete.Collection>
|
||||
{(tag: Tag) => (
|
||||
<Autocomplete.Item key={tag.id} value={tag}>
|
||||
{tag.label}
|
||||
</Autocomplete.Item>
|
||||
)}
|
||||
</Autocomplete.Collection>
|
||||
</Autocomplete.Group>
|
||||
)}
|
||||
</Autocomplete.List>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
label: string;
|
||||
group: 'Type' | 'Component';
|
||||
}
|
||||
|
||||
interface TagGroup {
|
||||
value: string;
|
||||
items: Tag[];
|
||||
}
|
||||
|
||||
const tagsData: Tag[] = [
|
||||
{ id: 't1', label: 'feature', group: 'Type' },
|
||||
{ id: 't2', label: 'fix', group: 'Type' },
|
||||
{ id: 't3', label: 'bug', group: 'Type' },
|
||||
{ id: 't4', label: 'docs', group: 'Type' },
|
||||
{ id: 't5', label: 'internal', group: 'Type' },
|
||||
{ id: 't6', label: 'mobile', group: 'Type' },
|
||||
{ id: 'c-accordion', label: 'component: accordion', group: 'Component' },
|
||||
{ id: 'c-alert-dialog', label: 'component: alert dialog', group: 'Component' },
|
||||
{ id: 'c-autocomplete', label: 'component: autocomplete', group: 'Component' },
|
||||
{ id: 'c-avatar', label: 'component: avatar', group: 'Component' },
|
||||
{ id: 'c-checkbox', label: 'component: checkbox', group: 'Component' },
|
||||
{ id: 'c-checkbox-group', label: 'component: checkbox group', group: 'Component' },
|
||||
{ id: 'c-collapsible', label: 'component: collapsible', group: 'Component' },
|
||||
{ id: 'c-combobox', label: 'component: combobox', group: 'Component' },
|
||||
{ id: 'c-context-menu', label: 'component: context menu', group: 'Component' },
|
||||
{ id: 'c-dialog', label: 'component: dialog', group: 'Component' },
|
||||
{ id: 'c-field', label: 'component: field', group: 'Component' },
|
||||
{ id: 'c-fieldset', label: 'component: fieldset', group: 'Component' },
|
||||
{ id: 'c-filterable-menu', label: 'component: filterable menu', group: 'Component' },
|
||||
{ id: 'c-form', label: 'component: form', group: 'Component' },
|
||||
{ id: 'c-input', label: 'component: input', group: 'Component' },
|
||||
{ id: 'c-menu', label: 'component: menu', group: 'Component' },
|
||||
{ id: 'c-menubar', label: 'component: menubar', group: 'Component' },
|
||||
{ id: 'c-meter', label: 'component: meter', group: 'Component' },
|
||||
{ id: 'c-navigation-menu', label: 'component: navigation menu', group: 'Component' },
|
||||
{ id: 'c-number-field', label: 'component: number field', group: 'Component' },
|
||||
{ id: 'c-popover', label: 'component: popover', group: 'Component' },
|
||||
{ id: 'c-preview-card', label: 'component: preview card', group: 'Component' },
|
||||
{ id: 'c-progress', label: 'component: progress', group: 'Component' },
|
||||
{ id: 'c-radio', label: 'component: radio', group: 'Component' },
|
||||
{ id: 'c-scroll-area', label: 'component: scroll area', group: 'Component' },
|
||||
{ id: 'c-select', label: 'component: select', group: 'Component' },
|
||||
{ id: 'c-separator', label: 'component: separator', group: 'Component' },
|
||||
{ id: 'c-slider', label: 'component: slider', group: 'Component' },
|
||||
{ id: 'c-switch', label: 'component: switch', group: 'Component' },
|
||||
{ id: 'c-tabs', label: 'component: tabs', group: 'Component' },
|
||||
{ id: 'c-toast', label: 'component: toast', group: 'Component' },
|
||||
{ id: 'c-toggle', label: 'component: toggle', group: 'Component' },
|
||||
{ id: 'c-toggle-group', label: 'component: toggle group', group: 'Component' },
|
||||
{ id: 'c-toolbar', label: 'component: toolbar', group: 'Component' },
|
||||
{ id: 'c-tooltip', label: 'component: tooltip', group: 'Component' },
|
||||
];
|
||||
|
||||
function groupTags(tags: Tag[]): TagGroup[] {
|
||||
const groups: { [key: string]: Tag[] } = {};
|
||||
tags.forEach((t) => {
|
||||
(groups[t.group] ??= []).push(t);
|
||||
});
|
||||
const order = ['Type', 'Component'];
|
||||
return order.map((value) => ({ value, items: groups[value] ?? [] }));
|
||||
}
|
||||
|
||||
const groupedTags: TagGroup[] = groupTags(tagsData);
|
||||
@@ -0,0 +1,77 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
|
||||
export default function ExampleAutocompleteInline() {
|
||||
return (
|
||||
<Autocomplete.Root items={tags} mode="both">
|
||||
<label className="flex flex-col gap-1 text-sm font-bold text-neutral-950 dark:text-white">
|
||||
Search tags
|
||||
<Autocomplete.Input placeholder="e.g. feature" />
|
||||
</label>
|
||||
|
||||
<Autocomplete.Portal>
|
||||
<Autocomplete.Positioner className="data-empty:hidden" sideOffset={4}>
|
||||
<Autocomplete.Popup>
|
||||
<Autocomplete.List>
|
||||
{(tag: Tag) => (
|
||||
<Autocomplete.Item key={tag.id} value={tag}>
|
||||
{tag.value}
|
||||
</Autocomplete.Item>
|
||||
)}
|
||||
</Autocomplete.List>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const tags: Tag[] = [
|
||||
{ id: 't1', value: 'feature' },
|
||||
{ id: 't2', value: 'fix' },
|
||||
{ id: 't3', value: 'bug' },
|
||||
{ id: 't4', value: 'docs' },
|
||||
{ id: 't5', value: 'internal' },
|
||||
{ id: 't6', value: 'mobile' },
|
||||
{ id: 'c-accordion', value: 'component: accordion' },
|
||||
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
|
||||
{ id: 'c-autocomplete', value: 'component: autocomplete' },
|
||||
{ id: 'c-avatar', value: 'component: avatar' },
|
||||
{ id: 'c-checkbox', value: 'component: checkbox' },
|
||||
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
|
||||
{ id: 'c-collapsible', value: 'component: collapsible' },
|
||||
{ id: 'c-combobox', value: 'component: combobox' },
|
||||
{ id: 'c-context-menu', value: 'component: context menu' },
|
||||
{ id: 'c-dialog', value: 'component: dialog' },
|
||||
{ id: 'c-field', value: 'component: field' },
|
||||
{ id: 'c-fieldset', value: 'component: fieldset' },
|
||||
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
|
||||
{ id: 'c-form', value: 'component: form' },
|
||||
{ id: 'c-input', value: 'component: input' },
|
||||
{ id: 'c-menu', value: 'component: menu' },
|
||||
{ id: 'c-menubar', value: 'component: menubar' },
|
||||
{ id: 'c-meter', value: 'component: meter' },
|
||||
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
|
||||
{ id: 'c-number-field', value: 'component: number field' },
|
||||
{ id: 'c-popover', value: 'component: popover' },
|
||||
{ id: 'c-preview-card', value: 'component: preview card' },
|
||||
{ id: 'c-progress', value: 'component: progress' },
|
||||
{ id: 'c-radio', value: 'component: radio' },
|
||||
{ id: 'c-scroll-area', value: 'component: scroll area' },
|
||||
{ id: 'c-select', value: 'component: select' },
|
||||
{ id: 'c-separator', value: 'component: separator' },
|
||||
{ id: 'c-slider', value: 'component: slider' },
|
||||
{ id: 'c-switch', value: 'component: switch' },
|
||||
{ id: 'c-tabs', value: 'component: tabs' },
|
||||
{ id: 'c-toast', value: 'component: toast' },
|
||||
{ id: 'c-toggle', value: 'component: toggle' },
|
||||
{ id: 'c-toggle-group', value: 'component: toggle group' },
|
||||
{ id: 'c-toolbar', value: 'component: toolbar' },
|
||||
{ id: 'c-tooltip', value: 'component: tooltip' },
|
||||
];
|
||||
@@ -0,0 +1,118 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
|
||||
const limit = 8;
|
||||
|
||||
export default function ExampleAutocompleteLimit() {
|
||||
const [value, setValue] = React.useState('');
|
||||
|
||||
const { contains } = Autocomplete.useFilter({ sensitivity: 'base' });
|
||||
|
||||
const totalMatches = React.useMemo(() => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return tags.length;
|
||||
}
|
||||
return tags.filter((t) => contains(t.value, trimmed)).length;
|
||||
}, [value, contains]);
|
||||
|
||||
const moreCount = Math.max(0, totalMatches - limit);
|
||||
|
||||
return (
|
||||
<Autocomplete.Root items={tags} value={value} onValueChange={setValue} limit={limit}>
|
||||
<label className="flex flex-col gap-1 text-sm font-bold text-neutral-950 dark:text-white">
|
||||
Limit results to 8
|
||||
<Autocomplete.Input placeholder="e.g. component" />
|
||||
</label>
|
||||
|
||||
<Autocomplete.Portal>
|
||||
<Autocomplete.Positioner sideOffset={4}>
|
||||
<Autocomplete.Popup>
|
||||
<Autocomplete.Empty>
|
||||
<div className="py-2 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No results found for "{value}"
|
||||
</div>
|
||||
</Autocomplete.Empty>
|
||||
|
||||
<Autocomplete.List>
|
||||
{(tag: Tag) => (
|
||||
<Autocomplete.Item key={tag.id} value={tag}>
|
||||
{tag.value}
|
||||
</Autocomplete.Item>
|
||||
)}
|
||||
</Autocomplete.List>
|
||||
|
||||
<Autocomplete.Status>
|
||||
{moreCount > 0 ? (
|
||||
<div className="py-2 pr-4 pl-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{`Hiding ${moreCount} results (type a more specific query to narrow results)`}
|
||||
</div>
|
||||
) : null}
|
||||
</Autocomplete.Status>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// Larger dataset to make the limit visible.
|
||||
const tags: Tag[] = [
|
||||
{ id: 't1', value: 'feature' },
|
||||
{ id: 't2', value: 'fix' },
|
||||
{ id: 't3', value: 'bug' },
|
||||
{ id: 't4', value: 'docs' },
|
||||
{ id: 't5', value: 'internal' },
|
||||
{ id: 't6', value: 'mobile' },
|
||||
{ id: 't7', value: 'frontend' },
|
||||
{ id: 't8', value: 'backend' },
|
||||
{ id: 't9', value: 'performance' },
|
||||
{ id: 't10', value: 'accessibility' },
|
||||
{ id: 't11', value: 'design' },
|
||||
{ id: 't12', value: 'research' },
|
||||
{ id: 't13', value: 'testing' },
|
||||
{ id: 't14', value: 'infrastructure' },
|
||||
{ id: 't15', value: 'documentation' },
|
||||
{ id: 'c-accordion', value: 'component: accordion' },
|
||||
{ id: 'c-alert-dialog', value: 'component: alert dialog' },
|
||||
{ id: 'c-autocomplete', value: 'component: autocomplete' },
|
||||
{ id: 'c-avatar', value: 'component: avatar' },
|
||||
{ id: 'c-checkbox', value: 'component: checkbox' },
|
||||
{ id: 'c-checkbox-group', value: 'component: checkbox group' },
|
||||
{ id: 'c-collapsible', value: 'component: collapsible' },
|
||||
{ id: 'c-combobox', value: 'component: combobox' },
|
||||
{ id: 'c-context-menu', value: 'component: context menu' },
|
||||
{ id: 'c-dialog', value: 'component: dialog' },
|
||||
{ id: 'c-field', value: 'component: field' },
|
||||
{ id: 'c-fieldset', value: 'component: fieldset' },
|
||||
{ id: 'c-filterable-menu', value: 'component: filterable menu' },
|
||||
{ id: 'c-form', value: 'component: form' },
|
||||
{ id: 'c-input', value: 'component: input' },
|
||||
{ id: 'c-menu', value: 'component: menu' },
|
||||
{ id: 'c-menubar', value: 'component: menubar' },
|
||||
{ id: 'c-meter', value: 'component: meter' },
|
||||
{ id: 'c-navigation-menu', value: 'component: navigation menu' },
|
||||
{ id: 'c-number-field', value: 'component: number field' },
|
||||
{ id: 'c-popover', value: 'component: popover' },
|
||||
{ id: 'c-preview-card', value: 'component: preview card' },
|
||||
{ id: 'c-progress', value: 'component: progress' },
|
||||
{ id: 'c-radio', value: 'component: radio' },
|
||||
{ id: 'c-scroll-area', value: 'component: scroll area' },
|
||||
{ id: 'c-select', value: 'component: select' },
|
||||
{ id: 'c-separator', value: 'component: separator' },
|
||||
{ id: 'c-slider', value: 'component: slider' },
|
||||
{ id: 'c-switch', value: 'component: switch' },
|
||||
{ id: 'c-tabs', value: 'component: tabs' },
|
||||
{ id: 'c-toast', value: 'component: toast' },
|
||||
{ id: 'c-toggle', value: 'component: toggle' },
|
||||
{ id: 'c-toggle-group', value: 'component: toggle group' },
|
||||
{ id: 'c-toolbar', value: 'component: toolbar' },
|
||||
{ id: 'c-tooltip', value: 'component: tooltip' },
|
||||
];
|
||||
@@ -0,0 +1,151 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Autocomplete from '@/components/ui/autocomplete';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
|
||||
export default function ExampleVirtualizedAutocomplete() {
|
||||
const virtualizerRef = React.useRef<Virtualizer | null>(null);
|
||||
|
||||
return (
|
||||
<Autocomplete.Root
|
||||
virtualized
|
||||
items={virtualizedItems}
|
||||
openOnInputClick
|
||||
itemToStringValue={getItemLabel}
|
||||
onItemHighlighted={(item, { reason, index }) => {
|
||||
const virtualizer = virtualizerRef.current;
|
||||
|
||||
if (!item || !virtualizer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isStart = index === 0;
|
||||
const isEnd = index === virtualizer.options.count - 1;
|
||||
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));
|
||||
|
||||
if (shouldScroll) {
|
||||
queueMicrotask(() => {
|
||||
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label className="flex flex-col gap-1 text-sm font-bold text-neutral-950 dark:text-white">
|
||||
Search 10,000 items
|
||||
<Autocomplete.Input />
|
||||
</label>
|
||||
|
||||
<Autocomplete.Portal>
|
||||
<Autocomplete.Positioner sideOffset={4}>
|
||||
<Autocomplete.Popup>
|
||||
<Autocomplete.Empty>
|
||||
<div className="py-3 px-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No items found.
|
||||
</div>
|
||||
</Autocomplete.Empty>
|
||||
<Autocomplete.List className="p-0">
|
||||
<VirtualizedList virtualizerRef={virtualizerRef} />
|
||||
</Autocomplete.List>
|
||||
</Autocomplete.Popup>
|
||||
</Autocomplete.Positioner>
|
||||
</Autocomplete.Portal>
|
||||
</Autocomplete.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function VirtualizedList({
|
||||
virtualizerRef,
|
||||
}: {
|
||||
virtualizerRef: React.RefObject<Virtualizer | null>;
|
||||
}) {
|
||||
const filteredItems = Autocomplete.useFilteredItems<VirtualizedItem>();
|
||||
|
||||
const scrollElementRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: filteredItems.length,
|
||||
getScrollElement: () => scrollElementRef.current,
|
||||
estimateSize: () => 32,
|
||||
overscan: 20,
|
||||
paddingStart: 4,
|
||||
paddingEnd: 4,
|
||||
scrollPaddingEnd: 4,
|
||||
scrollPaddingStart: 4,
|
||||
});
|
||||
|
||||
React.useImperativeHandle(virtualizerRef, () => virtualizer);
|
||||
|
||||
const handleScrollElementRef = React.useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
scrollElementRef.current = element;
|
||||
if (element) {
|
||||
virtualizer.measure();
|
||||
}
|
||||
},
|
||||
[virtualizer],
|
||||
);
|
||||
|
||||
const totalSize = virtualizer.getTotalSize();
|
||||
|
||||
if (!filteredItems.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="presentation"
|
||||
ref={handleScrollElementRef}
|
||||
className="h-[min(22.5rem,var(--total-size))] max-h-[var(--available-height)] overflow-auto overscroll-contain scroll-py-1"
|
||||
style={{ '--total-size': `${totalSize}px` } as React.CSSProperties}
|
||||
>
|
||||
<div role="presentation" className="relative w-full" style={{ height: totalSize }}>
|
||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||
const item = filteredItems[virtualItem.index];
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Autocomplete.Item
|
||||
key={virtualItem.key}
|
||||
index={virtualItem.index}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
value={item}
|
||||
aria-setsize={filteredItems.length}
|
||||
aria-posinset={virtualItem.index + 1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: virtualItem.size,
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Autocomplete.Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface VirtualizedItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function getItemLabel(item: VirtualizedItem | null) {
|
||||
return item ? item.name : '';
|
||||
}
|
||||
|
||||
const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
|
||||
const id = String(index + 1);
|
||||
const indexLabel = id.padStart(4, '0');
|
||||
return { id, name: `Item ${indexLabel}` };
|
||||
});
|
||||
|
||||
type Virtualizer = ReturnType<typeof useVirtualizer<HTMLDivElement, Element>>;
|
||||
@@ -0,0 +1,23 @@
|
||||
// @ts-nocheck
|
||||
import Avatar from '@/components/ui/avatar';
|
||||
|
||||
export default function ExampleAvatar() {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<Avatar.Root className="inline-flex size-8 items-center justify-center overflow-hidden rounded-full bg-neutral-200 align-middle text-sm leading-none font-normal text-neutral-950 select-none dark:bg-neutral-800 dark:text-white">
|
||||
<Avatar.Image
|
||||
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80"
|
||||
width="48"
|
||||
height="48"
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delay={600} className="flex size-full items-center justify-center text-sm">
|
||||
LT
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<Avatar.Root className="inline-flex size-8 items-center justify-center overflow-hidden rounded-full bg-neutral-200 align-middle text-sm leading-none font-normal text-neutral-950 select-none dark:bg-neutral-800 dark:text-white">
|
||||
LT
|
||||
</Avatar.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export default function ExampleButton() {
|
||||
return <Button>Submit</Button>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export default function ExampleButton() {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={loading}
|
||||
focusableWhenDisabled
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 4000);
|
||||
}}
|
||||
>
|
||||
{loading ? 'Submitting' : 'Submit'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Checkbox from '@/components/ui/checkbox';
|
||||
import { CheckboxGroup } from '@/components/ui/checkbox-group';
|
||||
|
||||
export default function ExampleCheckboxGroup() {
|
||||
const id = React.useId();
|
||||
return (
|
||||
<CheckboxGroup
|
||||
aria-labelledby={id}
|
||||
defaultValue={['fuji-apple']}
|
||||
className="flex flex-col items-start gap-1 text-neutral-950 dark:text-white"
|
||||
>
|
||||
<div className="text-sm font-bold" id={id}>
|
||||
Apples
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm font-normal text-neutral-950 dark:text-white">
|
||||
<Checkbox.Root name="apple" value="fuji-apple">
|
||||
<Checkbox.Indicator>
|
||||
<CheckIcon />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
Fuji
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm font-normal text-neutral-950 dark:text-white">
|
||||
<Checkbox.Root name="apple" value="gala-apple">
|
||||
<Checkbox.Indicator>
|
||||
<CheckIcon />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
Gala
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm font-normal text-neutral-950 dark:text-white">
|
||||
<Checkbox.Root name="apple" value="granny-smith-apple">
|
||||
<Checkbox.Indicator>
|
||||
<CheckIcon />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
Granny Smith
|
||||
</label>
|
||||
</CheckboxGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// @ts-nocheck
|
||||
import * as React from 'react';
|
||||
import Checkbox from '@/components/ui/checkbox';
|
||||
|
||||
export default function ExampleCheckbox() {
|
||||
return (
|
||||
<label className="flex items-center gap-2 text-sm font-normal text-neutral-950 dark:text-white">
|
||||
<Checkbox.Root defaultChecked>
|
||||
<Checkbox.Indicator>
|
||||
<CheckIcon />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
Enable notifications
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// @ts-nocheck
|
||||
import * as React from 'react';
|
||||
import Collapsible from '@/components/ui/collapsible';
|
||||
|
||||
export default function ExampleCollapsible() {
|
||||
return (
|
||||
<Collapsible.Root className="flex min-h-36 w-48 flex-col justify-center text-neutral-950 dark:text-white">
|
||||
<Collapsible.Trigger>
|
||||
Recovery keys
|
||||
<CaretRightIcon className="transition-transform duration-100 ease-[ease-out] group-data-panel-open:rotate-90" />
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Panel>
|
||||
<div className="flex flex-col gap-2 px-3.5 py-2">
|
||||
<div>alien-bean-pasta</div>
|
||||
<div>wild-irish-burrito</div>
|
||||
<div>horse-battery-staple</div>
|
||||
</div>
|
||||
</Collapsible.Panel>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function CaretRightIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="M6 12V4l4.5 4z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Combobox from '@/components/ui/combobox';
|
||||
|
||||
export default function ExampleAsyncMultipleCombobox() {
|
||||
const id = React.useId();
|
||||
|
||||
const [searchResults, setSearchResults] = React.useState<DirectoryUser[]>([]);
|
||||
const [selectedValues, setSelectedValues] = React.useState<DirectoryUser[]>([]);
|
||||
const [searchValue, setSearchValue] = React.useState('');
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [blockStartStatus, setBlockStartStatus] = React.useState(false);
|
||||
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
|
||||
const { contains } = Combobox.useFilter();
|
||||
|
||||
const abortControllerRef = React.useRef<AbortController | null>(null);
|
||||
const selectedValuesRef = React.useRef<DirectoryUser[]>([]);
|
||||
|
||||
const trimmedSearchValue = searchValue.trim();
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
if (selectedValues.length === 0) {
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
const merged = [...searchResults];
|
||||
|
||||
selectedValues.forEach((user) => {
|
||||
if (!searchResults.some((result) => result.id === user.id)) {
|
||||
merged.push(user);
|
||||
}
|
||||
});
|
||||
|
||||
return merged;
|
||||
}, [searchResults, selectedValues]);
|
||||
|
||||
function getStatus() {
|
||||
if (isPending) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span
|
||||
aria-hidden
|
||||
className="inline-block size-3 animate-[spin_0.75s_linear_infinite] rounded-full border border-current border-r-transparent"
|
||||
/>
|
||||
Searching…
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (trimmedSearchValue === '' && !blockStartStatus) {
|
||||
return selectedValues.length > 0 ? null : 'Start typing to search people…';
|
||||
}
|
||||
|
||||
if (searchResults.length === 0 && !blockStartStatus) {
|
||||
return `No matches for "${trimmedSearchValue}".`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getEmptyMessage() {
|
||||
if (trimmedSearchValue === '' || isPending || searchResults.length > 0 || error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'Try a different search term.';
|
||||
}
|
||||
|
||||
const status = getStatus();
|
||||
const emptyMessage = getEmptyMessage();
|
||||
|
||||
return (
|
||||
<Combobox.Root
|
||||
items={items}
|
||||
itemToStringLabel={(user: DirectoryUser) => user.name}
|
||||
multiple
|
||||
filter={null}
|
||||
onOpenChangeComplete={(open) => {
|
||||
if (!open) {
|
||||
setSearchResults(selectedValuesRef.current);
|
||||
setBlockStartStatus(false);
|
||||
}
|
||||
}}
|
||||
onValueChange={(nextSelectedValues) => {
|
||||
selectedValuesRef.current = nextSelectedValues;
|
||||
setSelectedValues(nextSelectedValues);
|
||||
setSearchValue('');
|
||||
setError(null);
|
||||
|
||||
if (nextSelectedValues.length === 0) {
|
||||
setSearchResults([]);
|
||||
setBlockStartStatus(false);
|
||||
} else {
|
||||
setBlockStartStatus(true);
|
||||
}
|
||||
}}
|
||||
onInputValueChange={(nextSearchValue, { reason }) => {
|
||||
setSearchValue(nextSearchValue);
|
||||
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current?.abort();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
if (nextSearchValue === '') {
|
||||
setSearchResults(selectedValuesRef.current);
|
||||
setError(null);
|
||||
setBlockStartStatus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason === 'item-press') {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
setError(null);
|
||||
|
||||
const result = await searchUsers(nextSearchValue, contains);
|
||||
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
setSearchResults(result.users);
|
||||
setError(result.error);
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="max-w-md flex flex-col gap-1">
|
||||
<label
|
||||
className="flex flex-col gap-1 text-sm leading-5 font-bold text-neutral-950 dark:text-white"
|
||||
htmlFor={id}
|
||||
>
|
||||
Assign reviewers
|
||||
</label>
|
||||
<Combobox.InputGroup className="flex min-h-8 w-64 cursor-text flex-wrap items-center gap-0.5 border border-neutral-950 bg-white dark:bg-neutral-950 px-2 py-1 focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-neutral-950 dark:focus-within:outline-white has-[button]:px-1 dark:border-white [@media(min-width:32rem)]:w-[22rem]">
|
||||
<Combobox.Chips className="flex w-full flex-wrap items-center gap-1">
|
||||
<Combobox.Value>
|
||||
{(value: DirectoryUser[]) => (
|
||||
<React.Fragment>
|
||||
{value.map((user) => (
|
||||
<Combobox.Chip
|
||||
key={user.id}
|
||||
className="group flex min-h-[calc(1.5rem-2px)] cursor-default items-center gap-1 overflow-hidden bg-neutral-100 py-0 pr-[0.2rem] pl-[0.4rem] text-sm leading-none text-neutral-950 outline-none focus-within:bg-neutral-950 focus-within:text-white [@media(hover:hover)]:data-highlighted:bg-neutral-950 [@media(hover:hover)]:data-highlighted:text-white dark:bg-neutral-800 dark:text-white dark:focus-within:bg-white dark:focus-within:text-neutral-950 dark:[@media(hover:hover)]:data-highlighted:bg-white dark:[@media(hover:hover)]:data-highlighted:text-neutral-950"
|
||||
aria-label={user.name}
|
||||
>
|
||||
{user.name}
|
||||
<Combobox.ChipRemove
|
||||
className="flex size-4 items-center justify-center border-0 bg-transparent p-0 text-inherit hover:bg-neutral-200 group-focus-within:hover:bg-neutral-700 dark:hover:bg-neutral-700 dark:group-focus-within:hover:bg-neutral-200"
|
||||
aria-label={`Remove ${user.name}`}
|
||||
>
|
||||
<XIcon />
|
||||
</Combobox.ChipRemove>
|
||||
</Combobox.Chip>
|
||||
))}
|
||||
<Combobox.Input
|
||||
id={id}
|
||||
placeholder={value.length > 0 ? '' : 'e.g. Michael'}
|
||||
className="h-[calc(1.5rem-2px)] min-w-12 flex-1 border-0 bg-white p-0 text-sm any-pointer-coarse:text-base dark:bg-neutral-950 font-normal text-neutral-950 outline-none placeholder:text-neutral-500 dark:placeholder:text-neutral-400 dark:text-white"
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Combobox.Value>
|
||||
</Combobox.Chips>
|
||||
</Combobox.InputGroup>
|
||||
</div>
|
||||
|
||||
<Combobox.Portal>
|
||||
<Combobox.Positioner sideOffset={4}>
|
||||
<Combobox.Popup aria-busy={isPending || undefined}>
|
||||
<div className="max-h-[min(var(--available-height),24.5rem)] overflow-y-auto overscroll-contain py-1 scroll-pt-1 scroll-pb-1">
|
||||
<Combobox.Status>
|
||||
{status ? (
|
||||
<div className="flex items-center gap-2 py-1 pr-5 pl-2 text-sm leading-5 text-neutral-500 dark:text-neutral-400">
|
||||
{status}
|
||||
</div>
|
||||
) : null}
|
||||
</Combobox.Status>
|
||||
<Combobox.Empty>
|
||||
{emptyMessage ? (
|
||||
<div className="py-2 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
) : null}
|
||||
</Combobox.Empty>
|
||||
<Combobox.List>
|
||||
{(user: DirectoryUser) => (
|
||||
<Combobox.Item
|
||||
key={user.id}
|
||||
value={user}
|
||||
className="grid cursor-default grid-cols-[1rem_1fr] items-start gap-2 px-2 py-2 text-sm leading-[1.2rem] outline-none select-none [@media(hover:hover)]:data-highlighted:relative [@media(hover:hover)]:data-highlighted:z-0 [@media(hover:hover)]:data-highlighted:text-neutral-950 [@media(hover:hover)]:data-highlighted:before:absolute [@media(hover:hover)]:data-highlighted:before:inset-0 [@media(hover:hover)]:data-highlighted:before:z-[-1] [@media(hover:hover)]:data-highlighted:before:bg-neutral-100 dark:[@media(hover:hover)]:data-highlighted:text-white dark:[@media(hover:hover)]:data-highlighted:before:bg-neutral-800"
|
||||
>
|
||||
<Combobox.ItemIndicator className="col-start-1 mt-1">
|
||||
<CheckIcon />
|
||||
</Combobox.ItemIndicator>
|
||||
<span className="col-start-2 flex flex-col gap-1">
|
||||
<span className="text-sm leading-5 font-bold">{user.name}</span>
|
||||
<span className="text-xs">{user.email}</span>
|
||||
<span className="flex flex-wrap gap-2 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<span>@{user.username}</span>
|
||||
<span>{user.title}</span>
|
||||
</span>
|
||||
</span>
|
||||
</Combobox.Item>
|
||||
)}
|
||||
</Combobox.List>
|
||||
</div>
|
||||
</Combobox.Popup>
|
||||
</Combobox.Positioner>
|
||||
</Combobox.Portal>
|
||||
</Combobox.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m4.5 4.5 7 7m-7 0 7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
interface DirectoryUser {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
async function searchUsers(
|
||||
query: string,
|
||||
filter: (item: string, query: string) => boolean,
|
||||
): Promise<{ users: DirectoryUser[]; error: string | null }> {
|
||||
// Simulate network delay
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, Math.random() * 500 + 100);
|
||||
});
|
||||
|
||||
// Simulate occasional network errors (1% chance)
|
||||
if (Math.random() < 0.01 || query === 'will_error') {
|
||||
return {
|
||||
users: [],
|
||||
error: 'Failed to fetch people. Please try again.',
|
||||
};
|
||||
}
|
||||
|
||||
const users = allUsers.filter((user) => {
|
||||
return (
|
||||
filter(user.name, query) ||
|
||||
filter(user.username, query) ||
|
||||
filter(user.email, query) ||
|
||||
filter(user.title, query)
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
users,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
const allUsers: DirectoryUser[] = [
|
||||
{
|
||||
id: 'leslie-alexander',
|
||||
name: 'Leslie Alexander',
|
||||
username: 'leslie',
|
||||
email: 'leslie.alexander@example.com',
|
||||
title: 'Product Manager',
|
||||
},
|
||||
{
|
||||
id: 'kathryn-murphy',
|
||||
name: 'Kathryn Murphy',
|
||||
username: 'kathryn',
|
||||
email: 'kathryn.murphy@example.com',
|
||||
title: 'Marketing Lead',
|
||||
},
|
||||
{
|
||||
id: 'courtney-henry',
|
||||
name: 'Courtney Henry',
|
||||
username: 'courtney',
|
||||
email: 'courtney.henry@example.com',
|
||||
title: 'Design Systems',
|
||||
},
|
||||
{
|
||||
id: 'michael-foster',
|
||||
name: 'Michael Foster',
|
||||
username: 'michael',
|
||||
email: 'michael.foster@example.com',
|
||||
title: 'Engineering Manager',
|
||||
},
|
||||
{
|
||||
id: 'lindsay-walton',
|
||||
name: 'Lindsay Walton',
|
||||
username: 'lindsay',
|
||||
email: 'lindsay.walton@example.com',
|
||||
title: 'Product Designer',
|
||||
},
|
||||
{
|
||||
id: 'tom-cook',
|
||||
name: 'Tom Cook',
|
||||
username: 'tom',
|
||||
email: 'tom.cook@example.com',
|
||||
title: 'Frontend Engineer',
|
||||
},
|
||||
{
|
||||
id: 'whitney-francis',
|
||||
name: 'Whitney Francis',
|
||||
username: 'whitney',
|
||||
email: 'whitney.francis@example.com',
|
||||
title: 'Customer Success',
|
||||
},
|
||||
{
|
||||
id: 'jacob-jones',
|
||||
name: 'Jacob Jones',
|
||||
username: 'jacob',
|
||||
email: 'jacob.jones@example.com',
|
||||
title: 'Security Engineer',
|
||||
},
|
||||
{
|
||||
id: 'arlene-mccoy',
|
||||
name: 'Arlene McCoy',
|
||||
username: 'arlene',
|
||||
email: 'arlene.mccoy@example.com',
|
||||
title: 'Data Analyst',
|
||||
},
|
||||
{
|
||||
id: 'marvin-mckinney',
|
||||
name: 'Marvin McKinney',
|
||||
username: 'marvin',
|
||||
email: 'marvin.mckinney@example.com',
|
||||
title: 'QA Specialist',
|
||||
},
|
||||
{
|
||||
id: 'eleanor-pena',
|
||||
name: 'Eleanor Pena',
|
||||
username: 'eleanor',
|
||||
email: 'eleanor.pena@example.com',
|
||||
title: 'Operations',
|
||||
},
|
||||
{
|
||||
id: 'jerome-bell',
|
||||
name: 'Jerome Bell',
|
||||
username: 'jerome',
|
||||
email: 'jerome.bell@example.com',
|
||||
title: 'DevOps Engineer',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,351 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Combobox from '@/components/ui/combobox';
|
||||
|
||||
export default function ExampleAsyncSingleCombobox() {
|
||||
const id = React.useId();
|
||||
|
||||
const [searchResults, setSearchResults] = React.useState<DirectoryUser[]>([]);
|
||||
const [selectedValue, setSelectedValue] = React.useState<DirectoryUser | null>(null);
|
||||
const [searchValue, setSearchValue] = React.useState('');
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
|
||||
const { contains } = Combobox.useFilter();
|
||||
|
||||
const abortControllerRef = React.useRef<AbortController | null>(null);
|
||||
|
||||
const trimmedSearchValue = searchValue.trim();
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
if (!selectedValue || searchResults.some((user) => user.id === selectedValue.id)) {
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
return [...searchResults, selectedValue];
|
||||
}, [searchResults, selectedValue]);
|
||||
|
||||
function getStatus() {
|
||||
if (isPending) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span
|
||||
aria-hidden
|
||||
className="inline-block size-3 animate-spin rounded-full border border-current border-r-transparent"
|
||||
/>
|
||||
Searching…
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (trimmedSearchValue === '') {
|
||||
return selectedValue ? null : 'Start typing to search people…';
|
||||
}
|
||||
|
||||
if (searchResults.length === 0) {
|
||||
return `No matches for "${trimmedSearchValue}".`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getEmptyMessage() {
|
||||
if (trimmedSearchValue === '' || isPending || searchResults.length > 0 || error) {
|
||||
return null;
|
||||
}
|
||||
return 'Try a different search term.';
|
||||
}
|
||||
|
||||
const status = getStatus();
|
||||
const emptyMessage = getEmptyMessage();
|
||||
|
||||
return (
|
||||
<Combobox.Root
|
||||
items={items}
|
||||
itemToStringLabel={(user: DirectoryUser) => user.name}
|
||||
filter={null}
|
||||
onOpenChangeComplete={(open) => {
|
||||
if (!open && selectedValue) {
|
||||
setSearchResults([selectedValue]);
|
||||
}
|
||||
}}
|
||||
onValueChange={(nextSelectedValue) => {
|
||||
setSelectedValue(nextSelectedValue);
|
||||
setSearchValue('');
|
||||
setError(null);
|
||||
}}
|
||||
onInputValueChange={(nextSearchValue, { reason }) => {
|
||||
setSearchValue(nextSearchValue);
|
||||
|
||||
if (nextSearchValue === '') {
|
||||
setSearchResults([]);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason === 'item-press') {
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current?.abort();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
startTransition(async () => {
|
||||
setError(null);
|
||||
|
||||
const result = await searchUsers(nextSearchValue, contains);
|
||||
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
setSearchResults(result.users);
|
||||
setError(result.error);
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="relative flex flex-col gap-1 text-sm leading-5 font-bold text-neutral-950 dark:text-white">
|
||||
<label htmlFor={id}>Assign reviewer</label>
|
||||
<Combobox.InputGroup className="w-64 md:w-80">
|
||||
<Combobox.Input id={id} placeholder="e.g. Michael" />
|
||||
<div className="absolute right-0 bottom-0 flex h-full items-center justify-center text-neutral-500 dark:text-neutral-400">
|
||||
<Combobox.Clear aria-label="Clear selection">
|
||||
<XIcon />
|
||||
</Combobox.Clear>
|
||||
<Combobox.Trigger aria-label="Open popup">
|
||||
<CaretDownIcon />
|
||||
</Combobox.Trigger>
|
||||
</div>
|
||||
</Combobox.InputGroup>
|
||||
</div>
|
||||
|
||||
<Combobox.Portal>
|
||||
<Combobox.Positioner sideOffset={4}>
|
||||
<Combobox.Popup aria-busy={isPending || undefined}>
|
||||
<div className="max-h-[min(var(--available-height),22.5rem)] overflow-y-auto overscroll-contain py-1 scroll-pt-1 scroll-pb-1">
|
||||
<Combobox.Status>
|
||||
{status ? (
|
||||
<div className="flex items-center gap-2 py-1 pr-5 pl-2 text-sm leading-5 text-neutral-500 dark:text-neutral-400">
|
||||
{status}
|
||||
</div>
|
||||
) : null}
|
||||
</Combobox.Status>
|
||||
<Combobox.Empty>
|
||||
{emptyMessage ? (
|
||||
<div className="py-2 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
) : null}
|
||||
</Combobox.Empty>
|
||||
<Combobox.List>
|
||||
{(user: DirectoryUser) => (
|
||||
<Combobox.Item
|
||||
key={user.id}
|
||||
value={user}
|
||||
className="grid cursor-default grid-cols-[1rem_1fr] items-start gap-2 px-2 py-2 text-sm leading-[1.2rem] outline-none select-none [@media(hover:hover)]:data-highlighted:relative [@media(hover:hover)]:data-highlighted:z-0 [@media(hover:hover)]:data-highlighted:text-neutral-950 [@media(hover:hover)]:data-highlighted:before:absolute [@media(hover:hover)]:data-highlighted:before:inset-0 [@media(hover:hover)]:data-highlighted:before:z-[-1] [@media(hover:hover)]:data-highlighted:before:bg-neutral-100 dark:[@media(hover:hover)]:data-highlighted:text-white dark:[@media(hover:hover)]:data-highlighted:before:bg-neutral-800"
|
||||
>
|
||||
<Combobox.ItemIndicator className="col-start-1 mt-1">
|
||||
<CheckIcon />
|
||||
</Combobox.ItemIndicator>
|
||||
<span className="col-start-2 flex flex-col gap-1">
|
||||
<span className="text-sm leading-5 font-bold">{user.name}</span>
|
||||
<span className="text-xs">{user.email}</span>
|
||||
<span className="flex flex-wrap gap-2 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<span>@{user.username}</span>
|
||||
<span>{user.title}</span>
|
||||
</span>
|
||||
</span>
|
||||
</Combobox.Item>
|
||||
)}
|
||||
</Combobox.List>
|
||||
</div>
|
||||
</Combobox.Popup>
|
||||
</Combobox.Positioner>
|
||||
</Combobox.Portal>
|
||||
</Combobox.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m4.5 4.5 7 7m-7 0 7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CaretDownIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="M12 6H4l4 4.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface DirectoryUser {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
async function searchUsers(
|
||||
query: string,
|
||||
filter: (item: string, query: string) => boolean,
|
||||
): Promise<{ users: DirectoryUser[]; error: string | null }> {
|
||||
// Simulate network delay
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, Math.random() * 500 + 100);
|
||||
});
|
||||
|
||||
// Simulate occasional network errors (1% chance)
|
||||
if (Math.random() < 0.01 || query === 'will_error') {
|
||||
return {
|
||||
users: [],
|
||||
error: 'Failed to fetch people. Please try again.',
|
||||
};
|
||||
}
|
||||
|
||||
const users = allUsers.filter((user) => {
|
||||
return (
|
||||
filter(user.name, query) ||
|
||||
filter(user.username, query) ||
|
||||
filter(user.email, query) ||
|
||||
filter(user.title, query)
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
users,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
const allUsers: DirectoryUser[] = [
|
||||
{
|
||||
id: 'leslie-alexander',
|
||||
name: 'Leslie Alexander',
|
||||
username: 'leslie',
|
||||
email: 'leslie.alexander@example.com',
|
||||
title: 'Product Manager',
|
||||
},
|
||||
{
|
||||
id: 'kathryn-murphy',
|
||||
name: 'Kathryn Murphy',
|
||||
username: 'kathryn',
|
||||
email: 'kathryn.murphy@example.com',
|
||||
title: 'Marketing Lead',
|
||||
},
|
||||
{
|
||||
id: 'courtney-henry',
|
||||
name: 'Courtney Henry',
|
||||
username: 'courtney',
|
||||
email: 'courtney.henry@example.com',
|
||||
title: 'Design Systems',
|
||||
},
|
||||
{
|
||||
id: 'michael-foster',
|
||||
name: 'Michael Foster',
|
||||
username: 'michael',
|
||||
email: 'michael.foster@example.com',
|
||||
title: 'Engineering Manager',
|
||||
},
|
||||
{
|
||||
id: 'lindsay-walton',
|
||||
name: 'Lindsay Walton',
|
||||
username: 'lindsay',
|
||||
email: 'lindsay.walton@example.com',
|
||||
title: 'Product Designer',
|
||||
},
|
||||
{
|
||||
id: 'tom-cook',
|
||||
name: 'Tom Cook',
|
||||
username: 'tom',
|
||||
email: 'tom.cook@example.com',
|
||||
title: 'Frontend Engineer',
|
||||
},
|
||||
{
|
||||
id: 'whitney-francis',
|
||||
name: 'Whitney Francis',
|
||||
username: 'whitney',
|
||||
email: 'whitney.francis@example.com',
|
||||
title: 'Customer Success',
|
||||
},
|
||||
{
|
||||
id: 'jacob-jones',
|
||||
name: 'Jacob Jones',
|
||||
username: 'jacob',
|
||||
email: 'jacob.jones@example.com',
|
||||
title: 'Security Engineer',
|
||||
},
|
||||
{
|
||||
id: 'arlene-mccoy',
|
||||
name: 'Arlene McCoy',
|
||||
username: 'arlene',
|
||||
email: 'arlene.mccoy@example.com',
|
||||
title: 'Data Analyst',
|
||||
},
|
||||
{
|
||||
id: 'marvin-mckinney',
|
||||
name: 'Marvin McKinney',
|
||||
username: 'marvin',
|
||||
email: 'marvin.mckinney@example.com',
|
||||
title: 'QA Specialist',
|
||||
},
|
||||
{
|
||||
id: 'eleanor-pena',
|
||||
name: 'Eleanor Pena',
|
||||
username: 'eleanor',
|
||||
email: 'eleanor.pena@example.com',
|
||||
title: 'Operations',
|
||||
},
|
||||
{
|
||||
id: 'jerome-bell',
|
||||
name: 'Jerome Bell',
|
||||
username: 'jerome',
|
||||
email: 'jerome.bell@example.com',
|
||||
title: 'DevOps Engineer',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,130 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Combobox from '@/components/ui/combobox';
|
||||
|
||||
export default function ExampleCombobox() {
|
||||
const id = React.useId();
|
||||
return (
|
||||
<Combobox.Root items={fruits}>
|
||||
<div className="relative flex flex-col gap-1 text-sm leading-5 font-bold text-neutral-950 dark:text-white">
|
||||
<label htmlFor={id}>Choose a fruit</label>
|
||||
<Combobox.InputGroup>
|
||||
<Combobox.Input placeholder="e.g. Apple" id={id} />
|
||||
<div className="absolute right-0 bottom-0 flex h-full items-center justify-center text-neutral-500 dark:text-neutral-400">
|
||||
<Combobox.Clear aria-label="Clear selection">
|
||||
<XIcon />
|
||||
</Combobox.Clear>
|
||||
<Combobox.Trigger aria-label="Open popup">
|
||||
<CaretDownIcon />
|
||||
</Combobox.Trigger>
|
||||
</div>
|
||||
</Combobox.InputGroup>
|
||||
</div>
|
||||
|
||||
<Combobox.Portal>
|
||||
<Combobox.Positioner sideOffset={4}>
|
||||
<Combobox.Popup>
|
||||
<Combobox.Empty>
|
||||
<div className="py-4 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No fruits found.
|
||||
</div>
|
||||
</Combobox.Empty>
|
||||
<Combobox.List>
|
||||
{(item: Fruit) => (
|
||||
<Combobox.Item key={item.value} value={item}>
|
||||
<Combobox.ItemIndicator>
|
||||
<CheckIcon />
|
||||
</Combobox.ItemIndicator>
|
||||
<span className="col-start-2">{item.label}</span>
|
||||
</Combobox.Item>
|
||||
)}
|
||||
</Combobox.List>
|
||||
</Combobox.Popup>
|
||||
</Combobox.Positioner>
|
||||
</Combobox.Portal>
|
||||
</Combobox.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m4.5 4.5 7 7m-7 0 7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CaretDownIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="M12 6H4l4 4.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface Fruit {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const fruits: Fruit[] = [
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
{ label: 'Banana', value: 'banana' },
|
||||
{ label: 'Orange', value: 'orange' },
|
||||
{ label: 'Pineapple', value: 'pineapple' },
|
||||
{ label: 'Grape', value: 'grape' },
|
||||
{ label: 'Mango', value: 'mango' },
|
||||
{ label: 'Strawberry', value: 'strawberry' },
|
||||
{ label: 'Blueberry', value: 'blueberry' },
|
||||
{ label: 'Raspberry', value: 'raspberry' },
|
||||
{ label: 'Blackberry', value: 'blackberry' },
|
||||
{ label: 'Cherry', value: 'cherry' },
|
||||
{ label: 'Peach', value: 'peach' },
|
||||
{ label: 'Pear', value: 'pear' },
|
||||
{ label: 'Plum', value: 'plum' },
|
||||
{ label: 'Kiwi', value: 'kiwi' },
|
||||
{ label: 'Watermelon', value: 'watermelon' },
|
||||
{ label: 'Cantaloupe', value: 'cantaloupe' },
|
||||
{ label: 'Honeydew', value: 'honeydew' },
|
||||
{ label: 'Papaya', value: 'papaya' },
|
||||
{ label: 'Guava', value: 'guava' },
|
||||
{ label: 'Lychee', value: 'lychee' },
|
||||
{ label: 'Pomegranate', value: 'pomegranate' },
|
||||
{ label: 'Apricot', value: 'apricot' },
|
||||
{ label: 'Grapefruit', value: 'grapefruit' },
|
||||
{ label: 'Passionfruit', value: 'passionfruit' },
|
||||
];
|
||||
@@ -0,0 +1,307 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Combobox from '@/components/ui/combobox';
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
|
||||
export default function ExampleCreatableCombobox() {
|
||||
const id = React.useId();
|
||||
|
||||
const [labels, setLabels] = React.useState<LabelItem[]>(initialLabels);
|
||||
const [selected, setSelected] = React.useState<LabelItem[]>([]);
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [openDialog, setOpenDialog] = React.useState(false);
|
||||
|
||||
const createInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const comboboxInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const pendingQueryRef = React.useRef('');
|
||||
const highlightedItemRef = React.useRef<LabelItem | undefined>(undefined);
|
||||
|
||||
function handleInputKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key !== 'Enter' || highlightedItemRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTrimmed = query.trim();
|
||||
if (currentTrimmed === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = currentTrimmed.toLocaleLowerCase();
|
||||
const existing = labels.find((label) => label.value.trim().toLocaleLowerCase() === normalized);
|
||||
|
||||
if (existing) {
|
||||
setSelected((prev) =>
|
||||
prev.some((item) => item.id === existing.id) ? prev : [...prev, existing],
|
||||
);
|
||||
setQuery('');
|
||||
return;
|
||||
}
|
||||
|
||||
pendingQueryRef.current = currentTrimmed;
|
||||
setOpenDialog(true);
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
const input = createInputRef.current || comboboxInputRef.current;
|
||||
const value = input ? input.value.trim() : '';
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = value.toLocaleLowerCase();
|
||||
const baseId = normalized.replace(/\s+/g, '-');
|
||||
const existing = labels.find((l) => l.value.trim().toLocaleLowerCase() === normalized);
|
||||
|
||||
if (existing) {
|
||||
setSelected((prev) => (prev.some((i) => i.id === existing.id) ? prev : [...prev, existing]));
|
||||
setOpenDialog(false);
|
||||
setQuery('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure we don't collide with an existing id (e.g., value "docs" vs. existing id "docs")
|
||||
const existingIds = new Set(labels.map((l) => l.id));
|
||||
let uniqueId = baseId;
|
||||
if (existingIds.has(uniqueId)) {
|
||||
let i = 2;
|
||||
while (existingIds.has(`${baseId}-${i}`)) {
|
||||
i += 1;
|
||||
}
|
||||
uniqueId = `${baseId}-${i}`;
|
||||
}
|
||||
|
||||
const newItem: LabelItem = { id: uniqueId, value };
|
||||
|
||||
if (!selected.find((item) => item.id === newItem.id)) {
|
||||
setLabels((prev) => [...prev, newItem]);
|
||||
setSelected((prev) => [...prev, newItem]);
|
||||
}
|
||||
|
||||
setOpenDialog(false);
|
||||
setQuery('');
|
||||
}
|
||||
|
||||
function handleCreateSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
handleCreate();
|
||||
}
|
||||
|
||||
const trimmed = query.trim();
|
||||
const lowered = trimmed.toLocaleLowerCase();
|
||||
const exactExists = labels.some((l) => l.value.trim().toLocaleLowerCase() === lowered);
|
||||
// Show the creatable item alongside matches if there's no exact match
|
||||
const itemsForView: Array<LabelItem> =
|
||||
trimmed !== '' && !exactExists
|
||||
? [...labels, { creatable: trimmed, id: `create:${lowered}`, value: `Create "${trimmed}"` }]
|
||||
: labels;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Combobox.Root
|
||||
items={itemsForView}
|
||||
multiple
|
||||
onValueChange={(next) => {
|
||||
const creatableSelection = next.find(
|
||||
(item) => item.creatable && !selected.some((current) => current.id === item.id),
|
||||
);
|
||||
|
||||
if (creatableSelection && creatableSelection.creatable) {
|
||||
pendingQueryRef.current = creatableSelection.creatable;
|
||||
setOpenDialog(true);
|
||||
return;
|
||||
}
|
||||
const clean = next.filter((i) => !i.creatable);
|
||||
setSelected(clean);
|
||||
setQuery('');
|
||||
}}
|
||||
value={selected}
|
||||
inputValue={query}
|
||||
onInputValueChange={setQuery}
|
||||
onItemHighlighted={(item) => {
|
||||
highlightedItemRef.current = item;
|
||||
}}
|
||||
>
|
||||
<div className="max-w-md flex flex-col gap-1">
|
||||
<label
|
||||
className="flex flex-col gap-1 text-sm leading-5 font-bold text-neutral-950 dark:text-white"
|
||||
htmlFor={id}
|
||||
>
|
||||
Labels
|
||||
</label>
|
||||
<Combobox.InputGroup className="flex min-h-8 w-64 cursor-text flex-wrap items-center gap-0.5 border border-neutral-950 bg-white dark:bg-neutral-950 px-2 py-1 focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-neutral-950 dark:focus-within:outline-white has-[button]:px-1 dark:border-white [@media(min-width:32rem)]:w-[22rem]">
|
||||
<Combobox.Chips className="flex w-full flex-wrap items-center gap-1">
|
||||
<Combobox.Value>
|
||||
{(value: LabelItem[]) => (
|
||||
<React.Fragment>
|
||||
{value.map((label) => (
|
||||
<Combobox.Chip
|
||||
key={label.id}
|
||||
className="group flex min-h-[calc(1.5rem-2px)] cursor-default items-center gap-1 overflow-hidden bg-neutral-100 py-0 pr-[0.2rem] pl-[0.4rem] text-sm leading-none text-neutral-950 outline-none focus-within:bg-neutral-950 focus-within:text-white [@media(hover:hover)]:data-highlighted:bg-neutral-950 [@media(hover:hover)]:data-highlighted:text-white dark:bg-neutral-800 dark:text-white dark:focus-within:bg-white dark:focus-within:text-neutral-950 dark:[@media(hover:hover)]:data-highlighted:bg-white dark:[@media(hover:hover)]:data-highlighted:text-neutral-950"
|
||||
aria-label={label.value}
|
||||
>
|
||||
{label.value}
|
||||
<Combobox.ChipRemove
|
||||
className="flex size-4 items-center justify-center border-0 bg-transparent p-0 text-inherit hover:bg-neutral-200 group-focus-within:hover:bg-neutral-700 dark:hover:bg-neutral-700 dark:group-focus-within:hover:bg-neutral-200"
|
||||
aria-label={`Remove ${label.value}`}
|
||||
>
|
||||
<XIcon />
|
||||
</Combobox.ChipRemove>
|
||||
</Combobox.Chip>
|
||||
))}
|
||||
<Combobox.Input
|
||||
ref={comboboxInputRef}
|
||||
id={id}
|
||||
placeholder={value.length > 0 ? '' : 'e.g. bug'}
|
||||
className="h-[calc(1.5rem-2px)] min-w-12 flex-1 border-0 bg-white p-0 text-sm any-pointer-coarse:text-base dark:bg-neutral-950 font-normal text-neutral-950 outline-none placeholder:text-neutral-500 dark:placeholder:text-neutral-400 dark:text-white"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Combobox.Value>
|
||||
</Combobox.Chips>
|
||||
</Combobox.InputGroup>
|
||||
</div>
|
||||
|
||||
<Combobox.Portal>
|
||||
<Combobox.Positioner className="z-50" sideOffset={4}>
|
||||
<Combobox.Popup>
|
||||
<Combobox.Empty>
|
||||
<div className="py-2 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No labels found.
|
||||
</div>
|
||||
</Combobox.Empty>
|
||||
<Combobox.List>
|
||||
{(item: LabelItem) =>
|
||||
item.creatable ? (
|
||||
<Combobox.Item
|
||||
key={item.id}
|
||||
className="grid cursor-default grid-cols-[1rem_1fr] items-center gap-2 p-2 text-sm leading-4 outline-none select-none data-selected:relative data-selected:z-0 data-selected:text-neutral-950 data-selected:before:absolute data-selected:before:inset-0 data-selected:before:z-[-1] [@media(hover:hover)]:data-highlighted:relative [@media(hover:hover)]:data-highlighted:z-0 [@media(hover:hover)]:data-highlighted:text-white [@media(hover:hover)]:data-highlighted:before:absolute [@media(hover:hover)]:data-highlighted:before:inset-0 [@media(hover:hover)]:data-highlighted:before:z-[-1] [@media(hover:hover)]:data-highlighted:before:bg-neutral-950 dark:data-selected:text-white dark:[@media(hover:hover)]:data-highlighted:text-neutral-950 dark:[@media(hover:hover)]:data-highlighted:before:bg-white"
|
||||
value={item}
|
||||
>
|
||||
<span className="col-start-1">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
<span className="col-start-2">Create "{item.creatable}"</span>
|
||||
</Combobox.Item>
|
||||
) : (
|
||||
<Combobox.Item
|
||||
key={item.id}
|
||||
className="grid cursor-default grid-cols-[1rem_1fr] items-center gap-2 p-2 text-sm leading-4 outline-none select-none data-selected:relative data-selected:z-0 data-selected:text-neutral-950 data-selected:before:absolute data-selected:before:inset-0 data-selected:before:z-[-1] [@media(hover:hover)]:data-highlighted:relative [@media(hover:hover)]:data-highlighted:z-0 [@media(hover:hover)]:data-highlighted:text-white [@media(hover:hover)]:data-highlighted:before:absolute [@media(hover:hover)]:data-highlighted:before:inset-0 [@media(hover:hover)]:data-highlighted:before:z-[-1] [@media(hover:hover)]:data-highlighted:before:bg-neutral-950 dark:data-selected:text-white dark:[@media(hover:hover)]:data-highlighted:text-neutral-950 dark:[@media(hover:hover)]:data-highlighted:before:bg-white"
|
||||
value={item}
|
||||
>
|
||||
<Combobox.ItemIndicator className="col-start-1">
|
||||
<CheckIcon />
|
||||
</Combobox.ItemIndicator>
|
||||
<span className="col-start-2">{item.value}</span>
|
||||
</Combobox.Item>
|
||||
)
|
||||
}
|
||||
</Combobox.List>
|
||||
</Combobox.Popup>
|
||||
</Combobox.Positioner>
|
||||
</Combobox.Portal>
|
||||
</Combobox.Root>
|
||||
|
||||
<Dialog.Root open={openDialog} onOpenChange={setOpenDialog}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className="fixed inset-0 min-h-dvh bg-black opacity-20 transition-opacity dark:opacity-70 data-starting-style:opacity-0 data-ending-style:opacity-0 supports-[-webkit-touch-callout:none]:absolute" />
|
||||
<Dialog.Popup
|
||||
className="fixed top-1/2 left-1/2 mt-[-2rem] w-[24rem] max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 border border-neutral-950 bg-white p-6 text-neutral-950 shadow-[0.25rem_0.25rem_0_rgb(0_0_0_/_12%)] transition-all data-starting-style:scale-90 data-starting-style:opacity-0 data-ending-style:scale-90 data-ending-style:opacity-0 dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none"
|
||||
initialFocus={createInputRef}
|
||||
>
|
||||
<Dialog.Title className="text-sm leading-5 font-bold">Create new label</Dialog.Title>
|
||||
<Dialog.Description className="mb-4 text-sm leading-5 text-neutral-600 dark:text-neutral-400">
|
||||
Add a new label to select.
|
||||
</Dialog.Description>
|
||||
<form onSubmit={handleCreateSubmit}>
|
||||
<input
|
||||
ref={createInputRef}
|
||||
className="h-8 w-full border border-neutral-950 bg-white dark:bg-neutral-950 px-2 text-sm any-pointer-coarse:text-base font-normal text-neutral-950 placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white dark:border-white dark:text-white"
|
||||
placeholder="Label name"
|
||||
defaultValue={pendingQueryRef.current}
|
||||
/>
|
||||
<div className="mt-4 flex justify-end gap-3">
|
||||
<Dialog.Close className="flex h-8 items-center justify-center gap-2 border border-neutral-950 bg-white px-3 text-sm whitespace-nowrap font-normal text-neutral-950 select-none hover:bg-neutral-100 active:bg-neutral-200 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700">
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex h-8 items-center justify-center gap-2 border border-neutral-950 bg-white px-3 text-sm whitespace-nowrap font-normal text-neutral-950 select-none hover:bg-neutral-100 active:bg-neutral-200 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PlusIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="M1.5 8h13M8 14.5v-13" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m4.5 4.5 7 7m-7 0 7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface LabelItem {
|
||||
creatable?: string;
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const initialLabels: LabelItem[] = [
|
||||
{ id: 'bug', value: 'bug' },
|
||||
{ id: 'docs', value: 'documentation' },
|
||||
{ id: 'enhancement', value: 'enhancement' },
|
||||
{ id: 'help-wanted', value: 'help wanted' },
|
||||
{ id: 'good-first-issue', value: 'good first issue' },
|
||||
];
|
||||
@@ -0,0 +1,151 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Combobox from '@/components/ui/combobox';
|
||||
|
||||
export default function ExampleGroupedCombobox() {
|
||||
const id = React.useId();
|
||||
return (
|
||||
<Combobox.Root items={groupedProduce}>
|
||||
<div className="relative flex flex-col gap-1 text-sm leading-5 font-bold text-neutral-950 dark:text-white">
|
||||
<label htmlFor={id}>Select produce</label>
|
||||
<Combobox.InputGroup className="w-64">
|
||||
<Combobox.Input placeholder="e.g. Mango" id={id} />
|
||||
<div className="absolute right-0 bottom-0 flex h-full items-center justify-center text-neutral-500 dark:text-neutral-400">
|
||||
<Combobox.Clear aria-label="Clear selection">
|
||||
<XIcon />
|
||||
</Combobox.Clear>
|
||||
<Combobox.Trigger aria-label="Open popup">
|
||||
<CaretDownIcon />
|
||||
</Combobox.Trigger>
|
||||
</div>
|
||||
</Combobox.InputGroup>
|
||||
</div>
|
||||
|
||||
<Combobox.Portal>
|
||||
<Combobox.Positioner sideOffset={4}>
|
||||
<Combobox.Popup>
|
||||
<Combobox.Empty>
|
||||
<div className="py-4 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No produce found.
|
||||
</div>
|
||||
</Combobox.Empty>
|
||||
<Combobox.List>
|
||||
{(group: ProduceGroup) => (
|
||||
<Combobox.Group
|
||||
key={group.value}
|
||||
items={group.items}
|
||||
className="block pb-2 last:pb-0"
|
||||
>
|
||||
<Combobox.GroupLabel className="py-2 pr-2 pl-8 text-sm leading-4 text-neutral-500 select-none dark:text-neutral-400">
|
||||
{group.value}
|
||||
</Combobox.GroupLabel>
|
||||
<Combobox.Collection>
|
||||
{(item: Produce) => (
|
||||
<Combobox.Item key={item.id} value={item}>
|
||||
<Combobox.ItemIndicator className="col-start-1 flex items-center justify-center">
|
||||
<CheckIcon />
|
||||
</Combobox.ItemIndicator>
|
||||
<span className="col-start-2">{item.label}</span>
|
||||
</Combobox.Item>
|
||||
)}
|
||||
</Combobox.Collection>
|
||||
</Combobox.Group>
|
||||
)}
|
||||
</Combobox.List>
|
||||
</Combobox.Popup>
|
||||
</Combobox.Positioner>
|
||||
</Combobox.Portal>
|
||||
</Combobox.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m4.5 4.5 7 7m-7 0 7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CaretDownIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="M12 6H4l4 4.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface Produce {
|
||||
id: string;
|
||||
label: string;
|
||||
group: 'Fruits' | 'Vegetables';
|
||||
}
|
||||
|
||||
interface ProduceGroup {
|
||||
value: string;
|
||||
items: Produce[];
|
||||
}
|
||||
|
||||
const produceData: Produce[] = [
|
||||
{ id: 'fruit-apple', label: 'Apple', group: 'Fruits' },
|
||||
{ id: 'fruit-banana', label: 'Banana', group: 'Fruits' },
|
||||
{ id: 'fruit-mango', label: 'Mango', group: 'Fruits' },
|
||||
{ id: 'fruit-kiwi', label: 'Kiwi', group: 'Fruits' },
|
||||
{ id: 'fruit-grape', label: 'Grape', group: 'Fruits' },
|
||||
{ id: 'fruit-orange', label: 'Orange', group: 'Fruits' },
|
||||
{ id: 'fruit-strawberry', label: 'Strawberry', group: 'Fruits' },
|
||||
{ id: 'fruit-watermelon', label: 'Watermelon', group: 'Fruits' },
|
||||
{ id: 'veg-broccoli', label: 'Broccoli', group: 'Vegetables' },
|
||||
{ id: 'veg-carrot', label: 'Carrot', group: 'Vegetables' },
|
||||
{ id: 'veg-cauliflower', label: 'Cauliflower', group: 'Vegetables' },
|
||||
{ id: 'veg-cucumber', label: 'Cucumber', group: 'Vegetables' },
|
||||
{ id: 'veg-kale', label: 'Kale', group: 'Vegetables' },
|
||||
{ id: 'veg-pepper', label: 'Bell pepper', group: 'Vegetables' },
|
||||
{ id: 'veg-spinach', label: 'Spinach', group: 'Vegetables' },
|
||||
{ id: 'veg-zucchini', label: 'Zucchini', group: 'Vegetables' },
|
||||
];
|
||||
|
||||
function groupProduce(items: Produce[]): ProduceGroup[] {
|
||||
const groups: Record<string, Produce[]> = {};
|
||||
items.forEach((item) => {
|
||||
(groups[item.group] ??= []).push(item);
|
||||
});
|
||||
const order = ['Fruits', 'Vegetables'];
|
||||
return order.map((value) => ({ value, items: groups[value] ?? [] }));
|
||||
}
|
||||
|
||||
const groupedProduce: ProduceGroup[] = groupProduce(produceData);
|
||||
@@ -0,0 +1,304 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Combobox from '@/components/ui/combobox';
|
||||
|
||||
export default function ExamplePopoverCombobox() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<Combobox.Root items={countries}>
|
||||
<Combobox.Label className="cursor-default text-sm leading-5 font-bold text-neutral-950 dark:text-white">
|
||||
Country
|
||||
</Combobox.Label>
|
||||
<Combobox.Trigger className="flex h-8 min-w-40 cursor-default items-center justify-between gap-3 border border-neutral-950 bg-white pl-2 pr-1 text-sm leading-none whitespace-nowrap font-normal text-neutral-950 select-none hover:bg-neutral-100 active:bg-neutral-200 data-popup-open:bg-neutral-100 data-placeholder:text-neutral-500 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:data-placeholder:text-neutral-400 dark:focus-visible:outline-white dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700 dark:data-popup-open:bg-neutral-800">
|
||||
<Combobox.Value placeholder="Select country" />
|
||||
<Combobox.Icon className="text-neutral-950 dark:text-white">
|
||||
<CaretUpDownIcon />
|
||||
</Combobox.Icon>
|
||||
</Combobox.Trigger>
|
||||
<Combobox.Portal>
|
||||
<Combobox.Positioner align="start" sideOffset={4}>
|
||||
<Combobox.Popup
|
||||
className="[--input-container-height:2rem] max-h-[24.5rem] max-w-[var(--available-width)] origin-[var(--transform-origin)] bg-white text-neutral-950 shadow-[0.25rem_0.25rem_0_rgb(0_0_0_/_12%)] transition-[transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out data-starting-style:scale-90 data-starting-style:opacity-0 data-ending-style:scale-90 data-ending-style:opacity-0 dark:bg-neutral-950 dark:text-white dark:shadow-none"
|
||||
aria-label="Select country"
|
||||
>
|
||||
<Combobox.Input
|
||||
placeholder="e.g. United Kingdom"
|
||||
className="h-8 w-full min-w-80 border border-neutral-950 bg-white px-2 text-sm font-normal text-neutral-950 placeholder:text-neutral-500 focus:outline-2 focus:-outline-offset-2 focus:outline-neutral-950 any-pointer-coarse:text-base dark:bg-neutral-950 dark:text-white dark:placeholder:text-neutral-400 dark:border-white dark:focus:outline-white"
|
||||
/>
|
||||
<div className="border-x border-b border-neutral-950 dark:border-white">
|
||||
<Combobox.Empty>
|
||||
<div className="py-4 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No countries found.
|
||||
</div>
|
||||
</Combobox.Empty>
|
||||
<Combobox.List className="max-h-[min(calc(24.5rem-var(--input-container-height)-2px),calc(var(--available-height)-var(--input-container-height)-2px))] overflow-auto overscroll-contain py-1 scroll-py-1 empty:p-0">
|
||||
{(country: Country) => (
|
||||
<Combobox.Item
|
||||
key={country.code}
|
||||
value={country}
|
||||
className="grid min-w-[var(--anchor-width)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 p-2 text-sm leading-4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-white data-highlighted:before:absolute data-highlighted:before:inset-0 data-highlighted:before:z-[-1] data-highlighted:before:bg-neutral-950 dark:data-highlighted:text-neutral-950 dark:data-highlighted:before:bg-white"
|
||||
>
|
||||
<Combobox.ItemIndicator className="col-start-1">
|
||||
<CheckIcon />
|
||||
</Combobox.ItemIndicator>
|
||||
<span className="col-start-2">{country.label}</span>
|
||||
</Combobox.Item>
|
||||
)}
|
||||
</Combobox.List>
|
||||
</div>
|
||||
</Combobox.Popup>
|
||||
</Combobox.Positioner>
|
||||
</Combobox.Portal>
|
||||
</Combobox.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CaretUpDownIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="M11 10H5l3 3.5zm0-4H5l3-3.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface Country {
|
||||
code: string;
|
||||
value: string;
|
||||
continent: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const countries: Country[] = [
|
||||
{ code: 'af', value: 'afghanistan', label: 'Afghanistan', continent: 'Asia' },
|
||||
{ code: 'al', value: 'albania', label: 'Albania', continent: 'Europe' },
|
||||
{ code: 'dz', value: 'algeria', label: 'Algeria', continent: 'Africa' },
|
||||
{ code: 'ad', value: 'andorra', label: 'Andorra', continent: 'Europe' },
|
||||
{ code: 'ao', value: 'angola', label: 'Angola', continent: 'Africa' },
|
||||
{ code: 'ar', value: 'argentina', label: 'Argentina', continent: 'South America' },
|
||||
{ code: 'am', value: 'armenia', label: 'Armenia', continent: 'Asia' },
|
||||
{ code: 'au', value: 'australia', label: 'Australia', continent: 'Oceania' },
|
||||
{ code: 'at', value: 'austria', label: 'Austria', continent: 'Europe' },
|
||||
{ code: 'az', value: 'azerbaijan', label: 'Azerbaijan', continent: 'Asia' },
|
||||
{ code: 'bs', value: 'bahamas', label: 'Bahamas', continent: 'North America' },
|
||||
{ code: 'bh', value: 'bahrain', label: 'Bahrain', continent: 'Asia' },
|
||||
{ code: 'bd', value: 'bangladesh', label: 'Bangladesh', continent: 'Asia' },
|
||||
{ code: 'bb', value: 'barbados', label: 'Barbados', continent: 'North America' },
|
||||
{ code: 'by', value: 'belarus', label: 'Belarus', continent: 'Europe' },
|
||||
{ code: 'be', value: 'belgium', label: 'Belgium', continent: 'Europe' },
|
||||
{ code: 'bz', value: 'belize', label: 'Belize', continent: 'North America' },
|
||||
{ code: 'bj', value: 'benin', label: 'Benin', continent: 'Africa' },
|
||||
{ code: 'bt', value: 'bhutan', label: 'Bhutan', continent: 'Asia' },
|
||||
{ code: 'bo', value: 'bolivia', label: 'Bolivia', continent: 'South America' },
|
||||
{
|
||||
code: 'ba',
|
||||
value: 'bosnia-and-herzegovina',
|
||||
label: 'Bosnia and Herzegovina',
|
||||
continent: 'Europe',
|
||||
},
|
||||
{ code: 'bw', value: 'botswana', label: 'Botswana', continent: 'Africa' },
|
||||
{ code: 'br', value: 'brazil', label: 'Brazil', continent: 'South America' },
|
||||
{ code: 'bn', value: 'brunei', label: 'Brunei', continent: 'Asia' },
|
||||
{ code: 'bg', value: 'bulgaria', label: 'Bulgaria', continent: 'Europe' },
|
||||
{ code: 'bf', value: 'burkina-faso', label: 'Burkina Faso', continent: 'Africa' },
|
||||
{ code: 'bi', value: 'burundi', label: 'Burundi', continent: 'Africa' },
|
||||
{ code: 'kh', value: 'cambodia', label: 'Cambodia', continent: 'Asia' },
|
||||
{ code: 'cm', value: 'cameroon', label: 'Cameroon', continent: 'Africa' },
|
||||
{ code: 'ca', value: 'canada', label: 'Canada', continent: 'North America' },
|
||||
{ code: 'cv', value: 'cape-verde', label: 'Cape Verde', continent: 'Africa' },
|
||||
{
|
||||
code: 'cf',
|
||||
value: 'central-african-republic',
|
||||
label: 'Central African Republic',
|
||||
continent: 'Africa',
|
||||
},
|
||||
{ code: 'td', value: 'chad', label: 'Chad', continent: 'Africa' },
|
||||
{ code: 'cl', value: 'chile', label: 'Chile', continent: 'South America' },
|
||||
{ code: 'cn', value: 'china', label: 'China', continent: 'Asia' },
|
||||
{ code: 'co', value: 'colombia', label: 'Colombia', continent: 'South America' },
|
||||
{ code: 'km', value: 'comoros', label: 'Comoros', continent: 'Africa' },
|
||||
{ code: 'cg', value: 'congo', label: 'Congo', continent: 'Africa' },
|
||||
{ code: 'cr', value: 'costa-rica', label: 'Costa Rica', continent: 'North America' },
|
||||
{ code: 'hr', value: 'croatia', label: 'Croatia', continent: 'Europe' },
|
||||
{ code: 'cu', value: 'cuba', label: 'Cuba', continent: 'North America' },
|
||||
{ code: 'cy', value: 'cyprus', label: 'Cyprus', continent: 'Asia' },
|
||||
{ code: 'cz', value: 'czech-republic', label: 'Czech Republic', continent: 'Europe' },
|
||||
{ code: 'dk', value: 'denmark', label: 'Denmark', continent: 'Europe' },
|
||||
{ code: 'dj', value: 'djibouti', label: 'Djibouti', continent: 'Africa' },
|
||||
{ code: 'dm', value: 'dominica', label: 'Dominica', continent: 'North America' },
|
||||
{
|
||||
code: 'do',
|
||||
value: 'dominican-republic',
|
||||
label: 'Dominican Republic',
|
||||
continent: 'North America',
|
||||
},
|
||||
{ code: 'ec', value: 'ecuador', label: 'Ecuador', continent: 'South America' },
|
||||
{ code: 'eg', value: 'egypt', label: 'Egypt', continent: 'Africa' },
|
||||
{ code: 'sv', value: 'el-salvador', label: 'El Salvador', continent: 'North America' },
|
||||
{ code: 'gq', value: 'equatorial-guinea', label: 'Equatorial Guinea', continent: 'Africa' },
|
||||
{ code: 'er', value: 'eritrea', label: 'Eritrea', continent: 'Africa' },
|
||||
{ code: 'ee', value: 'estonia', label: 'Estonia', continent: 'Europe' },
|
||||
{ code: 'et', value: 'ethiopia', label: 'Ethiopia', continent: 'Africa' },
|
||||
{ code: 'fj', value: 'fiji', label: 'Fiji', continent: 'Oceania' },
|
||||
{ code: 'fi', value: 'finland', label: 'Finland', continent: 'Europe' },
|
||||
{ code: 'fr', value: 'france', label: 'France', continent: 'Europe' },
|
||||
{ code: 'ga', value: 'gabon', label: 'Gabon', continent: 'Africa' },
|
||||
{ code: 'gm', value: 'gambia', label: 'Gambia', continent: 'Africa' },
|
||||
{ code: 'ge', value: 'georgia', label: 'Georgia', continent: 'Asia' },
|
||||
{ code: 'de', value: 'germany', label: 'Germany', continent: 'Europe' },
|
||||
{ code: 'gh', value: 'ghana', label: 'Ghana', continent: 'Africa' },
|
||||
{ code: 'gr', value: 'greece', label: 'Greece', continent: 'Europe' },
|
||||
{ code: 'gd', value: 'grenada', label: 'Grenada', continent: 'North America' },
|
||||
{ code: 'gt', value: 'guatemala', label: 'Guatemala', continent: 'North America' },
|
||||
{ code: 'gn', value: 'guinea', label: 'Guinea', continent: 'Africa' },
|
||||
{ code: 'gw', value: 'guinea-bissau', label: 'Guinea-Bissau', continent: 'Africa' },
|
||||
{ code: 'gy', value: 'guyana', label: 'Guyana', continent: 'South America' },
|
||||
{ code: 'ht', value: 'haiti', label: 'Haiti', continent: 'North America' },
|
||||
{ code: 'hn', value: 'honduras', label: 'Honduras', continent: 'North America' },
|
||||
{ code: 'hu', value: 'hungary', label: 'Hungary', continent: 'Europe' },
|
||||
{ code: 'is', value: 'iceland', label: 'Iceland', continent: 'Europe' },
|
||||
{ code: 'in', value: 'india', label: 'India', continent: 'Asia' },
|
||||
{ code: 'id', value: 'indonesia', label: 'Indonesia', continent: 'Asia' },
|
||||
{ code: 'ir', value: 'iran', label: 'Iran', continent: 'Asia' },
|
||||
{ code: 'iq', value: 'iraq', label: 'Iraq', continent: 'Asia' },
|
||||
{ code: 'ie', value: 'ireland', label: 'Ireland', continent: 'Europe' },
|
||||
{ code: 'il', value: 'israel', label: 'Israel', continent: 'Asia' },
|
||||
{ code: 'it', value: 'italy', label: 'Italy', continent: 'Europe' },
|
||||
{ code: 'jm', value: 'jamaica', label: 'Jamaica', continent: 'North America' },
|
||||
{ code: 'jp', value: 'japan', label: 'Japan', continent: 'Asia' },
|
||||
{ code: 'jo', value: 'jordan', label: 'Jordan', continent: 'Asia' },
|
||||
{ code: 'kz', value: 'kazakhstan', label: 'Kazakhstan', continent: 'Asia' },
|
||||
{ code: 'ke', value: 'kenya', label: 'Kenya', continent: 'Africa' },
|
||||
{ code: 'kw', value: 'kuwait', label: 'Kuwait', continent: 'Asia' },
|
||||
{ code: 'kg', value: 'kyrgyzstan', label: 'Kyrgyzstan', continent: 'Asia' },
|
||||
{ code: 'la', value: 'laos', label: 'Laos', continent: 'Asia' },
|
||||
{ code: 'lv', value: 'latvia', label: 'Latvia', continent: 'Europe' },
|
||||
{ code: 'lb', value: 'lebanon', label: 'Lebanon', continent: 'Asia' },
|
||||
{ code: 'ls', value: 'lesotho', label: 'Lesotho', continent: 'Africa' },
|
||||
{ code: 'lr', value: 'liberia', label: 'Liberia', continent: 'Africa' },
|
||||
{ code: 'ly', value: 'libya', label: 'Libya', continent: 'Africa' },
|
||||
{ code: 'li', value: 'liechtenstein', label: 'Liechtenstein', continent: 'Europe' },
|
||||
{ code: 'lt', value: 'lithuania', label: 'Lithuania', continent: 'Europe' },
|
||||
{ code: 'lu', value: 'luxembourg', label: 'Luxembourg', continent: 'Europe' },
|
||||
{ code: 'mg', value: 'madagascar', label: 'Madagascar', continent: 'Africa' },
|
||||
{ code: 'mw', value: 'malawi', label: 'Malawi', continent: 'Africa' },
|
||||
{ code: 'my', value: 'malaysia', label: 'Malaysia', continent: 'Asia' },
|
||||
{ code: 'mv', value: 'maldives', label: 'Maldives', continent: 'Asia' },
|
||||
{ code: 'ml', value: 'mali', label: 'Mali', continent: 'Africa' },
|
||||
{ code: 'mt', value: 'malta', label: 'Malta', continent: 'Europe' },
|
||||
{ code: 'mh', value: 'marshall-islands', label: 'Marshall Islands', continent: 'Oceania' },
|
||||
{ code: 'mr', value: 'mauritania', label: 'Mauritania', continent: 'Africa' },
|
||||
{ code: 'mu', value: 'mauritius', label: 'Mauritius', continent: 'Africa' },
|
||||
{ code: 'mx', value: 'mexico', label: 'Mexico', continent: 'North America' },
|
||||
{ code: 'fm', value: 'micronesia', label: 'Micronesia', continent: 'Oceania' },
|
||||
{ code: 'md', value: 'moldova', label: 'Moldova', continent: 'Europe' },
|
||||
{ code: 'mc', value: 'monaco', label: 'Monaco', continent: 'Europe' },
|
||||
{ code: 'mn', value: 'mongolia', label: 'Mongolia', continent: 'Asia' },
|
||||
{ code: 'me', value: 'montenegro', label: 'Montenegro', continent: 'Europe' },
|
||||
{ code: 'ma', value: 'morocco', label: 'Morocco', continent: 'Africa' },
|
||||
{ code: 'mz', value: 'mozambique', label: 'Mozambique', continent: 'Africa' },
|
||||
{ code: 'mm', value: 'myanmar', label: 'Myanmar', continent: 'Asia' },
|
||||
{ code: 'na', value: 'namibia', label: 'Namibia', continent: 'Africa' },
|
||||
{ code: 'nr', value: 'nauru', label: 'Nauru', continent: 'Oceania' },
|
||||
{ code: 'np', value: 'nepal', label: 'Nepal', continent: 'Asia' },
|
||||
{ code: 'nl', value: 'netherlands', label: 'Netherlands', continent: 'Europe' },
|
||||
{ code: 'nz', value: 'new-zealand', label: 'New Zealand', continent: 'Oceania' },
|
||||
{ code: 'ni', value: 'nicaragua', label: 'Nicaragua', continent: 'North America' },
|
||||
{ code: 'ne', value: 'niger', label: 'Niger', continent: 'Africa' },
|
||||
{ code: 'ng', value: 'nigeria', label: 'Nigeria', continent: 'Africa' },
|
||||
{ code: 'kp', value: 'north-korea', label: 'North Korea', continent: 'Asia' },
|
||||
{ code: 'mk', value: 'north-macedonia', label: 'North Macedonia', continent: 'Europe' },
|
||||
{ code: 'no', value: 'norway', label: 'Norway', continent: 'Europe' },
|
||||
{ code: 'om', value: 'oman', label: 'Oman', continent: 'Asia' },
|
||||
{ code: 'pk', value: 'pakistan', label: 'Pakistan', continent: 'Asia' },
|
||||
{ code: 'pw', value: 'palau', label: 'Palau', continent: 'Oceania' },
|
||||
{ code: 'ps', value: 'palestine', label: 'Palestine', continent: 'Asia' },
|
||||
{ code: 'pa', value: 'panama', label: 'Panama', continent: 'North America' },
|
||||
{ code: 'pg', value: 'papua-new-guinea', label: 'Papua New Guinea', continent: 'Oceania' },
|
||||
{ code: 'py', value: 'paraguay', label: 'Paraguay', continent: 'South America' },
|
||||
{ code: 'pe', value: 'peru', label: 'Peru', continent: 'South America' },
|
||||
{ code: 'ph', value: 'philippines', label: 'Philippines', continent: 'Asia' },
|
||||
{ code: 'pl', value: 'poland', label: 'Poland', continent: 'Europe' },
|
||||
{ code: 'pt', value: 'portugal', label: 'Portugal', continent: 'Europe' },
|
||||
{ code: 'qa', value: 'qatar', label: 'Qatar', continent: 'Asia' },
|
||||
{ code: 'ro', value: 'romania', label: 'Romania', continent: 'Europe' },
|
||||
{ code: 'ru', value: 'russia', label: 'Russia', continent: 'Europe' },
|
||||
{ code: 'rw', value: 'rwanda', label: 'Rwanda', continent: 'Africa' },
|
||||
{ code: 'ws', value: 'samoa', label: 'Samoa', continent: 'Oceania' },
|
||||
{ code: 'sm', value: 'san-marino', label: 'San Marino', continent: 'Europe' },
|
||||
{ code: 'sa', value: 'saudi-arabia', label: 'Saudi Arabia', continent: 'Asia' },
|
||||
{ code: 'sn', value: 'senegal', label: 'Senegal', continent: 'Africa' },
|
||||
{ code: 'rs', value: 'serbia', label: 'Serbia', continent: 'Europe' },
|
||||
{ code: 'sc', value: 'seychelles', label: 'Seychelles', continent: 'Africa' },
|
||||
{ code: 'sl', value: 'sierra-leone', label: 'Sierra Leone', continent: 'Africa' },
|
||||
{ code: 'sg', value: 'singapore', label: 'Singapore', continent: 'Asia' },
|
||||
{ code: 'sk', value: 'slovakia', label: 'Slovakia', continent: 'Europe' },
|
||||
{ code: 'si', value: 'slovenia', label: 'Slovenia', continent: 'Europe' },
|
||||
{ code: 'sb', value: 'solomon-islands', label: 'Solomon Islands', continent: 'Oceania' },
|
||||
{ code: 'so', value: 'somalia', label: 'Somalia', continent: 'Africa' },
|
||||
{ code: 'za', value: 'south-africa', label: 'South Africa', continent: 'Africa' },
|
||||
{ code: 'kr', value: 'south-korea', label: 'South Korea', continent: 'Asia' },
|
||||
{ code: 'ss', value: 'south-sudan', label: 'South Sudan', continent: 'Africa' },
|
||||
{ code: 'es', value: 'spain', label: 'Spain', continent: 'Europe' },
|
||||
{ code: 'lk', value: 'sri-lanka', label: 'Sri Lanka', continent: 'Asia' },
|
||||
{ code: 'sd', value: 'sudan', label: 'Sudan', continent: 'Africa' },
|
||||
{ code: 'sr', value: 'suriname', label: 'Suriname', continent: 'South America' },
|
||||
{ code: 'se', value: 'sweden', label: 'Sweden', continent: 'Europe' },
|
||||
{ code: 'ch', value: 'switzerland', label: 'Switzerland', continent: 'Europe' },
|
||||
{ code: 'sy', value: 'syria', label: 'Syria', continent: 'Asia' },
|
||||
{ code: 'tw', value: 'taiwan', label: 'Taiwan', continent: 'Asia' },
|
||||
{ code: 'tj', value: 'tajikistan', label: 'Tajikistan', continent: 'Asia' },
|
||||
{ code: 'tz', value: 'tanzania', label: 'Tanzania', continent: 'Africa' },
|
||||
{ code: 'th', value: 'thailand', label: 'Thailand', continent: 'Asia' },
|
||||
{ code: 'tl', value: 'timor-leste', label: 'Timor-Leste', continent: 'Asia' },
|
||||
{ code: 'tg', value: 'togo', label: 'Togo', continent: 'Africa' },
|
||||
{ code: 'to', value: 'tonga', label: 'Tonga', continent: 'Oceania' },
|
||||
{
|
||||
code: 'tt',
|
||||
value: 'trinidad-and-tobago',
|
||||
label: 'Trinidad and Tobago',
|
||||
continent: 'North America',
|
||||
},
|
||||
{ code: 'tn', value: 'tunisia', label: 'Tunisia', continent: 'Africa' },
|
||||
{ code: 'tr', value: 'turkey', label: 'Turkey', continent: 'Asia' },
|
||||
{ code: 'tm', value: 'turkmenistan', label: 'Turkmenistan', continent: 'Asia' },
|
||||
{ code: 'tv', value: 'tuvalu', label: 'Tuvalu', continent: 'Oceania' },
|
||||
{ code: 'ug', value: 'uganda', label: 'Uganda', continent: 'Africa' },
|
||||
{ code: 'ua', value: 'ukraine', label: 'Ukraine', continent: 'Europe' },
|
||||
{ code: 'ae', value: 'united-arab-emirates', label: 'United Arab Emirates', continent: 'Asia' },
|
||||
{ code: 'gb', value: 'united-kingdom', label: 'United Kingdom', continent: 'Europe' },
|
||||
{ code: 'us', value: 'united-states', label: 'United States', continent: 'North America' },
|
||||
{ code: 'uy', value: 'uruguay', label: 'Uruguay', continent: 'South America' },
|
||||
{ code: 'uz', value: 'uzbekistan', label: 'Uzbekistan', continent: 'Asia' },
|
||||
{ code: 'vu', value: 'vanuatu', label: 'Vanuatu', continent: 'Oceania' },
|
||||
{ code: 'va', value: 'vatican-city', label: 'Vatican City', continent: 'Europe' },
|
||||
{ code: 've', value: 'venezuela', label: 'Venezuela', continent: 'South America' },
|
||||
{ code: 'vn', value: 'vietnam', label: 'Vietnam', continent: 'Asia' },
|
||||
{ code: 'ye', value: 'yemen', label: 'Yemen', continent: 'Asia' },
|
||||
{ code: 'zm', value: 'zambia', label: 'Zambia', continent: 'Africa' },
|
||||
{ code: 'zw', value: 'zimbabwe', label: 'Zimbabwe', continent: 'Africa' },
|
||||
];
|
||||
@@ -0,0 +1,130 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Combobox from '@/components/ui/combobox';
|
||||
|
||||
export default function ExampleMultipleCombobox() {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<Combobox.Root items={langs} multiple>
|
||||
<div className="max-w-md flex flex-col gap-1">
|
||||
<label
|
||||
className="flex flex-col gap-1 text-sm leading-5 font-bold text-neutral-950 dark:text-white"
|
||||
htmlFor={id}
|
||||
>
|
||||
Programming languages
|
||||
</label>
|
||||
<Combobox.InputGroup className="flex min-h-8 w-64 cursor-text flex-wrap items-center gap-0.5 border border-neutral-950 bg-white dark:bg-neutral-950 px-2 py-1 focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-neutral-950 dark:focus-within:outline-white has-[button]:px-1 dark:border-white [@media(min-width:32rem)]:w-[22rem]">
|
||||
<Combobox.Chips className="flex w-full flex-wrap items-center gap-1">
|
||||
<Combobox.Value>
|
||||
{(value: ProgrammingLanguage[]) => (
|
||||
<React.Fragment>
|
||||
{value.map((language) => (
|
||||
<Combobox.Chip
|
||||
key={language.id}
|
||||
className="group flex min-h-[calc(1.5rem-2px)] cursor-default items-center gap-1 overflow-hidden bg-neutral-100 py-0 pr-[0.2rem] pl-[0.4rem] text-sm leading-none text-neutral-950 outline-none focus-within:bg-neutral-950 focus-within:text-white [@media(hover:hover)]:data-highlighted:bg-neutral-950 [@media(hover:hover)]:data-highlighted:text-white dark:bg-neutral-800 dark:text-white dark:focus-within:bg-white dark:focus-within:text-neutral-950 dark:[@media(hover:hover)]:data-highlighted:bg-white dark:[@media(hover:hover)]:data-highlighted:text-neutral-950"
|
||||
aria-label={language.value}
|
||||
>
|
||||
{language.value}
|
||||
<Combobox.ChipRemove
|
||||
className="flex size-4 items-center justify-center border-0 bg-transparent p-0 text-inherit hover:bg-neutral-200 group-focus-within:hover:bg-neutral-700 dark:hover:bg-neutral-700 dark:group-focus-within:hover:bg-neutral-200"
|
||||
aria-label={`Remove ${language.value}`}
|
||||
>
|
||||
<XIcon />
|
||||
</Combobox.ChipRemove>
|
||||
</Combobox.Chip>
|
||||
))}
|
||||
<Combobox.Input
|
||||
id={id}
|
||||
placeholder={value.length > 0 ? '' : 'e.g. TypeScript'}
|
||||
className="h-[calc(1.5rem-2px)] min-w-12 flex-1 border-0 bg-white p-0 text-sm any-pointer-coarse:text-base dark:bg-neutral-950 font-normal text-neutral-950 outline-none placeholder:text-neutral-500 dark:placeholder:text-neutral-400 dark:text-white"
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Combobox.Value>
|
||||
</Combobox.Chips>
|
||||
</Combobox.InputGroup>
|
||||
</div>
|
||||
|
||||
<Combobox.Portal>
|
||||
<Combobox.Positioner className="z-50" sideOffset={4}>
|
||||
<Combobox.Popup>
|
||||
<Combobox.Empty>
|
||||
<div className="py-2 pr-4 pl-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No languages found.
|
||||
</div>
|
||||
</Combobox.Empty>
|
||||
<Combobox.List>
|
||||
{(language: ProgrammingLanguage) => (
|
||||
<Combobox.Item
|
||||
key={language.id}
|
||||
className="grid cursor-default grid-cols-[1rem_1fr] items-center gap-2 p-2 text-sm leading-4 outline-none select-none data-selected:relative data-selected:z-0 data-selected:text-neutral-950 data-selected:before:absolute data-selected:before:inset-0 data-selected:before:z-[-1] [@media(hover:hover)]:data-highlighted:relative [@media(hover:hover)]:data-highlighted:z-0 [@media(hover:hover)]:data-highlighted:text-white [@media(hover:hover)]:data-highlighted:before:absolute [@media(hover:hover)]:data-highlighted:before:inset-0 [@media(hover:hover)]:data-highlighted:before:z-[-1] [@media(hover:hover)]:data-highlighted:before:bg-neutral-950 dark:data-selected:text-white dark:[@media(hover:hover)]:data-highlighted:text-neutral-950 dark:[@media(hover:hover)]:data-highlighted:before:bg-white"
|
||||
value={language}
|
||||
>
|
||||
<Combobox.ItemIndicator className="col-start-1">
|
||||
<CheckIcon />
|
||||
</Combobox.ItemIndicator>
|
||||
<span className="col-start-2">{language.value}</span>
|
||||
</Combobox.Item>
|
||||
)}
|
||||
</Combobox.List>
|
||||
</Combobox.Popup>
|
||||
</Combobox.Positioner>
|
||||
</Combobox.Portal>
|
||||
</Combobox.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m4.5 4.5 7 7m-7 0 7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProgrammingLanguage {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const langs: ProgrammingLanguage[] = [
|
||||
{ id: 'js', value: 'JavaScript' },
|
||||
{ id: 'ts', value: 'TypeScript' },
|
||||
{ id: 'py', value: 'Python' },
|
||||
{ id: 'java', value: 'Java' },
|
||||
{ id: 'cpp', value: 'C++' },
|
||||
{ id: 'cs', value: 'C#' },
|
||||
{ id: 'php', value: 'PHP' },
|
||||
{ id: 'ruby', value: 'Ruby' },
|
||||
{ id: 'go', value: 'Go' },
|
||||
{ id: 'rust', value: 'Rust' },
|
||||
{ id: 'swift', value: 'Swift' },
|
||||
];
|
||||
@@ -0,0 +1,176 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Combobox from '@/components/ui/combobox';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
|
||||
export default function ExampleVirtualizedCombobox() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const virtualizerRef = React.useRef<Virtualizer | null>(null);
|
||||
|
||||
return (
|
||||
<Combobox.Root
|
||||
virtualized
|
||||
items={virtualizedItems}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
itemToStringLabel={getItemLabel}
|
||||
onItemHighlighted={(item, { reason, index }) => {
|
||||
const virtualizer = virtualizerRef.current;
|
||||
|
||||
if (!item || !virtualizer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isStart = index === 0;
|
||||
const isEnd = index === virtualizer.options.count - 1;
|
||||
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));
|
||||
|
||||
if (shouldScroll) {
|
||||
queueMicrotask(() => {
|
||||
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label className="flex flex-col gap-1 text-sm leading-5 font-bold text-neutral-950 dark:text-white">
|
||||
Search 10,000 items
|
||||
<Combobox.Input className="h-8 w-64 border border-neutral-950 bg-white dark:bg-neutral-950 px-2 text-sm any-pointer-coarse:text-base font-normal text-neutral-950 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white dark:border-white dark:text-white" />
|
||||
</label>
|
||||
|
||||
<Combobox.Portal>
|
||||
<Combobox.Positioner sideOffset={4}>
|
||||
<Combobox.Popup>
|
||||
<Combobox.Empty>
|
||||
<div className="py-3 px-2 text-sm leading-4 text-neutral-500 dark:text-neutral-400">
|
||||
No items found.
|
||||
</div>
|
||||
</Combobox.Empty>
|
||||
<Combobox.List className="p-0">
|
||||
<VirtualizedList open={open} virtualizerRef={virtualizerRef} />
|
||||
</Combobox.List>
|
||||
</Combobox.Popup>
|
||||
</Combobox.Positioner>
|
||||
</Combobox.Portal>
|
||||
</Combobox.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function VirtualizedList({
|
||||
open,
|
||||
virtualizerRef,
|
||||
}: {
|
||||
open: boolean;
|
||||
virtualizerRef: React.RefObject<Virtualizer | null>;
|
||||
}) {
|
||||
const filteredItems = Combobox.useFilteredItems<VirtualizedItem>();
|
||||
|
||||
const scrollElementRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
enabled: open,
|
||||
count: filteredItems.length,
|
||||
getScrollElement: () => scrollElementRef.current,
|
||||
estimateSize: () => 32,
|
||||
overscan: 20,
|
||||
paddingStart: 4,
|
||||
paddingEnd: 4,
|
||||
scrollPaddingEnd: 4,
|
||||
scrollPaddingStart: 4,
|
||||
});
|
||||
|
||||
React.useImperativeHandle(virtualizerRef, () => virtualizer);
|
||||
|
||||
const handleScrollElementRef = React.useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
scrollElementRef.current = element;
|
||||
if (element) {
|
||||
virtualizer.measure();
|
||||
}
|
||||
},
|
||||
[virtualizer],
|
||||
);
|
||||
|
||||
const totalSize = virtualizer.getTotalSize();
|
||||
|
||||
if (!filteredItems.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="presentation"
|
||||
ref={handleScrollElementRef}
|
||||
className="h-[min(22.5rem,var(--total-size))] max-h-[var(--available-height)] overflow-auto overscroll-contain scroll-py-1"
|
||||
style={{ '--total-size': `${totalSize}px` } as React.CSSProperties}
|
||||
>
|
||||
<div role="presentation" className="relative w-full" style={{ height: totalSize }}>
|
||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||
const item = filteredItems[virtualItem.index];
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Combobox.Item
|
||||
key={virtualItem.key}
|
||||
index={virtualItem.index}
|
||||
data-index={virtualItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
value={item}
|
||||
className="grid cursor-default grid-cols-[1rem_1fr] items-center gap-2 p-2 text-sm leading-4 outline-none select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-white data-highlighted:before:absolute data-highlighted:before:inset-0 data-highlighted:before:z-[-1] data-highlighted:before:bg-neutral-950 dark:data-highlighted:text-neutral-950 dark:data-highlighted:before:bg-white"
|
||||
aria-setsize={filteredItems.length}
|
||||
aria-posinset={virtualItem.index + 1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: virtualItem.size,
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
>
|
||||
<Combobox.ItemIndicator>
|
||||
<CheckIcon />
|
||||
</Combobox.ItemIndicator>
|
||||
<span className="col-start-2">{item.name}</span>
|
||||
</Combobox.Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 8.5 4 4 7-9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface VirtualizedItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function getItemLabel(item: VirtualizedItem | null) {
|
||||
return item ? item.name : '';
|
||||
}
|
||||
|
||||
const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
|
||||
const id = String(index + 1);
|
||||
const indexLabel = id.padStart(4, '0');
|
||||
return { id, name: `Item ${indexLabel}` };
|
||||
});
|
||||
|
||||
type Virtualizer = ReturnType<typeof useVirtualizer<HTMLDivElement, Element>>;
|
||||
@@ -0,0 +1,26 @@
|
||||
// @ts-nocheck
|
||||
import ContextMenu from '@/components/ui/context-menu';
|
||||
|
||||
export default function ExampleMenu() {
|
||||
return (
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
Right click here
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Positioner>
|
||||
<ContextMenu.Popup>
|
||||
<ContextMenu.Item>Add to Library</ContextMenu.Item>
|
||||
<ContextMenu.Item>Add to Playlist</ContextMenu.Item>
|
||||
<ContextMenu.Separator className="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" />
|
||||
<ContextMenu.Item>Play Next</ContextMenu.Item>
|
||||
<ContextMenu.Item>Play Last</ContextMenu.Item>
|
||||
<ContextMenu.Separator className="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" />
|
||||
<ContextMenu.Item>Favorite</ContextMenu.Item>
|
||||
<ContextMenu.Item>Share</ContextMenu.Item>
|
||||
</ContextMenu.Popup>
|
||||
</ContextMenu.Positioner>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// @ts-nocheck
|
||||
import * as React from 'react';
|
||||
import ContextMenu from '@/components/ui/context-menu';
|
||||
|
||||
export default function ExampleContextMenu() {
|
||||
return (
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
Right click here
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Positioner>
|
||||
<ContextMenu.Popup>
|
||||
<ContextMenu.Item>Add to Library</ContextMenu.Item>
|
||||
|
||||
<ContextMenu.SubmenuRoot>
|
||||
<ContextMenu.SubmenuTrigger>
|
||||
Add to Playlist <CaretRightIcon />
|
||||
</ContextMenu.SubmenuTrigger>
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Positioner alignOffset={-4} sideOffset={-4}>
|
||||
<ContextMenu.Popup>
|
||||
<ContextMenu.Item>Get Up!</ContextMenu.Item>
|
||||
<ContextMenu.Item>Inside Out</ContextMenu.Item>
|
||||
<ContextMenu.Item>Night Beats</ContextMenu.Item>
|
||||
<ContextMenu.Separator className="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" />
|
||||
<ContextMenu.Item>New playlist...</ContextMenu.Item>
|
||||
</ContextMenu.Popup>
|
||||
</ContextMenu.Positioner>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.SubmenuRoot>
|
||||
|
||||
<ContextMenu.Separator className="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" />
|
||||
|
||||
<ContextMenu.Item>Play Next</ContextMenu.Item>
|
||||
<ContextMenu.Item>Play Last</ContextMenu.Item>
|
||||
<ContextMenu.Separator className="mx-1 my-1 h-px bg-neutral-950 dark:bg-white" />
|
||||
<ContextMenu.Item>Favorite</ContextMenu.Item>
|
||||
<ContextMenu.Item>Share</ContextMenu.Item>
|
||||
</ContextMenu.Popup>
|
||||
</ContextMenu.Positioner>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function CaretRightIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="M6 12V4l4.5 4z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import AlertDialog from '@/components/ui/alert-dialog';
|
||||
import Button from '@/components/ui/button';
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
|
||||
export default function ExampleDialog() {
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const [confirmationOpen, setConfirmationOpen] = React.useState(false);
|
||||
const [textareaValue, setTextareaValue] = React.useState('');
|
||||
const titleId = React.useId();
|
||||
|
||||
return (
|
||||
<Dialog.Root
|
||||
open={dialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
// Show the close confirmation if there’s text in the textarea
|
||||
if (!open && textareaValue) {
|
||||
setConfirmationOpen(true);
|
||||
} else {
|
||||
// Reset the text area value
|
||||
setTextareaValue('');
|
||||
// Open or close the dialog normally
|
||||
setDialogOpen(open);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog.Trigger>Tweet</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Popup className="fixed top-[calc(50%+1.25rem*var(--nested-dialogs))] left-1/2 -mt-8 flex w-96 max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-1 scale-[calc(1-0.1*var(--nested-dialogs))] bg-white dark:bg-neutral-950 p-4 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-[top,transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out after:absolute after:inset-0 after:bg-black/5 after:opacity-0 after:transition-opacity after:duration-100 after:ease-out after:pointer-events-none data-ending-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-ending-style:scale-[0.96] data-ending-style:opacity-0 data-nested-dialog-open:after:opacity-100 data-starting-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-starting-style:scale-[0.96] data-starting-style:opacity-0">
|
||||
<Dialog.Title id={titleId}>New tweet</Dialog.Title>
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
// Close the dialog when submitting
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
aria-labelledby={titleId}
|
||||
required
|
||||
className="min-h-32 w-full border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 p-2 text-sm any-pointer-coarse:text-base font-normal text-neutral-950 dark:text-white placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white"
|
||||
placeholder="What’s on your mind?"
|
||||
value={textareaValue}
|
||||
onChange={(event) => setTextareaValue(event.target.value)}
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Dialog.Close>Cancel</Dialog.Close>
|
||||
<Button type="submit">Tweet</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
|
||||
{/* Confirmation dialog */}
|
||||
<AlertDialog.Root open={confirmationOpen} onOpenChange={setConfirmationOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Popup className="fixed top-[calc(50%+1.25rem*var(--nested-dialogs))] left-1/2 -mt-8 flex w-96 max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 scale-[calc(1-0.1*var(--nested-dialogs))] bg-white dark:bg-neutral-950 p-4 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-[top,transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out after:absolute after:inset-0 after:bg-black/5 after:opacity-0 after:transition-opacity after:duration-100 after:ease-out after:pointer-events-none data-ending-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-ending-style:scale-[0.96] data-ending-style:opacity-0 data-nested-dialog-open:after:opacity-100 data-starting-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-starting-style:scale-[0.96] data-starting-style:opacity-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<AlertDialog.Title>Discard tweet?</AlertDialog.Title>
|
||||
<AlertDialog.Description>Your tweet will be lost.</AlertDialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<AlertDialog.Close>Go back</AlertDialog.Close>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setConfirmationOpen(false);
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDialog.Popup>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
|
||||
const demoDialog = Dialog.createHandle<number>();
|
||||
|
||||
export default function DialogDetachedTriggersControlledDemo() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [triggerId, setTriggerId] = React.useState<string | null>(null);
|
||||
|
||||
const handleOpenChange = (isOpen: boolean, eventDetails: Dialog.Root.ChangeEventDetails) => {
|
||||
setOpen(isOpen);
|
||||
setTriggerId(eventDetails.trigger?.id ?? null);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
<Dialog.Trigger
|
||||
|
||||
handle={demoDialog}
|
||||
id="trigger-1"
|
||||
payload={1}
|
||||
>
|
||||
Open 1
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Trigger
|
||||
|
||||
handle={demoDialog}
|
||||
id="trigger-2"
|
||||
payload={2}
|
||||
>
|
||||
Open 2
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Trigger
|
||||
|
||||
handle={demoDialog}
|
||||
id="trigger-3"
|
||||
payload={3}
|
||||
>
|
||||
Open 3
|
||||
</Dialog.Trigger>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 items-center justify-center gap-2 border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 px-3 text-sm leading-none whitespace-nowrap font-normal text-neutral-950 dark:text-white select-none hover:bg-neutral-100 dark:hover:bg-neutral-800 active:bg-neutral-200 dark:active:bg-neutral-700 disabled:border-neutral-500 disabled:text-neutral-500 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
|
||||
onClick={() => {
|
||||
setTriggerId('trigger-2');
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
Open programmatically
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Dialog.Root
|
||||
handle={demoDialog}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
triggerId={triggerId}
|
||||
>
|
||||
{({ payload }) => (
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Popup>
|
||||
{payload !== undefined && (
|
||||
<Dialog.Title>Dialog {payload}</Dialog.Title>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Dialog.Close>
|
||||
Close
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
)}
|
||||
</Dialog.Root>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
|
||||
const demoDialog = Dialog.createHandle();
|
||||
|
||||
export default function DialogDetachedTriggersSimpleDemo() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Dialog.Trigger
|
||||
|
||||
handle={demoDialog}
|
||||
>
|
||||
View notifications
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Root handle={demoDialog}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Popup>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Dialog.Title>Notifications</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
You are all caught up. Good job!
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Dialog.Close>
|
||||
Close
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// @ts-nocheck
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
|
||||
export default function ExampleDialog() {
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
View notifications
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Popup>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Dialog.Title>Notifications</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
You are all caught up. Good job!
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Dialog.Close>
|
||||
Close
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// @ts-nocheck
|
||||
import * as React from 'react';
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
import ScrollArea from '@/components/ui/scroll-area';
|
||||
|
||||
export default function InsideScrollDialog() {
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
Open dialog
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className="fixed inset-0 bg-black opacity-20 transition-opacity duration-150 data-starting-style:opacity-0 data-ending-style:opacity-0 dark:opacity-50 supports-[-webkit-touch-callout:none]:absolute" />
|
||||
<Dialog.Viewport className="fixed inset-0 flex items-center justify-center overflow-hidden py-6 [@media(min-height:600px)]:pb-12 [@media(min-height:600px)]:pt-8">
|
||||
<Dialog.Popup className="relative flex w-[min(40rem,calc(100vw-2rem))] max-h-full max-w-full min-h-0 flex-col bg-white dark:bg-neutral-950 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-[transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out data-starting-style:scale-[0.98] data-starting-style:opacity-0 data-ending-style:scale-[0.98] data-ending-style:opacity-0">
|
||||
<div className="flex flex-col gap-1 p-4 border-b border-neutral-950 dark:border-white">
|
||||
<Dialog.Title>Dialog</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
This layout keeps the popup fully on screen while allowing its content to scroll.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<ScrollArea.Root className="relative flex min-h-0 flex-auto overflow-hidden has-[>_:first-child:focus-visible]:outline-2 has-[>_:first-child:focus-visible]:outline-offset-0 has-[>_:first-child:focus-visible]:outline-neutral-950 dark:has-[>_:first-child:focus-visible]:outline-white">
|
||||
<ScrollArea.Viewport className="flex-auto min-h-0 overflow-y-auto overscroll-contain outline-none">
|
||||
<ScrollArea.Content className="flex flex-col">
|
||||
{CONTENT_SECTIONS.map((item) => (
|
||||
<section className="flex flex-col gap-1 p-4" key={item.title}>
|
||||
<h3 className="text-sm font-bold">{item.title}</h3>
|
||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">{item.body}</p>
|
||||
</section>
|
||||
))}
|
||||
</ScrollArea.Content>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar className="pointer-events-none flex w-4 justify-center bg-black/[.12] dark:bg-white/[.12] opacity-0 transition-opacity duration-150 data-hovering:pointer-events-auto data-hovering:opacity-100 data-scrolling:pointer-events-auto data-scrolling:opacity-100 data-scrolling:duration-0">
|
||||
<ScrollArea.Thumb className="w-full bg-neutral-950 dark:bg-white" />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
<div className="flex justify-end gap-3 p-4 border-t border-neutral-950 dark:border-white">
|
||||
<Dialog.Close>
|
||||
Close
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Viewport>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
const CONTENT_SECTIONS = [
|
||||
{
|
||||
title: 'What a dialog is for',
|
||||
body: 'Use a dialog when you need the user to complete a focused task or read something important without navigating away. It opens on top of the page and returns focus back where it started when closed.',
|
||||
},
|
||||
{
|
||||
title: 'Anatomy at a glance',
|
||||
body: 'Root, Trigger, Portal, Backdrop, Popup, Title/Description, Close. Keep the title short and the first paragraph specific so screen readers announce something meaningful.',
|
||||
},
|
||||
{
|
||||
title: 'Opening and closing',
|
||||
body: 'Control it using external state via the `open` and `onOpenChange` props, or let it manage state for you internally.',
|
||||
},
|
||||
{
|
||||
title: 'Keyboard and focus behavior',
|
||||
body: 'Focus moves inside the dialog when it opens. Tab and Shift+Tab loop within, and Esc requests close.',
|
||||
},
|
||||
{
|
||||
title: 'Accessible labeling',
|
||||
body: 'Set an explicit title and description using the `Dialog.Title` and `Dialog.Description` components.',
|
||||
},
|
||||
{
|
||||
title: 'Backdrop and page scrolling',
|
||||
body: 'The backdrop visually separates layers while background content is inert. Don’t rely on dimness alone—keep copy clear and buttons obvious so actions are easy to choose.',
|
||||
},
|
||||
{
|
||||
title: 'Portals and stacking',
|
||||
body: 'Dialogs render in a portal so they sit above the `isolation: isolate` app content and avoid local z-index wars.',
|
||||
},
|
||||
{
|
||||
title: 'Viewport overflow',
|
||||
body: 'Let long content overflow the bottom edge and reveal as you scroll the page container. Keep generous padding at the top and bottom so the dialog doesn’t feel jammed against the edges.',
|
||||
},
|
||||
{
|
||||
title: 'Nested dialogs and confirmations',
|
||||
body: 'If closing a dialog needs confirmation, open a child alert dialog rather than mutating the current one. The parent stays visible behind it; only the topmost layer should feel interactive.',
|
||||
},
|
||||
{
|
||||
title: 'Transitions that respect motion settings',
|
||||
body: 'Use small, fast transitions (opacity plus a few pixels of Y translation or scale). Subtle motion helps people notice what changed without slowing them down.',
|
||||
},
|
||||
{
|
||||
title: 'Controlled vs. uncontrolled',
|
||||
body: 'Controlled state is best when other parts of the page need to react to open/close. Uncontrolled is fine for local cases where only the dialog matters.',
|
||||
},
|
||||
{
|
||||
title: 'Close affordances',
|
||||
body: 'Always offer a visible close button in the corner. Don’t rely only on Esc or the backdrop for pointer outside presses. Touch screen readers and accessibility users benefit from a clear, targetable control to click to close the dialog.',
|
||||
},
|
||||
{
|
||||
title: 'Forms inside dialogs',
|
||||
body: 'Keep forms short; longer flows usually deserve a full page. Validate inline, keep button text specific (“Create project”), and disable destructive actions until the input is valid.',
|
||||
},
|
||||
{
|
||||
title: 'Content guidelines',
|
||||
body: 'Lead with the outcome (“Rename project?”) and follow with one or two short, concrete sentences. Avoid long prose; link out for details instead.',
|
||||
},
|
||||
{
|
||||
title: 'SSR and hydration notes',
|
||||
body: 'Because dialogs render in a portal, make sure your portal container exists on the client.',
|
||||
},
|
||||
{
|
||||
title: 'Mobile ergonomics',
|
||||
body: 'Use larger touch targets and keep the close button reachable with the thumb. Avoid full-screen modals unless the task truly needs a whole screen.',
|
||||
},
|
||||
{
|
||||
title: 'Theming and density',
|
||||
body: 'Match spacing and corner radius to your system. Use a slightly denser layout than pages so the dialog feels purpose-built, not like a mini web page.',
|
||||
},
|
||||
{
|
||||
title: 'Internationalization',
|
||||
body: 'Plan for longer text. Buttons can grow to two lines; titles should wrap gracefully. Keep destructive terms consistent across locales.',
|
||||
},
|
||||
{
|
||||
title: 'Performance',
|
||||
body: 'Children are mounted lazily when the dialog opens. If the dialog can reopen often, consider the `keepMounted` prop sparingly to perform the work only once on mount to avoid re-initializing complex React trees on each open.',
|
||||
},
|
||||
{
|
||||
title: 'When a popover is better',
|
||||
body: 'If the content is a small hint or a few quick actions anchored to a control, use a popover or menu instead of a dialog. Dialogs interrupt on purpose—use that sparingly.',
|
||||
},
|
||||
{
|
||||
title: 'Follow-up and cleanup',
|
||||
body: 'After a successful action, close the dialog and show confirmation in context (toast, inline message, or updated UI) so people can see the result of what they just did.',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,47 @@
|
||||
// @ts-nocheck
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
|
||||
export default function ExampleDialog() {
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
View notifications
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Popup className="fixed top-[calc(50%+1.25rem*var(--nested-dialogs))] left-1/2 -mt-8 flex w-96 max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 scale-[calc(1-0.1*var(--nested-dialogs))] bg-white dark:bg-neutral-950 p-4 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-[top,transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out after:absolute after:inset-0 after:bg-black/5 after:opacity-0 after:transition-opacity after:duration-100 after:ease-out after:pointer-events-none data-ending-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-ending-style:scale-[0.96] data-ending-style:opacity-0 data-nested-dialog-open:after:opacity-100 data-starting-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-starting-style:scale-[0.96] data-starting-style:opacity-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Dialog.Title>Notifications</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
You are all caught up. Good job!
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
Customize
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Popup className="fixed top-[calc(50%+1.25rem*var(--nested-dialogs))] left-1/2 -mt-8 flex w-96 max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 scale-[calc(1-0.1*var(--nested-dialogs))] bg-white dark:bg-neutral-950 p-4 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-[top,transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out after:absolute after:inset-0 after:bg-black/5 after:opacity-0 after:transition-opacity after:duration-100 after:ease-out after:pointer-events-none data-ending-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-ending-style:scale-[0.96] data-ending-style:opacity-0 data-nested-dialog-open:after:opacity-100 data-starting-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-starting-style:scale-[0.96] data-starting-style:opacity-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Dialog.Title>
|
||||
Customize notifications
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Review your settings here.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Dialog.Close>
|
||||
Close
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</div>
|
||||
</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
import ScrollArea from '@/components/ui/scroll-area';
|
||||
|
||||
export default function OutsideScrollDialog() {
|
||||
const popupRef = React.useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
Open dialog
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className="fixed inset-0 bg-black/20 dark:bg-black/50 transition-opacity duration-[600ms] ease-[var(--ease-out-fast)] data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-[350ms] data-ending-style:ease-[cubic-bezier(0.375,0.015,0.545,0.455)] supports-[-webkit-touch-callout:none]:absolute" />
|
||||
<Dialog.Viewport className="group/dialog fixed inset-0">
|
||||
<ScrollArea.Root
|
||||
style={{ position: undefined }}
|
||||
className="h-full overscroll-contain group-data-ending-style/dialog:pointer-events-none"
|
||||
>
|
||||
<ScrollArea.Viewport className="h-full overscroll-contain group-data-ending-style/dialog:pointer-events-none">
|
||||
<ScrollArea.Content className="flex min-h-full items-center justify-center">
|
||||
<Dialog.Popup
|
||||
ref={popupRef}
|
||||
initialFocus={popupRef}
|
||||
className="outline-0 relative mx-auto my-16 flex w-[min(40rem,calc(100vw-2rem))] flex-col gap-4 bg-white dark:bg-neutral-950 p-4 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-transform duration-[700ms] ease-[cubic-bezier(0.45,1.005,0,1.005)] data-starting-style:translate-y-[100dvh] data-ending-style:translate-y-[max(100dvh,100%)] data-ending-style:duration-[350ms] data-ending-style:ease-[cubic-bezier(0.375,0.015,0.545,0.455)] motion-reduce:transition-none"
|
||||
>
|
||||
<div className="relative flex flex-col gap-1 pr-8">
|
||||
<Dialog.Title>Dialog</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
This layout keeps an outer container scrollable while the dialog can extend
|
||||
past the bottom edge.
|
||||
</Dialog.Description>
|
||||
<Dialog.Close
|
||||
aria-label="Close"
|
||||
className="absolute -top-1 -right-1 inline-flex items-center justify-center w-8 h-8 border-none bg-transparent p-0 text-neutral-950 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 active:bg-neutral-200 dark:active:bg-neutral-700 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
|
||||
>
|
||||
<XIcon />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{CONTENT_SECTIONS.map((item) => (
|
||||
<section className="flex flex-col gap-1" key={item.title}>
|
||||
<h3 className="text-sm font-bold">{item.title}</h3>
|
||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{item.body}
|
||||
</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Related docs:{' '}
|
||||
{RELATED_LINKS.map((item, index) => (
|
||||
<React.Fragment key={item.href}>
|
||||
{index > 0 ? ', ' : null}
|
||||
<a
|
||||
className="text-neutral-950 dark:text-white underline underline-offset-[0.16em] decoration-[1px] hover:no-underline focus-visible:outline-2 focus-visible:outline-neutral-950 dark:focus-visible:outline-white focus-visible:outline-offset-2"
|
||||
href={item.href}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</React.Fragment>
|
||||
))}
|
||||
.
|
||||
</p>
|
||||
</Dialog.Popup>
|
||||
</ScrollArea.Content>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar className="pointer-events-none flex w-4 justify-center bg-black/[.12] dark:bg-white/[.12] opacity-0 transition-opacity duration-[250ms] data-scrolling:pointer-events-auto data-scrolling:opacity-100 data-scrolling:duration-[75ms] data-scrolling:delay-[0ms] hover:pointer-events-auto hover:opacity-100 hover:duration-[75ms] hover:delay-[0ms] group-data-ending-style/dialog:opacity-0 group-data-ending-style/dialog:duration-[250ms]">
|
||||
<ScrollArea.Thumb className="w-full bg-neutral-950 dark:bg-white" />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
</Dialog.Viewport>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 2.5 11 11m-11 0 11-11" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const CONTENT_SECTIONS = [
|
||||
{
|
||||
title: 'What a dialog is for',
|
||||
body: 'Use a dialog when you need the user to complete a focused task or read something important without navigating away. It opens on top of the page and returns focus back where it started when closed.',
|
||||
},
|
||||
{
|
||||
title: 'Anatomy at a glance',
|
||||
body: 'Root, Trigger, Portal, Backdrop, Viewport, Popup, Title, Description, Close. Keep the title short and the first paragraph specific so screen readers announce something meaningful.',
|
||||
},
|
||||
{
|
||||
title: 'Opening and closing',
|
||||
body: 'Control it using external state via the `open` and `onOpenChange` props, or let it manage state for you internally.',
|
||||
},
|
||||
{
|
||||
title: 'Keyboard and focus behavior',
|
||||
body: 'Focus moves inside the dialog when it opens. Tab and Shift+Tab loop within, and Esc requests close.',
|
||||
},
|
||||
{
|
||||
title: 'Accessible labeling',
|
||||
body: 'Set an explicit title and description using the `Dialog.Title` and `Dialog.Description` components.',
|
||||
},
|
||||
{
|
||||
title: 'Backdrop and page scrolling',
|
||||
body: 'The backdrop visually separates layers while background content is inert. Don’t rely on dimness alone—keep copy clear and buttons obvious so actions are easy to choose.',
|
||||
},
|
||||
{
|
||||
title: 'Portals and stacking',
|
||||
body: 'Dialogs render in a portal so they sit above the `isolation: isolate` app content and avoid local z-index wars.',
|
||||
},
|
||||
{
|
||||
title: 'Viewport overflow',
|
||||
body: 'Let long content overflow the bottom edge and reveal as you scroll the page container. Keep generous padding at the top and bottom so the dialog doesn’t feel jammed against the edges.',
|
||||
},
|
||||
{
|
||||
title: 'Nested dialogs and confirmations',
|
||||
body: 'If closing a dialog needs confirmation, open a child alert dialog rather than mutating the current one. The parent stays visible behind it; only the topmost layer should feel interactive.',
|
||||
},
|
||||
{
|
||||
title: 'Transitions that respect motion settings',
|
||||
body: 'Use small, fast transitions (opacity plus a few pixels of Y translation or scale). Subtle motion helps people notice what changed without slowing them down.',
|
||||
},
|
||||
{
|
||||
title: 'Controlled vs. uncontrolled',
|
||||
body: 'Controlled state is best when other parts of the page need to react to open/close. Uncontrolled is fine for local cases where only the dialog matters.',
|
||||
},
|
||||
{
|
||||
title: 'Close affordances',
|
||||
body: 'Always offer a visible close button in the corner. Don’t rely only on Esc or the backdrop for pointer outside presses. Touch screen readers and accessibility users benefit from a clear, targetable control to click to close the dialog.',
|
||||
},
|
||||
{
|
||||
title: 'Forms inside dialogs',
|
||||
body: 'Keep forms short; longer flows usually deserve a full page. Validate inline, keep button text specific (“Create project”), and disable destructive actions until the input is valid.',
|
||||
},
|
||||
{
|
||||
title: 'Content guidelines',
|
||||
body: 'Lead with the outcome (“Rename project?”) and follow with one or two short, concrete sentences. Avoid long prose; link out for details instead.',
|
||||
},
|
||||
{
|
||||
title: 'SSR and hydration notes',
|
||||
body: 'Because dialogs render in a portal, make sure your portal container exists on the client.',
|
||||
},
|
||||
{
|
||||
title: 'Mobile ergonomics',
|
||||
body: 'Use larger touch targets and keep the close button reachable with the thumb. Avoid full-screen modals unless the task truly needs a whole screen.',
|
||||
},
|
||||
{
|
||||
title: 'Theming and density',
|
||||
body: 'Match spacing and corner radius to your system. Use a slightly denser layout than pages so the dialog feels purpose-built, not like a mini web page.',
|
||||
},
|
||||
{
|
||||
title: 'Internationalization',
|
||||
body: 'Plan for longer text. Buttons can grow to two lines; titles should wrap gracefully. Keep destructive terms consistent across locales.',
|
||||
},
|
||||
{
|
||||
title: 'Performance',
|
||||
body: 'Children are mounted lazily when the dialog opens. If the dialog can reopen often, consider the `keepMounted` prop sparingly to perform the work only once on mount to avoid re-initializing complex React trees on each open.',
|
||||
},
|
||||
{
|
||||
title: 'When a popover is better',
|
||||
body: 'If the content is a small hint or a few quick actions anchored to a control, use a popover or menu instead of a dialog. Dialogs interrupt on purpose—use that sparingly.',
|
||||
},
|
||||
{
|
||||
title: 'Follow-up and cleanup',
|
||||
body: 'After a successful action, close the dialog and show confirmation in context (toast, inline message, or updated UI) so people can see the result of what they just did.',
|
||||
},
|
||||
];
|
||||
|
||||
const RELATED_LINKS = [
|
||||
{ href: '/react/components/scroll-area', label: 'Scroll Area' },
|
||||
{ href: '/react/components/drawer', label: 'Drawer' },
|
||||
{ href: '/react/components/popover', label: 'Popover' },
|
||||
] as const;
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import Dialog from '@/components/ui/dialog';
|
||||
|
||||
export default function ExampleUncontainedDialog() {
|
||||
return (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
Open dialog
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className="fixed inset-0 min-h-dvh bg-black/20 dark:bg-black/50 transition-opacity duration-150 data-starting-style:opacity-0 data-ending-style:opacity-0 supports-[-webkit-touch-callout:none]:absolute" />
|
||||
<Dialog.Viewport className="fixed inset-0 grid place-items-center px-4 py-12 xl:py-6">
|
||||
<Dialog.Popup className="group/popup relative flex h-full w-full max-w-[70rem] xl:max-w-none justify-center pointer-events-none transition-opacity duration-150 data-starting-style:opacity-0 data-ending-style:opacity-0">
|
||||
<Dialog.Close
|
||||
className="absolute right-0 -top-10 flex px-0 size-8 items-center justify-center border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 text-neutral-950 dark:text-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none hover:bg-neutral-100 dark:hover:bg-neutral-800 active:bg-neutral-200 dark:active:bg-neutral-700 xl:top-0 pointer-events-auto focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
|
||||
aria-label="Close"
|
||||
>
|
||||
<XIcon />
|
||||
</Dialog.Close>
|
||||
<div className="pointer-events-auto h-full w-full max-w-[70rem] bg-white dark:bg-neutral-950 p-4 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-[scale] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-starting-style/popup:scale-105" />
|
||||
</Dialog.Popup>
|
||||
</Dialog.Viewport>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon(props: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
style={{ display: 'block', ...props.style }}
|
||||
>
|
||||
<path d="m2.5 2.5 11 11m-11 0 11-11" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import Drawer from '@/components/ui/drawer';
|
||||
|
||||
const ACTIONS = ['Unfollow', 'Mute', 'Add to Favourites', 'Add to Close Friends', 'Restrict'];
|
||||
|
||||
export default function ExampleDrawerUncontained() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Drawer.Root open={open} onOpenChange={setOpen}>
|
||||
<Drawer.Trigger>
|
||||
Open action sheet
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Backdrop className="[--backdrop-opacity:0.4] fixed inset-0 min-h-dvh bg-black opacity-[calc(var(--backdrop-opacity)*(1-var(--drawer-swipe-progress)))] transition-opacity duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-swiping:duration-0 data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] supports-[-webkit-touch-callout:none]:absolute dark:[--backdrop-opacity:0.7]" />
|
||||
<Drawer.Viewport className="fixed inset-0 flex items-end justify-center">
|
||||
<Drawer.Popup className="pointer-events-none flex w-full max-w-[20rem] flex-col gap-3 px-4 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] outline-none focus-visible:outline-none [transform:translateY(var(--drawer-swipe-movement-y))] transition-transform duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-swiping:select-none data-starting-style:[transform:translateY(calc(100%+1rem+2px))] data-ending-style:[transform:translateY(calc(100%+1rem+2px))] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)]">
|
||||
<Drawer.Content className="pointer-events-auto overflow-hidden border border-neutral-950 bg-white text-neutral-950 shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none">
|
||||
<Drawer.Title className="sr-only">Profile actions</Drawer.Title>
|
||||
<Drawer.Description className="sr-only">
|
||||
Choose an action for this user.
|
||||
</Drawer.Description>
|
||||
|
||||
<ul
|
||||
className="m-0 list-none divide-y divide-neutral-950 p-0 dark:divide-white"
|
||||
aria-label="Profile actions"
|
||||
>
|
||||
{ACTIONS.map((action, index) => (
|
||||
<li key={action}>
|
||||
{index === 0 && (
|
||||
<Drawer.Close className="sr-only">Close action sheet</Drawer.Close>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 w-full border-0 bg-transparent px-5 text-center text-sm text-neutral-950 select-none hover:bg-neutral-100 active:bg-neutral-200 focus-visible:bg-neutral-100 focus-visible:outline-none dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700 dark:focus-visible:bg-neutral-800"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{action}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Drawer.Content>
|
||||
<div className="pointer-events-auto overflow-hidden border border-neutral-950 bg-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:border-white dark:bg-neutral-950 dark:shadow-none">
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 w-full border-0 bg-transparent px-5 text-center text-sm text-red-700 select-none dark:text-red-400 hover:bg-neutral-100 active:bg-neutral-200 focus-visible:bg-neutral-100 focus-visible:outline-none dark:hover:bg-neutral-800 dark:active:bg-neutral-700 dark:focus-visible:bg-neutral-800"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Block User
|
||||
</button>
|
||||
</div>
|
||||
</Drawer.Popup>
|
||||
</Drawer.Viewport>
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// @ts-nocheck
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import AlertDialog from '@/components/ui/alert-dialog';
|
||||
import Button from '@/components/ui/button';
|
||||
import Drawer from '@/components/ui/drawer';
|
||||
|
||||
export default function ExampleDrawer() {
|
||||
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
||||
const [confirmationOpen, setConfirmationOpen] = React.useState(false);
|
||||
const [textareaValue, setTextareaValue] = React.useState('');
|
||||
const titleId = React.useId();
|
||||
|
||||
return (
|
||||
<Drawer.Root
|
||||
swipeDirection="right"
|
||||
open={drawerOpen}
|
||||
onOpenChange={(open, eventDetails) => {
|
||||
// Show the close confirmation if there’s text in the textarea
|
||||
if (!open && textareaValue) {
|
||||
eventDetails.cancel();
|
||||
setConfirmationOpen(true);
|
||||
return;
|
||||
}
|
||||
if (!open) {
|
||||
// Reset the textarea value
|
||||
setTextareaValue('');
|
||||
}
|
||||
setDrawerOpen(open);
|
||||
}}
|
||||
>
|
||||
<Drawer.Trigger>Tweet</Drawer.Trigger>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Backdrop className="[--bleed:0px]" />
|
||||
<Drawer.Viewport>
|
||||
<Drawer.Popup
|
||||
className={`relative [--bleed:3rem] supports-[-webkit-touch-callout:none]:[--bleed:0px] h-full w-[calc(20rem+3rem)] max-w-[calc(100vw-3rem+3rem)] -mr-[3rem] border-l border-neutral-950 bg-white p-6 pr-[calc(1.5rem+3rem)] text-neutral-950 outline-none shadow-[0.25rem_0.25rem_0] shadow-black/12 overflow-y-auto overscroll-contain touch-auto [transform:translateX(var(--drawer-swipe-movement-x))] transition-transform duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-swiping:select-none data-ending-style:[transform:translateX(calc(100%-var(--bleed)+var(--viewport-padding)+2px))] data-starting-style:[transform:translateX(calc(100%-var(--bleed)+var(--viewport-padding)+2px))] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] supports-[-webkit-touch-callout:none]:mr-0 supports-[-webkit-touch-callout:none]:w-[20rem] supports-[-webkit-touch-callout:none]:max-w-[calc(100vw-3rem)] supports-[-webkit-touch-callout:none]:border supports-[-webkit-touch-callout:none]:pr-6 dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none after:absolute after:inset-0 after:bg-black/5 after:opacity-0 after:transition-opacity after:duration-100 after:ease-out after:pointer-events-none ${
|
||||
confirmationOpen ? 'after:opacity-100' : ''
|
||||
}`}
|
||||
>
|
||||
<Drawer.Content className="mx-auto flex flex-col gap-4 w-full max-w-[32rem]">
|
||||
<Drawer.Title id={titleId} className="mb-0">New tweet</Drawer.Title>
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
// Close the drawer when submitting
|
||||
setTextareaValue('');
|
||||
setDrawerOpen(false);
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
aria-labelledby={titleId}
|
||||
required
|
||||
className="min-h-32 w-full border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 p-2 text-sm any-pointer-coarse:text-base font-normal text-neutral-950 dark:text-white placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus:outline-2 focus:-outline-offset-1 focus:outline-neutral-950 dark:focus:outline-white"
|
||||
placeholder="What’s on your mind?"
|
||||
value={textareaValue}
|
||||
onChange={(event) => setTextareaValue(event.target.value)}
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Drawer.Close>Cancel</Drawer.Close>
|
||||
<Button type="submit">Tweet</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Drawer.Content>
|
||||
</Drawer.Popup>
|
||||
</Drawer.Viewport>
|
||||
</Drawer.Portal>
|
||||
|
||||
{/* Confirmation dialog */}
|
||||
<AlertDialog.Root open={confirmationOpen} onOpenChange={setConfirmationOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Popup className="fixed top-1/2 left-1/2 -mt-8 flex w-96 max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 bg-white dark:bg-neutral-950 p-4 text-neutral-950 dark:text-white border border-neutral-950 dark:border-white shadow-[0.25rem_0.25rem_0] shadow-black/12 dark:shadow-none transition-[transform,opacity] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-150 data-ending-style:ease-out data-ending-style:translate-y-[calc(-50%+0.25rem)] data-ending-style:scale-[0.96] data-ending-style:opacity-0 data-starting-style:translate-y-[calc(-50%+0.25rem)] data-starting-style:scale-[0.96] data-starting-style:opacity-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<AlertDialog.Title>Discard tweet?</AlertDialog.Title>
|
||||
<AlertDialog.Description>Your tweet will be lost.</AlertDialog.Description>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<AlertDialog.Close>Go back</AlertDialog.Close>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setConfirmationOpen(false);
|
||||
setTextareaValue('');
|
||||
setDrawerOpen(false);
|
||||
}}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDialog.Popup>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
</Drawer.Root>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user