Compare commits

..

110 Commits

Author SHA1 Message Date
shadcn
3a5fa409eb fix 2025-10-15 11:10:00 +04:00
shadcn
6567897393 fix: actions 2025-10-15 11:07:14 +04:00
shadcn
2675fa3941 ci: add deprecated action (#8465)
* ci: add deprecated action

* ci: add label

* Potential fix for code scanning alert no. 12: Workflow does not contain permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-10-15 11:04:30 +04:00
Shodai Suzuki
fbda67c88c docs: remove isolated remix page from installation guides (#7027) 2025-10-15 10:39:15 +04:00
shadcn
e8674ee848 feat(shadcn): allow path to override targets (#8452) 2025-10-15 10:37:58 +04:00
kuzma031
adb66f4d43 fix(components): use type-only import for VariantProps in sidebar component to support verbatimModuleSyntax (#7437)
* fix sidebar variantprops imports

* chore: registry build

---------

Co-authored-by: Djordje Kuzmanovic <djordje@voiced.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-15 10:36:32 +04:00
shadcn
3afb46eaf6 feat: add llms.txt (#8460) 2025-10-14 23:30:07 +04:00
shadcn
7cd019ad36 feat(shadcn): add support for color vars (#8459)
* feat(shadcn): add support for color vars

* chore: add changeset
2025-10-14 23:07:27 +04:00
Serhat Arslan
bea7d30536 docs: fix next/link import syntax in component examples (#8427)
Fix incorrect named import syntax for Next.js Link component.

Changed from:
  import { Link } from "next/link"

To correct default import:
  import Link from "next/link"

Next.js Link is a default export, not a named export. The incorrect
syntax causes TypeScript error:
'Module "next/link" has no exported member "Link"'

Affected files:
- Button component docs
- Badge component docs
- Breadcrumb component docs (v4 and www)
- Navigation Menu component docs

Fixes issue where users copying code snippets get immediate errors.

Co-authored-by: serhatx1 <armonikadijital@gmail.com>
2025-10-14 22:38:58 +04:00
Ziad Beyens
40c3ff513a fix(registry): handle universal registry items with no files (#8420)
* fix(registry): handle universal registry items with no files

Allow registry items with registryDependencies but no files to be
considered universal registry items. Previously the function required
files.length to be truthy, which excluded valid items with only
registryDependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-14 17:14:05 +04:00
Gildas Garcia
89ebfdce47 feat(registry): Add Shadcn Admin Kit to the registry index (#8442) 2025-10-14 17:12:12 +04:00
Haz
b83023034a fix(cli): fix add registry item with at-property css rule (#8451)
* Add fixture and test

* Handle at-property as regular CSS rules in updateCssPlugin

* Add changeset entry

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-14 17:11:29 +04:00
Ujjwal Sharma
6a534d7954 fix(components): use type import for ToasterProps (#8376)
* fix(components): use type import for ToasterProps

* chore: build registry

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-14 16:40:17 +04:00
Felix
ef1987ded9 docs(Toast): add deprecation Callout (#6982)
* docs(Toast): add deprecation Callout

* docs(Toast): add tw4 and react19 specifics
2025-10-13 22:21:50 +04:00
Jakob Guddas
77bf7d28b4 feat(components): changed sonner defaults to use lucide icons (#7620)
* feat(components): changed sonner defaults to use lucide icons

* Update new-york-v4 sonner.tsx

* fix: icons and docs

* fix

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-13 22:16:50 +04:00
Ayush Shrestha
41f4f7357d refactor: initialize FormFieldContext and FormItemContext with null values rather empty object with type assertion (#4847) 2025-10-13 21:32:43 +04:00
Carlos Pegueros
bc99818e04 docs(www): fix sonner not showing on first render (#6235) 2025-10-13 21:17:32 +04:00
Shahar Ilany
162ba7b13c Small fixes for installation document (#6623)
* Fix TanStack Start icon color

* Fix a11y for Laravel and TanStack Start
2025-10-13 21:14:11 +04:00
Ryann Mack
f12db1e3a2 fix(sonner): support border radius from theme (#7825)
* fix(sonner): support border radius from theme

* chore: rebuild registry

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-13 21:12:00 +04:00
shadcn
ce3e2b1df8 Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-13 12:39:19 +04:00
shadcn
dcfe911b33 docs: button cursor 2025-10-13 12:38:58 +04:00
Jrocam
7210a4919a fix(components): checkbox alignment with grid 🔳 (#4772)
* fix(components): checkbox alignment with grid 🔳

* fix(checkbox): internal checkbox check alignment (default style)

* fix: new-york-v4

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-13 12:29:44 +04:00
Krishna Agarwal
d198908510 fix(field): return null when errors are empty (#8419)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-13 12:22:34 +04:00
shadcn
b0b1cd1f0d feat: add dropdown menu dialog example (#8438)
* feat: add dropdown menu dialog example

* chore: remove dropdown-dialog
2025-10-13 11:57:20 +04:00
shadcn
f3d70724b6 chore: remove new-components-01 (#8439) 2025-10-13 11:55:08 +04:00
Dada Khalandar
407e9c6802 fix: correct link to Form documentation in form.mdx (#8416) 2025-10-11 10:15:07 +04:00
shadcn
c67e630521 feat: add docs and examples for react-hook-form and tanstack form (#8412)
* feat: add next forms docs

* feat

* docs: rhf and tsf

* docs: forms

* feat: update react-hook-form docs

* feat: update docs for both lib

* docs: update tanstack docs

* docs: update

* fix

* fix

* fix

* add forms link in sidebar
2025-10-10 21:29:30 +04:00
Daniel Petho
f494411953 feat(registry): add fancy components to registries (#8397) 2025-10-10 16:09:13 +04:00
Mudunuri bhaskara karthikeya varma
a43c1d1342 feat: add @eldoraui registry URL to registries.json (#8379)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-09 11:06:55 +04:00
Hin
607a6fd127 feat: add shadcn map to the registry (#8375) 2025-10-08 20:15:45 +04:00
Irsyad A. Panjaitan
fbcc665b49 Add @intentui registry URL to registries.json (#8380) 2025-10-08 00:07:59 +04:00
Shivam S.
7ddcf31e43 fix(docs): correct Empty component structure in documentation (#8374) 2025-10-07 20:58:15 +04:00
Louis J.
3e39163b08 feat: add @elevenlabs-ui registry URL to registries.json (#8364) 2025-10-07 13:12:24 +04:00
shadcn
e311fdae04 fix(www): dashboard 2025-10-07 10:09:34 +04:00
shadcn
26640d9d88 fix: code 2025-10-06 16:57:33 +04:00
Seoku
3e20c228da fix(input-group-textarea): prevent child elements from overflowing by… (#8341)
* fix(input-group-textarea): prevent child elements from overflowing by adding min-w-0

* chore: run registry:build

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-06 16:11:01 +04:00
Morgan Feeney
0810c0e1a2 Add @zippystarter registry URL to registries.json (#8354) 2025-10-06 16:00:28 +04:00
Ahdeetai
1205ea5445 Add @scrollxui in trusted registries (#8322)
* Add @scrollxui in trusted registries

* fix: missing comma

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-06 16:00:12 +04:00
Rob Austin
4430ab8bab Add @shadcnblocks registry URL to registries.json (#8344)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-05 20:03:40 +04:00
github-actions[bot]
d6716db9cc chore(release): version packages (#8349)
* chore(release): version packages

* chore: lock

* chore(release): version packages

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-05 14:23:43 +04:00
JunHyeok Ha
da8fa6aacd fix(cli): Update package.json name property when init next-monorepo (#7742)
* fix(cli): Update package.json name property when init next-monorepo

* test(cli): Fix failing test

* fix(cli): Remove unnecessary git changes

* chore: add changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-05 14:18:31 +04:00
shadcn
e96f9edf02 feat(shadcn): add mcp support for codex (#8348) 2025-10-05 14:05:14 +04:00
Ohh
b19e9cadb2 feat: add @skyrui in trusted registries (#8326) 2025-10-05 13:01:08 +04:00
Nicholas
3bb47bf914 feat: add better-upload to trusted registries (#8345) 2025-10-05 13:00:56 +04:00
Ajay Patel
a72fac6fde Add @shadcn-studio registry URL to registries.json (#8332)
Added https://shadcnstudio.com/ opensource components (https://shadcnstudio.com/components), blocks (https://shadcnstudio.com/blocks), and themes (https://shadcnstudio.com/theme-generator) registry
2025-10-04 11:44:59 +04:00
Ritesh Bucha
4b3186c46b feat: add @bucharitesh in trusted registries (#8330) 2025-10-04 11:44:32 +04:00
Bassim Shahidy
e67e955f2a Add assistant-ui registry URL to registries.json (#8312) 2025-10-04 11:44:05 +04:00
lucas kouzoukian
bf047b9824 correct alignment. (#8337) 2025-10-04 11:34:25 +04:00
shadcn
04432835f9 feat: new components (#8334)
* feat: add field.tsx and update blocks

* feat: add input group

* feat: implement button group

* fix

* fix

* wip

* fix: button group

* feat: update field

* fix

* feat

* feat: cooked

* fix

* chore: build registry

* feat: add kbd component

* chore: update input group demo

* feat: update kbd component

* feat: add empty

* feat: add spinner

* refactor: input group

* feat: blocks

* fix

* fix: app sidebar

* feat: add label to app sidebar

* fix

* fix

* fix

* fix

* fix

* feat

* feat

* fix

* docs: button group

* feat: add docs

* docs: kbd

* docs: empty

* fix

* docs

* docs

* feat: add sink link

* fix

* fix

* docs

* feat: add new page

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add registration form

* fix: chat settings

* fix

* fix preview

* fix examples

* feat: add changelog

* fix

* fix

* fix

* fix

* fix

* feat(www): add t3 versions

* chore: build registry

* fix

* fix

* fix

* feat: inline code examples for llm

* fix

* feat: home

* fix

* fix

* fix

* fix

* fix

* chore: changelog

* fix

* fix

* fix

* fix: callout

* fix
2025-10-03 21:05:22 +04:00
Pablo Hdez
77e6f28e81 feat: add @svgl in trusted registries (#8297) 2025-09-29 16:02:53 +04:00
shadcn
f1e51ec8a1 feat(tooltip): update colors (#8271) 2025-09-22 10:56:50 +04:00
Brandon McConnell
3c525b8305 fix: correct improper JSON syntax (#8256) 2025-09-20 08:40:42 +04:00
shadcn
e7e844ff63 feat(button): remove shadow from buttons except outline (#8252) 2025-09-18 21:04:02 +04:00
shadcn
e14c55ac65 feat(input): remove flex class (#8251)
* feat: remove flex class

* chore: rebuild registry
2025-09-18 21:03:11 +04:00
shadcn
043be944ab fix: build script (#8250)
* fix: registries.json

* fix: chart

* fix: build script
2025-09-18 20:48:40 +04:00
Sahaj Jain
4eb257bc14 Add @tweakcn to trusted registries & Add shadcraft to Figma docs (#8245)
* docs: add shadcraft figma kit

* docs: add tweakcn to trusted registries
2025-09-18 17:39:02 +04:00
github-actions[bot]
1289192d4f chore(release): version packages (#8231)
* chore(release): version packages

* chore: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-09-17 08:17:57 +04:00
shadcn
75dde2e646 fix(shadcn): deps in cts projects (#8229)
* fix(shadcn): deps in cts projects

* fix: deps

* chore: add changelog
2025-09-16 17:54:44 +04:00
github-actions[bot]
b9f3ce1988 chore(release): version packages (#8217)
* chore(release): version packages

* chore(release): version packages

* ci: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 16:27:20 +04:00
Elliot Sutton
cdf58be7e1 feat(shadcn): fix transformCssVars function (#8186)
* feat(shadcn): fix transformCssVars function

* test(shadcn): update snapshots

* chore: add changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 16:08:56 +04:00
Fuma Nama
fae1a81add fix(shadcn): fix async imports not being transformed (#8036)
* fix(shadcn): fix async imports not being transformed when installing components

* fix(shadcn): improve performance

* test(shadcn): add tests for transform import

* test: update timeout

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 14:55:18 +04:00
shadcn
fc6d909ba2 add getRegistriesIndex (#8216)
* feat: add getRegistriesIndex

* chore: changeset

* fix: formatting
2025-09-15 14:55:05 +04:00
shadcn
590b9be610 fix: toc 2025-09-15 10:52:27 +04:00
Dillion Verma
41eb9d5c46 fix: update magicui registry name (#8214)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 10:48:43 +04:00
shadcn
b7c28199be docs: add import and plugin examples (#8215) 2025-09-15 10:46:58 +04:00
Maximilian Kaske
7869defd42 feat(charts): support legend and tooltip type none (#8082)
* feat(charts): support legend and tooltip type none

* fix: format

* chore: run registry:build

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 09:14:47 +04:00
Itzik Sokolov
6daa5215cc Add 97cn to registrries.json (#8207) 2025-09-15 08:34:42 +04:00
WebDevSimplified
722fb81b95 Add WDS registry URL to registries.json (#8193)
I am attempting to add the [Web Dev Simplified Shadcn Registry](https://wds-shadcn-registry.netlify.app) as an indexed registry.
2025-09-11 15:56:28 +04:00
Elliot Hesp
543be31722 docs: fix bad link to registry index (#8184) 2025-09-09 20:19:58 +04:00
Talha Mujahid
09b90cd5c2 Add @shadcn-editor registry URL to registries.json (#8177)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-09 17:09:09 +04:00
Nitish
c95959a9b3 added rigidui to trusted registries (#8180)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-09 17:06:31 +04:00
Sean
08820ce5ee feat: add @reui in trusted registries (#8181) 2025-09-09 17:04:45 +04:00
Arif Hossain
cb96e58992 feat: add @retroui in trusted registries (#8167)
* feat: add @retroui in trusted registries

* merge conflict resolve
2025-09-09 17:02:28 +04:00
Ali Hussein
fce5926265 Add formcn.dev to trusted registries (#8163)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-08 16:04:47 +04:00
Rohan Gupta
f7c0f81258 feat: added limeplay registry (#8174)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-08 15:40:36 +04:00
Gxuri
960b22b301 feat: add @skiper-ui to trusted registries (#8170)
* feat: add @skiper-ui to trusted registries

* Fix URL for @skiper-ui registry

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-09-08 15:37:32 +04:00
Elliot Sutton
6f057c9cc3 feat(v4): add @animate-ui to trusted registries (#8162) 2025-09-08 15:31:51 +04:00
shadcn
615a32d97a Merge branch 'main' of github.com:shadcn-ui/ui 2025-09-04 20:42:21 +04:00
shadcn
bfe6e1946c docs: update changelog 2025-09-04 20:41:50 +04:00
Railly Hugo
baaa82e4e7 feat: add @elements to trusted registries (#8155)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 20:29:52 +04:00
Alex Carpenter
caeed7bd65 feat: Add @alexcarpenter registry to known registries (#8154) 2025-09-04 20:26:49 +04:00
Alex Carpenter
61254f0c3f feat: add clerk to known registries (#8153)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 20:24:38 +04:00
Edu Calvo
3dcd797f2c feat: add @smoothui to trusted registries (#8152)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 20:24:24 +04:00
Théo Ribbi
b76f5cdbf7 feat: add @nativeui to trusted registries (#8146)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 20:20:15 +04:00
Ephraim Duncan
fcb1e2ca50 Add blocks registry URL to registries.json (#8145) 2025-09-04 20:18:48 +04:00
Sahil Tiwaskar
df94537e0f add billingsdk registry (#8148) 2025-09-04 16:01:35 +04:00
github-actions[bot]
275e3a2d59 chore(release): version packages (#8151)
* chore(release): version packages

* chore: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 15:58:06 +04:00
shadcn
e5402f9a20 feat(shadcn): implement recursive registry namespaces (#8147)
* feat(shadcn): implement recursive registry namespaces

* fix
2025-09-04 15:40:18 +04:00
OrcDev
04668da018 feat: add @8bitcn to trusted registries (#8144) 2025-09-04 12:11:35 +04:00
github-actions[bot]
0805751703 chore(release): version packages (#8143)
* chore(release): version packages

* chore(release): version packages

* deps: update

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 11:43:40 +04:00
Denish Navadiya
9ecb19cf2e feat: add @paceui-ui in trusted registries (#8140)
* feat: add @paceui-ui in trusted registries

* feat: add @paceui-ui in trusted registries

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 11:30:42 +04:00
shadcn
9c5eb0d20f feat(shadcn): add support for registries index (#8128)
* feat(shadcn): add support for registries index

* fix

* fix

* chore: changeset

* feat(shadcn): update handling of add commands

* feat: add support for known registries

* docs: update index docs
2025-09-04 11:27:45 +04:00
Akash Moradiya
2752ce11d8 feat: add @basecn in trusted registries (#8142)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 11:20:32 +04:00
Chánh Đại
d972caa853 feat: add @ncdai in trusted registries (#8141) 2025-09-04 10:30:58 +04:00
preet
00b2f0796e feat: add @hesaui in trusted registries (#8136)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-03 20:27:25 +04:00
shadcn
3ed9af5757 ci: update registries workflow (#8135)
* ci: update registries workflow

* chore: remove invalid
2025-09-03 20:04:28 +04:00
Kokonut
a4237e38f7 feat: add kokonutui to open source registry (#8133) 2025-09-03 19:09:42 +04:00
David
1178d40352 feat: add @react-bits to registries.json (#8132) 2025-09-03 16:34:37 +04:00
shadcn
cc612359ee feat: add known open source registries (#8130)
* feat: add known open source registries

* feat: remove invalid registries
2025-09-03 12:14:23 +04:00
shadcn
4d0272a659 chore: add symlink 2025-09-03 11:50:08 +04:00
shadcn
a15534bdb7 feat: add ai-elements registry (#8129)
* feat: add ai-elements registry

* Fix URL for @ai-elements in registries.json
2025-09-03 10:47:08 +04:00
shadcn
62c41c3271 feat: add registries index (#8126)
* feat: add registries index

* ci: update workflow

* ci: update

* fix

* debug

* ci: debug

* debug

* fix: build

* refactor
2025-09-03 08:24:02 +04:00
github-actions[bot]
851c0fa0d1 chore(release): version packages (#8111)
* chore(release): version packages

* chore: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-09-01 21:30:39 +04:00
shadcn
e84c819977 feat(shadcn): update handling of import and apply at rules (#8109)
* fix: plugin imports

* fix(shadcn): import in css

* feat(shadcn): allow empty body for apply rules

* chore: changeset

* fix: type issue
2025-09-01 20:02:26 +04:00
shadcn
64f8baf9aa feat(shadcn): allow empty files items (#8110)
* feat(shadcn): allow no files items

* feat(v4): add themes

* chore: changeset

* fix
2025-09-01 20:00:02 +04:00
Ahmed Zougari
4b44c6489a fix: update tailwindcss intellisense settings (#8095) 2025-08-31 14:25:19 +04:00
shadcn
f9021e9388 fix 2025-08-28 21:18:18 +04:00
shadcn
b1e3d4b740 feat: mcp 2025-08-28 20:58:03 +04:00
Nicolas Vargas
084fb927a1 Docs: Fix link to namespaced registries documentation (#8091)
Corrected the link to the namespaced registries documentation.
2025-08-27 21:46:19 +04:00
shadcn
7304ef2105 docs: add registry docs (#8080)
* docs(www): namespaced registries

* fix

* docs(www): add cli command to docs

* fix

* docs(www): update registry docs

* feat(shadcn): add mcp init command

* docs: restructure mcp docs

* chore: add changesets

* fix: formatting

* fix(shadcn): dependencies

* debug

* fix

* docs: add more troubleshooting docs

* docs: update registry docs

* feat(shadcn): add audit checklist tool

* chore: add mcp flag

* fix: format

* docs: replace beta with latest

* docs: add changelog

* fix
2025-08-27 19:25:21 +04:00
711 changed files with 54131 additions and 3603 deletions

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
Fix support for universal registry items that only have dependencies without files

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
add support for color as var

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
fix adding registry item with CSS at-property

75
.github/workflows/deprecated.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: Deprecated
on:
pull_request:
types: [opened, synchronize]
permissions:
issues: write
contents: read
pull-requests: write
jobs:
deprecated:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files: |
apps/www/**
files_ignore: |
apps/www/public/r/**
base_sha: ${{ github.event.pull_request.base.sha }}
sha: ${{ github.event.pull_request.head.sha }}
- name: Comment on PR if www files changed
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
const wwwFiles = changedFiles.filter(file =>
file.startsWith('apps/www/') &&
!file.startsWith('apps/www/public/r/')
);
if (wwwFiles.length > 0) {
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
Consider applying the change to \`apps/v4\` if relevant.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
// Add deprecated label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['deprecated']
});
} else {
// Remove deprecated label if no www files are changed
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'deprecated'
});
} catch (error) {
// Label doesn't exist, which is fine
console.log('Deprecated label not found, skipping removal');
}
}

View File

@@ -0,0 +1,54 @@
name: Validate Registries
on:
pull_request:
paths:
- "apps/v4/public/r/registries.json"
push:
branches:
- main
paths:
- "apps/v4/public/r/registries.json"
jobs:
validate:
runs-on: ubuntu-latest
name: pnpm validate:registries
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
version: 9.0.6
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm build --filter=shadcn
- name: Validate registries
run: pnpm --filter=v4 validate:registries

12
.vscode/settings.json vendored
View File

@@ -3,15 +3,13 @@
{ "pattern": "apps/*/" },
{ "pattern": "packages/*/" }
],
"tailwindCSS.experimental.classRegex": [
["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]?([^\"'`]+)[\"'`]?"],
["cn\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
// "cva\\(([^)]*)\\)",
// "[\"'`]([^\"'`]*).*?[\"'`]"
],
"tailwindCSS.classFunctions": ["cva", "cn"],
"vitest.debugExclude": [
"<node_internals>/**",
"**/node_modules/**",
"**/fixtures/**"
]
],
"files.exclude": {
"apps/www": true
}
}

View File

@@ -0,0 +1,168 @@
"use client"
import { IconMinus, IconPlus } from "@tabler/icons-react"
import { CheckIcon } from "lucide-react"
import { useThemeConfig } from "@/components/active-theme"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import { Switch } from "@/registry/new-york-v4/ui/switch"
const accents = [
{
name: "Blue",
value: "blue",
},
{
name: "Amber",
value: "amber",
},
{
name: "Green",
value: "green",
},
{
name: "Rose",
value: "rose",
},
]
export function AppearanceSettings() {
const { activeTheme, setActiveTheme } = useThemeConfig()
return (
<FieldSet>
<FieldGroup>
<FieldSet>
<FieldLegend>Compute Environment</FieldLegend>
<FieldDescription>
Select the compute environment for your cluster.
</FieldDescription>
<RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="kubernetes-r2h">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>
Run GPU workloads on a K8s configured cluster. This is the
default.
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="kubernetes"
id="kubernetes-r2h"
aria-label="Kubernetes"
/>
</Field>
</FieldLabel>
<FieldLabel htmlFor="vm-z4k">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Virtual Machine</FieldTitle>
<FieldDescription>
Access a VM configured cluster to run workloads. (Coming
soon)
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="vm"
id="vm-z4k"
aria-label="Virtual Machine"
/>
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Accent</FieldTitle>
<FieldDescription>Select the accent color.</FieldDescription>
</FieldContent>
<FieldSet aria-label="Accent">
<RadioGroup
className="flex flex-wrap gap-2"
value={activeTheme}
onValueChange={setActiveTheme}
>
{accents.map((accent) => (
<Label
htmlFor={accent.value}
key={accent.value}
data-theme={accent.value}
className="flex size-6 items-center justify-center rounded-full data-[theme=amber]:bg-amber-600 data-[theme=blue]:bg-blue-700 data-[theme=green]:bg-green-600 data-[theme=rose]:bg-rose-600"
>
<RadioGroupItem
id={accent.value}
value={accent.value}
aria-label={accent.name}
className="peer sr-only"
/>
<CheckIcon className="hidden size-4 stroke-white peer-data-[state=checked]:block" />
</Label>
))}
</RadioGroup>
</FieldSet>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="number-of-gpus-f6l">Number of GPUs</FieldLabel>
<FieldDescription>You can add more later.</FieldDescription>
</FieldContent>
<ButtonGroup>
<Input
id="number-of-gpus-f6l"
placeholder="8"
size={3}
className="h-8 !w-14 font-mono"
maxLength={3}
/>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label="Decrement"
>
<IconMinus />
</Button>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label="Increment"
>
<IconPlus />
</Button>
</ButtonGroup>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
<FieldDescription>
Allow the wallpaper to be tinted.
</FieldDescription>
</FieldContent>
<Switch id="tinting" defaultChecked />
</Field>
</FieldGroup>
</FieldSet>
)
}

View File

@@ -0,0 +1,120 @@
"use client"
import * as React from "react"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterPlusIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
export function ButtonGroupDemo() {
const [label, setLabel] = React.useState("personal")
return (
<ButtonGroup>
<ButtonGroup className="hidden sm:flex">
<Button variant="outline" size="icon-sm" aria-label="Go Back">
<ArrowLeftIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
Archive
</Button>
<Button variant="outline" size="sm">
Report
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
Snooze
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon-sm" aria-label="More Options">
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 [--radius:1rem]">
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />
Mark as Read
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
Archive
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<ClockIcon />
Snooze
</DropdownMenuItem>
<DropdownMenuItem>
<CalendarPlusIcon />
Add to Calendar
</DropdownMenuItem>
<DropdownMenuItem>
<ListFilterPlusIcon />
Add to List
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<TagIcon />
Label As...
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={label}
onValueChange={setLabel}
>
<DropdownMenuRadioItem value="personal">
Personal
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="work">
Work
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="other">
Other
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
Trash
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function ButtonGroupInputGroup() {
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
return (
<ButtonGroup className="[--radius:9999rem]">
<ButtonGroup>
<Button variant="outline" size="icon" aria-label="Add">
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup className="flex-1">
<InputGroup>
<InputGroupInput
placeholder={
voiceEnabled ? "Record and send audio..." : "Send a message..."
}
disabled={voiceEnabled}
/>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
onClick={() => setVoiceEnabled(!voiceEnabled)}
data-active={voiceEnabled}
className="data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
aria-pressed={voiceEnabled}
size="icon-xs"
aria-label="Voice Mode"
>
<AudioLinesIcon />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>Voice Mode</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,32 @@
"use client"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
export function ButtonGroupNested() {
return (
<ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
1
</Button>
<Button variant="outline" size="sm">
2
</Button>
<Button variant="outline" size="sm">
3
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon-sm" aria-label="Previous">
<ArrowLeftIcon />
</Button>
<Button variant="outline" size="icon-sm" aria-label="Next">
<ArrowRightIcon />
</Button>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,45 @@
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
export function ButtonGroupPopover() {
return (
<ButtonGroup>
<Button variant="outline" size="sm">
<BotIcon /> Copilot
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="icon-sm" aria-label="Open Popover">
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
<div className="px-4 py-3">
<div className="text-sm font-medium">Agent Tasks</div>
</div>
<Separator />
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
<Textarea
placeholder="Describe your task in natural language."
className="mb-4 resize-none"
/>
<p className="font-medium">Start a new task with Copilot</p>
<p className="text-muted-foreground">
Describe your task in natural language. Copilot will work in the
background and open a pull request for your review.
</p>
</div>
</PopoverContent>
</Popover>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,57 @@
import { PlusIcon } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
export function EmptyAvatarGroup() {
return (
<Empty className="flex-none border">
<EmptyHeader>
<EmptyMedia>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
</EmptyMedia>
<EmptyTitle>No Team Members</EmptyTitle>
<EmptyDescription>
Invite your team to collaborate on this project.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">
<PlusIcon />
Invite Members
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -0,0 +1,43 @@
import { SearchIcon } from "lucide-react"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
export function EmptyInputGroup() {
return (
<Empty>
<EmptyHeader>
<EmptyTitle>404 - Not Found</EmptyTitle>
<EmptyDescription>
The page you&apos;re looking for doesn&apos;t exist. Try searching for
what you need below.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<InputGroup className="w-3/4">
<InputGroupInput placeholder="Try searching for pages..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>/</Kbd>
</InputGroupAddon>
</InputGroup>
<EmptyDescription>
Need help? <a href="#">Contact support</a>
</EmptyDescription>
</EmptyContent>
</Empty>
)
}

View File

@@ -0,0 +1,15 @@
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
export function FieldCheckbox() {
return (
<FieldLabel htmlFor="checkbox-demo">
<Field orientation="horizontal">
<Checkbox id="checkbox-demo" defaultChecked />
<FieldLabel htmlFor="checkbox-demo" className="line-clamp-1">
I agree to the terms and conditions
</FieldLabel>
</Field>
</FieldLabel>
)
}

View File

@@ -0,0 +1,62 @@
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
export function FieldChoiceCard() {
return (
<div className="w-full max-w-md">
<FieldGroup>
<FieldSet>
<FieldLabel htmlFor="compute-environment-p8w">
Compute Environment
</FieldLabel>
<FieldDescription>
Select the compute environment for your cluster.
</FieldDescription>
<RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="kubernetes-r2h">
<Field orientation="horizontal">
<RadioGroupItem
value="kubernetes"
id="kubernetes-r2h"
aria-label="Kubernetes"
/>
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>
Run GPU workloads on a K8s configured cluster.
</FieldDescription>
</FieldContent>
</Field>
</FieldLabel>
<FieldLabel htmlFor="vm-z4k">
<Field orientation="horizontal">
<RadioGroupItem
value="vm"
id="vm-z4k"
aria-label="Virtual Machine"
/>
<FieldContent>
<FieldTitle>Virtual Machine</FieldTitle>
<FieldDescription>
Access a VM configured cluster to run workloads.
</FieldDescription>
</FieldContent>
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
</FieldGroup>
</div>
)
}

View File

@@ -0,0 +1,153 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
export function FieldDemo() {
return (
<div className="w-full max-w-md rounded-lg border p-6">
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>Payment Method</FieldLegend>
<FieldDescription>
All transactions are secure and encrypted
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="checkout-7j9-card-name-43j">
Name on Card
</FieldLabel>
<Input
id="checkout-7j9-card-name-43j"
placeholder="John Doe"
required
/>
</Field>
<div className="grid grid-cols-3 gap-4">
<Field className="col-span-2">
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">
Card Number
</FieldLabel>
<Input
id="checkout-7j9-card-number-uw1"
placeholder="1234 5678 9012 3456"
required
/>
<FieldDescription>
Enter your 16-digit number.
</FieldDescription>
</Field>
<Field className="col-span-1">
<FieldLabel htmlFor="checkout-7j9-cvv">CVV</FieldLabel>
<Input id="checkout-7j9-cvv" placeholder="123" required />
</Field>
</div>
<div className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="checkout-7j9-exp-month-ts6">
Month
</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-7j9-exp-month-ts6">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
<SelectItem value="01">01</SelectItem>
<SelectItem value="02">02</SelectItem>
<SelectItem value="03">03</SelectItem>
<SelectItem value="04">04</SelectItem>
<SelectItem value="05">05</SelectItem>
<SelectItem value="06">06</SelectItem>
<SelectItem value="07">07</SelectItem>
<SelectItem value="08">08</SelectItem>
<SelectItem value="09">09</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">
Year
</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-7j9-exp-year-f59">
<SelectValue placeholder="YYYY" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
<SelectItem value="2027">2027</SelectItem>
<SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
</SelectContent>
</Select>
</Field>
</div>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Billing Address</FieldLegend>
<FieldDescription>
The billing address associated with your payment method
</FieldDescription>
<FieldGroup>
<Field orientation="horizontal">
<Checkbox
id="checkout-7j9-same-as-shipping-wgm"
defaultChecked
/>
<FieldLabel
htmlFor="checkout-7j9-same-as-shipping-wgm"
className="font-normal"
>
Same as shipping address
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="checkout-7j9-optional-comments">
Comments
</FieldLabel>
<Textarea
id="checkout-7j9-optional-comments"
placeholder="Add any additional comments"
/>
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button type="submit">Submit</Button>
<Button variant="outline" type="button">
Cancel
</Button>
</Field>
</FieldGroup>
</form>
</div>
)
}

View File

@@ -0,0 +1,72 @@
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
const options = [
{
label: "Social Media",
value: "social-media",
},
{
label: "Search Engine",
value: "search-engine",
},
{
label: "Referral",
value: "referral",
},
{
label: "Other",
value: "other",
},
]
export function FieldHear() {
return (
<Card className="py-4 shadow-none">
<CardContent className="px-4">
<form>
<FieldGroup>
<FieldSet className="gap-4">
<FieldLegend>How did you hear about us?</FieldLegend>
<FieldDescription className="line-clamp-1">
Select the option that best describes how you heard about us.
</FieldDescription>
<FieldGroup className="flex flex-row flex-wrap gap-2 [--radius:9999rem]">
{options.map((option) => (
<FieldLabel
htmlFor={option.value}
key={option.value}
className="!w-fit"
>
<Field
orientation="horizontal"
className="gap-1.5 overflow-hidden !px-3 !py-1.5 transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:!px-2"
>
<Checkbox
value={option.value}
id={option.value}
defaultChecked={option.value === "social-media"}
className="-ml-6 -translate-x-1 rounded-full transition-all duration-100 ease-linear data-[state=checked]:ml-0 data-[state=checked]:translate-x-0"
/>
<FieldTitle>{option.label}</FieldTitle>
</Field>
</FieldLabel>
))}
</FieldGroup>
</FieldSet>
</FieldGroup>
</form>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,35 @@
"use client"
import { useState } from "react"
import {
Field,
FieldDescription,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Slider } from "@/registry/new-york-v4/ui/slider"
export function FieldSlider() {
const [value, setValue] = useState([200, 800])
return (
<div className="w-full max-w-md">
<Field>
<FieldTitle>Price Range</FieldTitle>
<FieldDescription>
Set your budget range ($
<span className="font-medium tabular-nums">{value[0]}</span> -{" "}
<span className="font-medium tabular-nums">{value[1]}</span>).
</FieldDescription>
<Slider
value={value}
onValueChange={setValue}
max={1000}
min={0}
step={10}
className="mt-2 w-full"
aria-label="Price Range"
/>
</Field>
</div>
)
}

View File

@@ -0,0 +1,52 @@
import { FieldSeparator } from "@/registry/new-york-v4/ui/field"
import { AppearanceSettings } from "./appearance-settings"
import { ButtonGroupDemo } from "./button-group-demo"
import { ButtonGroupInputGroup } from "./button-group-input-group"
import { ButtonGroupNested } from "./button-group-nested"
import { ButtonGroupPopover } from "./button-group-popover"
import { EmptyAvatarGroup } from "./empty-avatar-group"
import { FieldCheckbox } from "./field-checkbox"
import { FieldDemo } from "./field-demo"
import { FieldHear } from "./field-hear"
import { FieldSlider } from "./field-slider"
import { InputGroupButtonExample } from "./input-group-button"
import { InputGroupDemo } from "./input-group-demo"
import { ItemDemo } from "./item-demo"
import { NotionPromptForm } from "./notion-prompt-form"
import { SpinnerBadge } from "./spinner-badge"
import { SpinnerEmpty } from "./spinner-empty"
export function RootComponents() {
return (
<div className="theme-container mx-auto grid gap-8 py-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<FieldDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<EmptyAvatarGroup />
<SpinnerBadge />
<ButtonGroupInputGroup />
<FieldSlider />
<InputGroupDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<InputGroupButtonExample />
<ItemDemo />
<FieldSeparator>Appearance Settings</FieldSeparator>
<AppearanceSettings />
</div>
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">
<NotionPromptForm />
<ButtonGroupDemo />
<FieldCheckbox />
<div className="flex justify-between gap-4">
<ButtonGroupNested />
<ButtonGroupPopover />
</div>
<FieldHear />
<SpinnerEmpty />
</div>
</div>
)
}

View File

@@ -0,0 +1,68 @@
"use client"
import * as React from "react"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
export function InputGroupButtonExample() {
const [isFavorite, setIsFavorite] = React.useState(false)
return (
<div className="grid w-full max-w-sm gap-6">
<Label htmlFor="input-secure-19" className="sr-only">
Input Secure
</Label>
<InputGroup className="[--radius:9999px]">
<InputGroupInput id="input-secure-19" className="!pl-0.5" />
<Popover>
<PopoverTrigger asChild>
<InputGroupAddon>
<InputGroupButton
variant="secondary"
size="icon-xs"
aria-label="Info"
>
<IconInfoCircle />
</InputGroupButton>
</InputGroupAddon>
</PopoverTrigger>
<PopoverContent
align="start"
alignOffset={10}
className="flex flex-col gap-1 rounded-xl text-sm"
>
<p className="font-medium">Your connection is not secure.</p>
<p>You should not enter any sensitive information on this site.</p>
</PopoverContent>
</Popover>
<InputGroupAddon className="text-muted-foreground !pl-1">
https://
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton
onClick={() => setIsFavorite(!isFavorite)}
size="icon-xs"
aria-label="Favorite"
>
<IconStar
data-favorite={isFavorite}
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
/>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -0,0 +1,102 @@
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function InputGroupDemo() {
return (
<div className="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="example.com" className="!pl-1" />
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
className="rounded-full"
size="icon-xs"
aria-label="Info"
>
<IconInfoCircle />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>This is content in a tooltip.</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder="Ask, Search or Chat..." />
<InputGroupAddon align="block-end">
<InputGroupButton
variant="outline"
className="rounded-full"
size="icon-xs"
aria-label="Add"
>
<IconPlus />
</InputGroupButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<InputGroupButton variant="ghost">Auto</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:0.95rem]"
>
<DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText className="ml-auto">52% used</InputGroupText>
<Separator orientation="vertical" className="!h-4" />
<InputGroupButton
variant="default"
className="rounded-full"
size="icon-xs"
>
<ArrowUpIcon />
<span className="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="@shadcn" />
<InputGroupAddon align="inline-end">
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
<IconCheck className="size-3 text-white" />
</div>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -0,0 +1,46 @@
import {
IconBrandJavascript,
IconCopy,
IconCornerDownLeft,
IconRefresh,
} from "@tabler/icons-react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
export function InputGroupTextareaExample() {
return (
<div className="grid w-full max-w-md gap-4">
<InputGroup>
<InputGroupTextarea
id="textarea-code-32"
placeholder="console.log('Hello, world!');"
className="min-h-[180px]"
/>
<InputGroupAddon align="block-end" className="border-t">
<InputGroupText>Line 1, Column 1</InputGroupText>
<InputGroupButton size="sm" className="ml-auto" variant="default">
Run <IconCornerDownLeft />
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon align="block-start" className="border-b">
<InputGroupText className="font-mono font-medium">
<IconBrandJavascript />
script.js
</InputGroupText>
<InputGroupButton className="ml-auto">
<IconRefresh />
</InputGroupButton>
<InputGroupButton variant="ghost">
<IconCopy />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -0,0 +1,78 @@
import { Plus } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
export function ItemAvatar() {
return (
<div className="flex w-full max-w-lg flex-col gap-6">
<Item variant="outline" className="hidden">
<ItemMedia>
<Avatar className="size-10">
<AvatarImage src="https://github.com/maxleiter.png" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>Max Leiter</ItemTitle>
<ItemDescription>Last seen 5 months ago</ItemDescription>
</ItemContent>
<ItemActions>
<Button
size="icon-sm"
variant="outline"
className="rounded-full"
aria-label="Invite"
>
<Plus />
</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemMedia>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<Avatar className="hidden sm:flex">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className="hidden sm:flex">
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
</ItemMedia>
<ItemContent>
<ItemTitle>No Team Members</ItemTitle>
<ItemDescription>Invite your team to collaborate.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">
Invite
</Button>
</ItemActions>
</Item>
</div>
)
}

View File

@@ -0,0 +1,42 @@
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
export function ItemDemo() {
return (
<div className="flex w-full max-w-md flex-col gap-6">
<Item variant="outline">
<ItemContent>
<ItemTitle>Two-factor authentication</ItemTitle>
<ItemDescription className="text-pretty xl:hidden 2xl:block">
Verify via email or phone number.
</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm">Enable</Button>
</ItemActions>
</Item>
<Item variant="outline" size="sm" asChild>
<a href="#">
<ItemMedia>
<BadgeCheckIcon className="size-5" />
</ItemMedia>
<ItemContent>
<ItemTitle>Your profile has been verified.</ItemTitle>
</ItemContent>
<ItemActions>
<ChevronRightIcon className="size-4" />
</ItemActions>
</a>
</Item>
</div>
)
}

View File

@@ -0,0 +1,456 @@
"use client"
import { useMemo, useState } from "react"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/registry/new-york-v4/ui/command"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
const SAMPLE_DATA = {
mentionable: [
{
type: "page",
title: "Meeting Notes",
image: "📝",
},
{
type: "page",
title: "Project Dashboard",
image: "📊",
},
{
type: "page",
title: "Ideas & Brainstorming",
image: "💡",
},
{
type: "page",
title: "Calendar & Events",
image: "📅",
},
{
type: "page",
title: "Documentation",
image: "📚",
},
{
type: "page",
title: "Goals & Objectives",
image: "🎯",
},
{
type: "page",
title: "Budget Planning",
image: "💰",
},
{
type: "page",
title: "Team Directory",
image: "👥",
},
{
type: "page",
title: "Technical Specs",
image: "🔧",
},
{
type: "page",
title: "Analytics Report",
image: "📈",
},
{
type: "user",
title: "shadcn",
image: "https://github.com/shadcn.png",
workspace: "Workspace",
},
{
type: "user",
title: "maxleiter",
image: "https://github.com/maxleiter.png",
workspace: "Workspace",
},
{
type: "user",
title: "evilrabbit",
image: "https://github.com/evilrabbit.png",
workspace: "Workspace",
},
],
models: [
{
name: "Auto",
},
{
name: "Agent Mode",
badge: "Beta",
},
{
name: "Plan Mode",
},
],
}
function MentionableIcon({
item,
}: {
item: (typeof SAMPLE_DATA.mentionable)[0]
}) {
return item.type === "page" ? (
<span className="flex size-4 items-center justify-center">
{item.image}
</span>
) : (
<Avatar className="size-4">
<AvatarImage src={item.image} />
<AvatarFallback>{item.title[0]}</AvatarFallback>
</Avatar>
)
}
export function NotionPromptForm() {
const [mentions, setMentions] = useState<string[]>([])
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
const [selectedModel, setSelectedModel] = useState<
(typeof SAMPLE_DATA.models)[0]
>(SAMPLE_DATA.models[0])
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
const grouped = useMemo(() => {
return SAMPLE_DATA.mentionable.reduce(
(acc, item) => {
const isAvailable = !mentions.includes(item.title)
if (isAvailable) {
if (!acc[item.type]) {
acc[item.type] = []
}
acc[item.type].push(item)
}
return acc
},
{} as Record<string, typeof SAMPLE_DATA.mentionable>
)
}, [mentions])
const hasMentions = mentions.length > 0
return (
<form className="[--radius:1.2rem]">
<Field>
<FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt
</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="notion-prompt"
placeholder="Ask, search, or make anything..."
/>
<InputGroupAddon align="block-start">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
>
<Tooltip>
<TooltipTrigger
asChild
onFocusCapture={(e) => e.stopPropagation()}
>
<PopoverTrigger asChild>
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="rounded-full transition-transform"
>
<IconAt /> {!hasMentions && "Add context"}
</InputGroupButton>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Mention a person, page, or date</TooltipContent>
</Tooltip>
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
<Command>
<CommandInput placeholder="Search pages..." />
<CommandList>
<CommandEmpty>No pages found</CommandEmpty>
{Object.entries(grouped).map(([type, items]) => (
<CommandGroup
key={type}
heading={type === "page" ? "Pages" : "Users"}
>
{items.map((item) => (
<CommandItem
key={item.title}
value={item.title}
onSelect={(currentValue) => {
setMentions((prev) => [...prev, currentValue])
setMentionPopoverOpen(false)
}}
>
<MentionableIcon item={item} />
{item.title}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
{mentions.map((mention) => {
const item = SAMPLE_DATA.mentionable.find(
(item) => item.title === mention
)
if (!item) {
return null
}
return (
<InputGroupButton
key={mention}
size="sm"
variant="secondary"
className="rounded-full !pl-2"
onClick={() => {
setMentions((prev) => prev.filter((m) => m !== mention))
}}
>
<MentionableIcon item={item} />
{item.title}
<IconX />
</InputGroupButton>
)
})}
</div>
</InputGroupAddon>
<InputGroupAddon align="block-end" className="gap-1">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
size="icon-sm"
className="rounded-full"
aria-label="Attach file"
>
<IconPaperclip />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>Attach file</TooltipContent>
</Tooltip>
<DropdownMenu
open={modelPopoverOpen}
onOpenChange={setModelPopoverOpen}
>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
{selectedModel.name}
</InputGroupButton>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Select AI model</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:1rem]"
>
<DropdownMenuGroup className="w-42">
<DropdownMenuLabel className="text-muted-foreground text-xs">
Select Agent Mode
</DropdownMenuLabel>
{SAMPLE_DATA.models.map((model) => (
<DropdownMenuCheckboxItem
key={model.name}
checked={model.name === selectedModel.name}
onCheckedChange={(checked) => {
if (checked) {
setSelectedModel(model)
}
}}
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
>
{model.name}
{model.badge && (
<Badge
variant="secondary"
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
>
{model.badge}
</Badge>
)}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
<IconWorld /> All Sources
</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="[--radius:1rem]"
>
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="web-search">
<IconWorld /> Web Search{" "}
<Switch
id="web-search"
className="ml-auto"
defaultChecked
/>
</label>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="apps">
<IconApps /> Apps and Integrations
<Switch id="apps" className="ml-auto" defaultChecked />
</label>
</DropdownMenuItem>
<DropdownMenuItem>
<IconCircleDashedPlus /> All Sources I can access
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Avatar className="size-4">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
shadcn
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-72 p-0 [--radius:1rem]">
<Command>
<CommandInput
placeholder="Find or use knowledge in..."
autoFocus
/>
<CommandList>
<CommandEmpty>No knowledge found</CommandEmpty>
<CommandGroup>
{SAMPLE_DATA.mentionable
.filter((item) => item.type === "user")
.map((user) => (
<CommandItem
key={user.title}
value={user.title}
onSelect={() => {
// Handle user selection here
console.log("Selected user:", user.title)
}}
>
<Avatar className="size-4">
<AvatarImage src={user.image} />
<AvatarFallback>
{user.title[0]}
</AvatarFallback>
</Avatar>
{user.title}{" "}
<span className="text-muted-foreground">
- {user.workspace}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem>
<IconBook /> Help Center
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconPlus /> Connect Apps
</DropdownMenuItem>
<DropdownMenuLabel className="text-muted-foreground text-xs">
We&apos;ll only search in the sources selected here.
</DropdownMenuLabel>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupButton
aria-label="Send"
className="ml-auto rounded-full"
variant="default"
size="icon-sm"
>
<IconArrowUp />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</form>
)
}

View File

@@ -0,0 +1,21 @@
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function SpinnerBadge() {
return (
<div className="flex items-center gap-2 [--radius:1.2rem]">
<Badge>
<Spinner />
Syncing
</Badge>
<Badge variant="secondary">
<Spinner />
Updating
</Badge>
<Badge variant="outline">
<Spinner />
Loading
</Badge>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function SpinnerEmpty() {
return (
<Empty className="w-full border md:p-6">
<EmptyHeader>
<EmptyMedia variant="icon">
<Spinner />
</EmptyMedia>
<EmptyTitle>Processing your request</EmptyTitle>
<EmptyDescription>
Please wait while we process your request. Do not refresh the page.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm">
Cancel
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -3,7 +3,6 @@ import Image from "next/image"
import Link from "next/link"
import { Announcement } from "@/components/announcement"
import { CardsDemo } from "@/components/cards"
import { ExamplesNav } from "@/components/examples-nav"
import {
PageActions,
@@ -15,6 +14,8 @@ import { PageNav } from "@/components/page-nav"
import { ThemeSelector } from "@/components/theme-selector"
import { Button } from "@/registry/new-york-v4/ui/button"
import { RootComponents } from "./components"
const title = "The Foundation for your Design System"
const description =
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code."
@@ -87,7 +88,7 @@ export default function IndexPage() {
/>
</section>
<section className="theme-container hidden md:block">
<CardsDemo />
<RootComponents />
</section>
</div>
</div>

View File

@@ -144,19 +144,19 @@ export default async function Page(props: {
)}
</div>
{links ? (
<div className="flex items-center space-x-2 pt-4">
<div className="flex items-center gap-2 pt-4">
{links?.doc && (
<Badge asChild variant="secondary">
<Link href={links.doc} target="_blank" rel="noreferrer">
<Badge asChild variant="secondary" className="rounded-full">
<a href={links.doc} target="_blank" rel="noreferrer">
Docs <IconArrowUpRight />
</Link>
</a>
</Badge>
)}
{links?.api && (
<Badge asChild variant="secondary">
<Link href={links.api} target="_blank" rel="noreferrer">
<Badge asChild variant="secondary" className="rounded-full">
<a href={links.api} target="_blank" rel="noreferrer">
API Reference <IconArrowUpRight />
</Link>
</a>
</Badge>
)}
</div>
@@ -193,7 +193,7 @@ export default async function Page(props: {
)}
</div>
</div>
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--header-height)-var(--footer-height))] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="h-(--top-spacing) shrink-0" />
{/* @ts-expect-error - revisit fumadocs types. */}
{doc.toc?.length ? (

View File

@@ -5,8 +5,14 @@ import * as React from "react"
import { cn } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Field,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import { Label } from "@/registry/new-york-v4/ui/label"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function UserAuthForm({
className,
@@ -26,11 +32,11 @@ export function UserAuthForm({
return (
<div className={cn("grid gap-6", className)} {...props}>
<form onSubmit={onSubmit}>
<div className="grid gap-2">
<div className="grid gap-1">
<Label className="sr-only" htmlFor="email">
<FieldGroup>
<Field>
<FieldLabel className="sr-only" htmlFor="email">
Email
</Label>
</FieldLabel>
<Input
id="email"
placeholder="name@example.com"
@@ -40,31 +46,18 @@ export function UserAuthForm({
autoCorrect="off"
disabled={isLoading}
/>
</div>
<Button disabled={isLoading}>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Sign In with Email
</Button>
</div>
</Field>
<Field>
<Button disabled={isLoading}>
{isLoading && <Spinner />}
Sign In with Email
</Button>
</Field>
</FieldGroup>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background text-muted-foreground px-2">
Or continue with
</span>
</div>
</div>
<FieldSeparator>Or continue with</FieldSeparator>
<Button variant="outline" type="button" disabled={isLoading}>
{isLoading ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : (
<Icons.gitHub className="mr-2 h-4 w-4" />
)}{" "}
{isLoading ? <Spinner /> : <Icons.gitHub className="mr-2 h-4 w-4" />}{" "}
GitHub
</Button>
</div>

View File

@@ -4,6 +4,7 @@ import Link from "next/link"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/registry/new-york-v4/ui/button"
import { FieldDescription } from "@/registry/new-york-v4/ui/field"
import { UserAuthForm } from "@/app/(app)/examples/authentication/components/user-auth-form"
export const metadata: Metadata = {
@@ -78,23 +79,11 @@ export default function AuthenticationPage() {
</p>
</div>
<UserAuthForm />
<p className="text-muted-foreground px-8 text-center text-sm">
<FieldDescription className="px-6 text-center">
By clicking continue, you agree to our{" "}
<Link
href="/terms"
className="hover:text-primary underline underline-offset-4"
>
Terms of Service
</Link>{" "}
and{" "}
<Link
href="/privacy"
className="hover:text-primary underline underline-offset-4"
>
Privacy Policy
</Link>
.
</p>
<Link href="/terms">Terms of Service</Link> and{" "}
<Link href="/privacy">Privacy Policy</Link>.
</FieldDescription>
</div>
</div>
</div>

View File

@@ -142,13 +142,7 @@ const chartConfig = {
export function ChartAreaInteractive() {
const isMobile = useIsMobile()
const [timeRange, setTimeRange] = React.useState("90d")
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d")
}
}, [isMobile])
const [timeRange, setTimeRange] = React.useState("7d")
const filteredData = chartData.filter((item) => {
const date = new Date(item.date)

View File

@@ -1,6 +1,7 @@
import { notFound } from "next/navigation"
import { NextResponse, type NextRequest } from "next/server"
import { processMdxForLLMs } from "@/lib/llm"
import { source } from "@/lib/source"
export const revalidate = false
@@ -17,7 +18,9 @@ export async function GET(
}
// @ts-expect-error - revisit fumadocs types.
return new NextResponse(page.data.content, {
const processedContent = processMdxForLLMs(page.data.content)
return new NextResponse(processedContent, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},

View File

@@ -0,0 +1,168 @@
import Image from "next/image"
import { CheckIcon } from "lucide-react"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Switch } from "@/registry/new-york-v4/ui/switch"
const modes = [
{
name: "Light",
value: "light",
image: "/placeholder.svg",
},
{
name: "Dark",
value: "dark",
image: "/placeholder.svg",
},
{
name: "System",
value: "system",
image: "/placeholder.svg",
},
]
const accents = [
{
name: "Blue",
value: "#007AFF",
},
{
name: "Purple",
value: "#6A4695",
},
{
name: "Red",
value: "#FF3B30",
},
{
name: "Orange",
value: "#FF9500",
},
]
export function AppearanceSettings() {
return (
<FieldSet>
<FieldLegend>Appearance</FieldLegend>
<FieldDescription>
Configure appearance. accent, scroll bar, and more.
</FieldDescription>
<FieldGroup>
<FieldSet>
<FieldLegend variant="label">Mode</FieldLegend>
<FieldDescription>
Select the mode to use for the appearance.
</FieldDescription>
<RadioGroup
className="flex flex-col gap-4 @min-[28rem]/field-group:grid @min-[28rem]/field-group:grid-cols-3"
defaultValue="light"
>
{modes.map((mode) => (
<FieldLabel
htmlFor={mode.value}
className="gap-0 overflow-hidden"
key={mode.value}
>
<Image
src={mode.image}
alt={mode.name}
width={160}
height={90}
className="hidden aspect-video w-full object-cover @min-[28rem]/field-group:block dark:brightness-[0.2] dark:grayscale"
/>
<Field
orientation="horizontal"
className="@min-[28rem]/field-group:border-t-input @min-[28rem]/field-group:border-t"
>
<FieldTitle>{mode.name}</FieldTitle>
<RadioGroupItem id={mode.value} value={mode.value} />
</Field>
</FieldLabel>
))}
</RadioGroup>
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Accent</FieldTitle>
<FieldDescription>
Select the accent color to use for the appearance.
</FieldDescription>
</FieldContent>
<FieldSet aria-label="Accent">
<RadioGroup className="flex flex-wrap gap-2" defaultValue="#007AFF">
{accents.map((accent) => (
<Label
htmlFor={accent.value}
key={accent.value}
className="flex size-6 items-center justify-center rounded-full"
style={{ backgroundColor: accent.value }}
>
<RadioGroupItem
id={accent.value}
value={accent.value}
aria-label={accent.name}
className="peer sr-only"
/>
<CheckIcon className="hidden size-4 stroke-white peer-data-[state=checked]:block" />
</Label>
))}
</RadioGroup>
</FieldSet>
</Field>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="icon-size">Sidebar Icon Size</FieldLabel>
<FieldDescription>
Select the size of the sidebar icons.
</FieldDescription>
</FieldContent>
<Select>
<SelectTrigger id="icon-size" className="ml-auto">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="small">Small</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="large">Large</SelectItem>
</SelectContent>
</Select>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
<FieldDescription>
Allow the wallpaper to be tinted with the accent color.
</FieldDescription>
</FieldContent>
<Switch id="tinting" defaultChecked />
</Field>
</FieldGroup>
</FieldSet>
)
}

View File

@@ -0,0 +1,463 @@
"use client"
import { useState } from "react"
import { CircleIcon, InfoIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
import {
Select,
SelectContent,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/new-york-v4/ui/tabs"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
const spokenLanguages = [
{ label: "English", value: "en" },
{ label: "Spanish", value: "es" },
{ label: "French", value: "fr" },
{ label: "German", value: "de" },
{ label: "Italian", value: "it" },
{ label: "Portuguese", value: "pt" },
{ label: "Russian", value: "ru" },
{ label: "Chinese", value: "zh" },
{ label: "Japanese", value: "ja" },
{ label: "Korean", value: "ko" },
{ label: "Arabic", value: "ar" },
{ label: "Hindi", value: "hi" },
{ label: "Bengali", value: "bn" },
{ label: "Telugu", value: "te" },
{ label: "Marathi", value: "mr" },
{ label: "Kannada", value: "kn" },
{ label: "Malayalam", value: "ml" },
]
const voices = [
{ label: "Samantha", value: "samantha" },
{ label: "Alex", value: "alex" },
{ label: "Fred", value: "fred" },
{ label: "Victoria", value: "victoria" },
{ label: "Tom", value: "tom" },
{ label: "Karen", value: "karen" },
{ label: "Sam", value: "sam" },
{ label: "Daniel", value: "daniel" },
]
const personalities = [
{
label: "Friendly",
value: "friendly",
description: "Friendly and approachable.",
},
{
label: "Professional",
value: "professional",
description: "Professional and authoritative.",
},
{ label: "Funny", value: "funny", description: "Funny and light-hearted." },
{
label: "Sarcastic",
value: "sarcastic",
description: "Sarcastic and witty.",
},
{ label: "Cynical", value: "cynical", description: "Cynical and skeptical." },
]
const instructions = [
{
label: "Witty",
value: "witty",
description: "Use quick and clever responses when appropriate.",
},
{
label: "Professional",
value: "professional",
description: "Have a professional and authoritative tone.",
},
{
label: "Funny",
value: "funny",
description: "Use humor and wit to engage the user.",
},
{
label: "Sarcastic",
value: "sarcastic",
description: "Use sarcasm and wit to engage the user.",
},
{
label: "Cynical",
value: "cynical",
description: "Use cynicism and skepticism to engage the user.",
},
]
export function ChatSettings() {
const [tab, setTab] = useState("general")
const [theme, setTheme] = useState("system")
const [accentColor, setAccentColor] = useState("default")
const [spokenLanguage, setSpokenLanguage] = useState("en")
const [voice, setVoice] = useState("samantha")
const [personality, setPersonality] = useState("friendly")
const [customInstructions, setCustomInstructions] = useState("")
return (
<div className="flex flex-col gap-4">
<Button variant="outline" asChild className="w-full md:hidden">
<select
value={tab}
onChange={(e) => setTab(e.target.value)}
className="appearance-none"
>
<option value="general">General</option>
<option value="notifications">Notifications</option>
<option value="personalization">Personalization</option>
<option value="security">Security</option>
</select>
</Button>
<Tabs value={tab} onValueChange={setTab}>
<TabsList className="hidden md:flex">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="notifications">Notifications</TabsTrigger>
<TabsTrigger value="personalization">Personalization</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
</TabsList>
<div className="rounded-lg border p-6 [&_[data-slot=select-trigger]]:min-w-[125px]">
<TabsContent value="general">
<FieldSet>
<FieldGroup>
<Field orientation="horizontal">
<FieldLabel htmlFor="theme">Theme</FieldLabel>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger id="theme">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldLabel htmlFor="accent-color">Accent Color</FieldLabel>
<Select value={accentColor} onValueChange={setAccentColor}>
<SelectTrigger id="accent-color">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="default">
<CircleIcon className="fill-neutral-500 stroke-neutral-500 dark:fill-neutral-400 dark:stroke-neutral-400" />
Default
</SelectItem>
<SelectItem value="red">
<CircleIcon className="fill-red-500 stroke-red-500 dark:fill-red-400 dark:stroke-red-400" />
Red
</SelectItem>
<SelectItem value="blue">
<CircleIcon className="fill-blue-500 stroke-blue-500 dark:fill-blue-400 dark:stroke-blue-400" />
Blue
</SelectItem>
<SelectItem value="green">
<CircleIcon className="fill-green-500 stroke-green-500 dark:fill-green-400 dark:stroke-green-400" />
Green
</SelectItem>
<SelectItem value="purple">
<CircleIcon className="fill-purple-500 stroke-purple-500 dark:fill-purple-400 dark:stroke-purple-400" />
Purple
</SelectItem>
<SelectItem value="pink">
<CircleIcon className="fill-pink-500 stroke-pink-500 dark:fill-pink-400 dark:stroke-pink-400" />
Pink
</SelectItem>
</SelectContent>
</Select>
</Field>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="spoken-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you mainly speak. If
it&apos;s not listed, it may still be supported via
auto-detection.
</FieldDescription>
</FieldContent>
<Select
value={spokenLanguage}
onValueChange={setSpokenLanguage}
>
<SelectTrigger id="spoken-language">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end" position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectSeparator />
{spokenLanguages.map((language) => (
<SelectItem key={language.value} value={language.value}>
{language.label}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldLabel htmlFor="voice">Voice</FieldLabel>
<Select value={voice} onValueChange={setVoice}>
<SelectTrigger id="voice">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end" position="item-aligned">
{voices.map((voice) => (
<SelectItem key={voice.value} value={voice.value}>
{voice.label}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
</FieldGroup>
</FieldSet>
</TabsContent>
<TabsContent value="notifications">
<FieldGroup>
<FieldSet>
<FieldLabel>Responses</FieldLabel>
<FieldDescription>
Get notified when ChatGPT responds to requests that take time,
like research or image generation.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field orientation="horizontal">
<Checkbox id="push" defaultChecked disabled />
<FieldLabel htmlFor="push" className="font-normal">
Push notifications
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLabel>Tasks</FieldLabel>
<FieldDescription>
Get notified when tasks you&apos;ve created have updates.{" "}
<a href="#">Manage tasks</a>
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field orientation="horizontal">
<Checkbox id="push-tasks" />
<FieldLabel htmlFor="push-tasks" className="font-normal">
Push notifications
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="email-tasks" />
<FieldLabel htmlFor="email-tasks" className="font-normal">
Email notifications
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
</FieldGroup>
</TabsContent>
<TabsContent value="personalization">
<FieldGroup>
<Field orientation="responsive">
<FieldLabel htmlFor="nickname">Nickname</FieldLabel>
<InputGroup>
<InputGroupInput
id="nickname"
placeholder="Broski"
className="@md/field-group:max-w-[200px]"
/>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton size="icon-xs">
<InfoIcon />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent className="flex items-center gap-2">
Used to identify you in the chat. <Kbd>N</Kbd>
</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</Field>
<FieldSeparator />
<Field
orientation="responsive"
className="@md/field-group:flex-col @2xl/field-group:flex-row"
>
<FieldContent>
<FieldLabel htmlFor="about">More about you</FieldLabel>
<FieldDescription>
Tell us more about yourself. This will be used to help us
personalize your experience.
</FieldDescription>
</FieldContent>
<Textarea
id="about"
placeholder="I'm a software engineer..."
className="min-h-[120px] @md/field-group:min-w-full @2xl/field-group:min-w-[300px]"
/>
</Field>
<FieldSeparator />
<FieldLabel>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="customization">
Enable customizations
</FieldLabel>
<FieldDescription>
Enable customizations to make ChatGPT more personalized.
</FieldDescription>
</FieldContent>
<Switch id="customization" defaultChecked />
</Field>
</FieldLabel>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="personality">
ChatGPT Personality
</FieldLabel>
<FieldDescription>
Set the style and tone ChatGPT should use when responding.
</FieldDescription>
</FieldContent>
<Select value={personality} onValueChange={setPersonality}>
<SelectTrigger id="personality">
{personalities.find((p) => p.value === personality)?.label}
</SelectTrigger>
<SelectContent align="end">
{personalities.map((personality) => (
<SelectItem
key={personality.value}
value={personality.value}
>
<FieldContent className="gap-0.5">
<FieldLabel>{personality.label}</FieldLabel>
<FieldDescription className="text-xs">
{personality.description}
</FieldDescription>
</FieldContent>
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
<FieldSeparator />
<Field>
<FieldLabel htmlFor="instructions">
Custom Instructions
</FieldLabel>
<Textarea
id="instructions"
value={customInstructions}
onChange={(e) => setCustomInstructions(e.target.value)}
/>
<div className="flex flex-wrap gap-2">
{instructions.map((instruction) => (
<Button
variant="outline"
key={instruction.value}
value={instruction.value}
className="rounded-full"
size="sm"
onClick={() =>
setCustomInstructions(
`${customInstructions} ${instruction.description}`
)
}
>
{instruction.label}
</Button>
))}
</div>
</Field>
</FieldGroup>
</TabsContent>
<TabsContent value="security">
<FieldGroup>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="2fa">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
If you do not have a two-factor authentication device, you
can use a one-time code sent to your email.
</FieldDescription>
</FieldContent>
<Switch id="2fa" />
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Log out</FieldTitle>
<FieldDescription>
Log out of your account on this device.
</FieldDescription>
</FieldContent>
<Button variant="outline" size="sm">
Log Out
</Button>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Log out of all devices</FieldTitle>
<FieldDescription>
This will log you out of all devices, including the current
session. It may take up to 30 minutes for the changes to
take effect.
</FieldDescription>
</FieldContent>
<Button variant="outline" size="sm">
Log Out All
</Button>
</Field>
</FieldGroup>
</TabsContent>
</div>
</Tabs>
</div>
)
}

View File

@@ -0,0 +1,137 @@
import { SunDimIcon, SunIcon } from "lucide-react"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Slider } from "@/registry/new-york-v4/ui/slider"
import { Switch } from "@/registry/new-york-v4/ui/switch"
export function DisplaySettings() {
return (
<FieldSet>
<FieldLegend>Display</FieldLegend>
<FieldDescription>
Configure display settings, brightness, refresh rate, and more.
</FieldDescription>
<FieldGroup>
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="resolution">Resolution</FieldLabel>
<FieldDescription>Select the display resolution.</FieldDescription>
</FieldContent>
<Select>
<SelectTrigger id="resolution" className="ml-auto">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="1920x1080">1920 x 1080</SelectItem>
<SelectItem value="2560x1440">2560 x 1440</SelectItem>
<SelectItem value="3840x2160">3840 x 2160</SelectItem>
<SelectItem value="auto">Auto</SelectItem>
</SelectContent>
</Select>
</Field>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldTitle>Brightness</FieldTitle>
<FieldDescription>
Adjust the display brightness level.
</FieldDescription>
</FieldContent>
<div className="flex min-w-[150px] items-center gap-2">
<SunDimIcon className="size-4 shrink-0" />
<Slider
id="brightness"
defaultValue={[75]}
max={100}
step={1}
aria-label="Brightness"
/>
<SunIcon className="size-4 shrink-0" />
</div>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="auto-brightness">
Automatically Adjust Brightness
</FieldLabel>
<FieldDescription>
Automatically adjust brightness based on ambient light.
</FieldDescription>
</FieldContent>
<Checkbox id="auto-brightness" defaultChecked />
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="true-tone">True Tone</FieldLabel>
<FieldDescription>
Automatically adjust colors to match ambient lighting.
</FieldDescription>
</FieldContent>
<Switch id="true-tone" />
</Field>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="refresh-rate">Refresh Rate</FieldLabel>
<FieldDescription>
Select the display refresh rate.
</FieldDescription>
</FieldContent>
<Select>
<SelectTrigger id="refresh-rate" className="ml-auto min-w-[200px]">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="60hz">60 Hz</SelectItem>
<SelectItem value="120hz">120 Hz</SelectItem>
<SelectItem value="144hz">144 Hz</SelectItem>
<SelectItem value="240hz">240 Hz</SelectItem>
</SelectContent>
</Select>
</Field>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="tv-connection">
When connected to TV
</FieldLabel>
<FieldDescription>
Choose display behavior when connected to a TV.
</FieldDescription>
</FieldContent>
<Select>
<SelectTrigger id="tv-connection" className="ml-auto">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="mirror">Mirror Display</SelectItem>
<SelectItem value="extend">Extend Display</SelectItem>
<SelectItem value="tv-only">TV Only</SelectItem>
<SelectItem value="auto">Auto</SelectItem>
</SelectContent>
</Select>
</Field>
</FieldGroup>
</FieldSet>
)
}

View File

@@ -0,0 +1,464 @@
"use client"
import { useMemo, useState } from "react"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconBrandAbstract,
IconBrandOpenai,
IconBrandZeit,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/registry/new-york-v4/ui/command"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
const SAMPLE_DATA = {
mentionable: [
{
type: "page",
title: "Meeting Notes",
image: "📝",
},
{
type: "page",
title: "Project Dashboard",
image: "📊",
},
{
type: "page",
title: "Ideas & Brainstorming",
image: "💡",
},
{
type: "page",
title: "Calendar & Events",
image: "📅",
},
{
type: "page",
title: "Documentation",
image: "📚",
},
{
type: "page",
title: "Goals & Objectives",
image: "🎯",
},
{
type: "page",
title: "Budget Planning",
image: "💰",
},
{
type: "page",
title: "Team Directory",
image: "👥",
},
{
type: "page",
title: "Technical Specs",
image: "🔧",
},
{
type: "page",
title: "Analytics Report",
image: "📈",
},
{
type: "user",
title: "shadcn",
image: "https://github.com/shadcn.png",
workspace: "Workspace",
},
{
type: "user",
title: "maxleiter",
image: "https://github.com/maxleiter.png",
workspace: "Cursor",
},
{
type: "user",
title: "evilrabbit",
image: "https://github.com/evilrabbit.png",
workspace: "Vercel",
},
],
models: [
{
name: "Auto",
icon: IconBrandZeit,
},
{
name: "Claude Sonnet 4",
icon: IconBrandAbstract,
badge: "Beta",
},
{
name: "GPT-5",
icon: IconBrandOpenai,
badge: "Beta",
},
],
}
function MentionableIcon({
item,
}: {
item: (typeof SAMPLE_DATA.mentionable)[0]
}) {
return item.type === "page" ? (
<span className="flex size-4 items-center justify-center">
{item.image}
</span>
) : (
<Avatar className="size-4">
<AvatarImage src={item.image} />
<AvatarFallback>{item.title[0]}</AvatarFallback>
</Avatar>
)
}
export function NotionPromptForm() {
const [mentions, setMentions] = useState<string[]>([])
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
const [selectedModel, setSelectedModel] = useState<
(typeof SAMPLE_DATA.models)[0]
>(SAMPLE_DATA.models[0])
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
const grouped = useMemo(() => {
return SAMPLE_DATA.mentionable.reduce(
(acc, item) => {
const isAvailable = !mentions.includes(item.title)
if (isAvailable) {
if (!acc[item.type]) {
acc[item.type] = []
}
acc[item.type].push(item)
}
return acc
},
{} as Record<string, typeof SAMPLE_DATA.mentionable>
)
}, [mentions])
const hasMentions = mentions.length > 0
return (
<form className="[--radius:1.2rem]">
<Field>
<FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt
</FieldLabel>
<InputGroup className="bg-background dark:bg-background shadow-none">
<InputGroupTextarea
id="notion-prompt"
placeholder="Ask, search, or make anything..."
/>
<InputGroupAddon align="block-start">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="rounded-full transition-transform"
>
<IconAt /> {!hasMentions && "Add context"}
</InputGroupButton>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Mention a person, page, or date</TooltipContent>
</Tooltip>
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
<Command>
<CommandInput placeholder="Search pages..." />
<CommandList>
<CommandEmpty>No pages found</CommandEmpty>
{Object.entries(grouped).map(([type, items]) => (
<CommandGroup
key={type}
heading={type === "page" ? "Pages" : "Users"}
>
{items.map((item) => (
<CommandItem
key={item.title}
value={item.title}
onSelect={(currentValue) => {
setMentions((prev) => [...prev, currentValue])
setMentionPopoverOpen(false)
}}
>
<MentionableIcon item={item} />
{item.title}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
{mentions.map((mention) => {
const item = SAMPLE_DATA.mentionable.find(
(item) => item.title === mention
)
if (!item) {
return null
}
return (
<InputGroupButton
key={mention}
size="sm"
variant="secondary"
className="rounded-full !pl-2"
onClick={() => {
setMentions((prev) => prev.filter((m) => m !== mention))
}}
>
<MentionableIcon item={item} />
{item.title}
<IconX />
</InputGroupButton>
)
})}
</div>
</InputGroupAddon>
<InputGroupAddon align="block-end" className="gap-1">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
size="icon-sm"
className="rounded-full"
aria-label="Attach file"
>
<IconPaperclip />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>Attach file</TooltipContent>
</Tooltip>
<DropdownMenu
open={modelPopoverOpen}
onOpenChange={setModelPopoverOpen}
>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
{selectedModel.icon && selectedModel.name !== "Auto" && (
<selectedModel.icon />
)}
{selectedModel.name}
</InputGroupButton>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Select AI model</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:1.2rem]"
>
<DropdownMenuGroup className="w-72">
<DropdownMenuLabel className="text-muted-foreground text-xs">
Get answers about your workspace
</DropdownMenuLabel>
{SAMPLE_DATA.models.map((model) => (
<DropdownMenuCheckboxItem
key={model.name}
checked={model.name === selectedModel.name}
onCheckedChange={(checked) => {
if (checked) {
setSelectedModel(model)
}
}}
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
>
{model.icon && <model.icon />}
{model.name}
{model.badge && (
<Badge
variant="secondary"
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
>
{model.badge}
</Badge>
)}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
<IconWorld /> All Sources
</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:1.2rem]"
>
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="web-search">
<IconWorld /> Web Search{" "}
<Switch
id="web-search"
className="ml-auto"
defaultChecked
/>
</label>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="apps">
<IconApps /> Apps and Integrations
<Switch id="apps" className="ml-auto" defaultChecked />
</label>
</DropdownMenuItem>
<DropdownMenuItem>
<IconCircleDashedPlus /> All Sources I can access
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Avatar className="size-4">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
shadcn
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-72 p-0 [--radius:1.2rem]">
<Command>
<CommandInput
placeholder="Find or use knowledge in..."
autoFocus
/>
<CommandList>
<CommandEmpty>No knowledge found</CommandEmpty>
<CommandGroup>
{SAMPLE_DATA.mentionable
.filter((item) => item.type === "user")
.map((user) => (
<CommandItem
key={user.title}
value={user.title}
onSelect={() => {
// Handle user selection here
console.log("Selected user:", user.title)
}}
>
<Avatar className="size-4">
<AvatarImage src={user.image} />
<AvatarFallback>
{user.title[0]}
</AvatarFallback>
</Avatar>
{user.title}{" "}
<span className="text-muted-foreground">
- {user.workspace}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem>
<IconBook /> Help Center
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconPlus /> Connect Apps
</DropdownMenuItem>
<DropdownMenuLabel className="text-muted-foreground text-xs">
We&apos;ll only search in the sources selected here.
</DropdownMenuLabel>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupButton
aria-label="Send"
className="ml-auto rounded-full"
variant="default"
size="icon-sm"
>
<IconArrowUp />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</form>
)
}

View File

@@ -0,0 +1,31 @@
import { AppearanceSettings } from "@/app/(internal)/sink/(pages)/forms/appearance-settings"
import { ChatSettings } from "@/app/(internal)/sink/(pages)/forms/chat-settings"
import { DisplaySettings } from "@/app/(internal)/sink/(pages)/forms/display-settings"
import { NotionPromptForm } from "@/app/(internal)/sink/(pages)/forms/notion-prompt-form"
import { ShipRegistrationForm } from "@/app/(internal)/sink/(pages)/forms/ship-registration-form"
import { ShippingForm } from "@/app/(internal)/sink/(pages)/forms/shipping-form"
export default function FormsPage() {
return (
<div className="@container flex flex-1 flex-col gap-12 p-4">
<div className="grid flex-1 gap-12 @3xl:grid-cols-2 @5xl:grid-cols-3 @[120rem]:grid-cols-4 [&>div]:max-w-lg">
<div className="flex flex-col gap-12">
<NotionPromptForm />
<ChatSettings />
</div>
<div className="flex flex-col gap-12">
<AppearanceSettings />
</div>
<div className="flex flex-col gap-12">
<DisplaySettings />
</div>
<div className="flex flex-col gap-12">
<ShippingForm />
</div>
<div className="col-span-2 flex flex-col gap-12">
<ShipRegistrationForm />
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,193 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
export function ShipRegistrationForm() {
return (
<div className="flex max-w-md flex-col gap-6">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold tracking-tight">
Join us in SF or online on October 23
</h1>
<FieldDescription>
Already signed up? <a href="#">Log in</a>
</FieldDescription>
</div>
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>1. Select your ticket type</FieldLegend>
<FieldDescription>
Select your ticket type to join us in San Francisco or online on
October 23.
</FieldDescription>
<Field>
<RadioGroup>
<FieldLabel htmlFor="in-person">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>In Person</FieldTitle>
<FieldDescription>
Join us in San Francisco on October 23.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="in-person" id="in-person" />
</Field>
</FieldLabel>
<FieldLabel htmlFor="online">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Online</FieldTitle>
<FieldDescription>
Join us online on October 23.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="online" id="online" />
</Field>
</FieldLabel>
</RadioGroup>
</Field>
</FieldSet>
<Field orientation="horizontal">
<Checkbox id="next-conf" />
<FieldLabel htmlFor="next-conf">
Also sign up for Next.js Conf 2025
</FieldLabel>
</Field>
<FieldSet>
<FieldLegend>2. Complete your attendee information</FieldLegend>
<FieldDescription>
By entering your information, you acknowledge that you have read
and agree to the <a href="#">Terms of Service</a> and{" "}
<a href="#">Privacy Policy</a>.
</FieldDescription>
<FieldGroup className="grid grid-cols-2 gap-x-4">
<Field>
<FieldLabel htmlFor="first-name">First Name</FieldLabel>
<Input id="first-name" placeholder="Jane" required />
</Field>
<Field>
<FieldLabel htmlFor="last-name">Last Name</FieldLabel>
<Input id="last-name" placeholder="Doe" required />
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" placeholder="jane.doe@example.com" required />
</Field>
<Field>
<FieldLabel htmlFor="company">Company</FieldLabel>
<Input id="company" placeholder="Example Inc." required />
</Field>
<Field>
<FieldLabel htmlFor="job-title">Job Title</FieldLabel>
<Input
id="job-title"
placeholder="Software Engineer"
required
/>
</Field>
<Field>
<FieldLabel htmlFor="country">Country</FieldLabel>
<Select>
<SelectTrigger>
<SelectValue placeholder="Select a country" />
</SelectTrigger>
<SelectContent>
<SelectItem value="us">United States</SelectItem>
<SelectItem value="uk">United Kingdom</SelectItem>
<SelectItem value="ca">Canada</SelectItem>
</SelectContent>
</Select>
</Field>
<Field className="col-span-2">
<FieldLabel htmlFor="topics">
What AI-related topics are you most curious about?
</FieldLabel>
<Textarea
id="topics"
placeholder="Agents, Security, Improving UX/Personalization, etc."
className="min-h-[100px]"
/>
</Field>
<Field className="col-span-2">
<FieldLabel htmlFor="workloads">
What types of AI workloads are you tackling right now?
</FieldLabel>
<Textarea id="workloads" className="min-h-[100px]" />
</Field>
</FieldGroup>
</FieldSet>
<FieldSet>
<FieldLegend>3. Buy your ticket</FieldLegend>
<FieldDescription>
Enter your card details to purchase your ticket.
</FieldDescription>
<FieldGroup className="grid grid-cols-2 gap-x-4">
<Field className="col-span-2">
<FieldLabel htmlFor="card-number">Card Number</FieldLabel>
<Input
id="card-number"
placeholder="1234 5678 9012 3456"
required
/>
</Field>
<Field>
<FieldLabel htmlFor="expiry-date">Expiry Date</FieldLabel>
<Input id="expiry-date" placeholder="MM/YY" required />
</Field>
<Field>
<FieldLabel htmlFor="cvv">CVV</FieldLabel>
<Input id="cvv" placeholder="123" required />
</Field>
<Field className="col-span-2">
<FieldLabel htmlFor="promo-code">Promo Code</FieldLabel>
<InputGroup>
<InputGroupInput id="promo-code" placeholder="PROMO10" />
<InputGroupAddon align="inline-end">
<InputGroupButton>Apply</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</FieldGroup>
</FieldSet>
<Field>
<Button type="submit">Purchase Ticket</Button>
<FieldDescription>
By clicking Purchase Ticket, you agree to the{" "}
<a href="#">Terms of Service</a> and{" "}
<a href="#">Privacy Policy</a>.
</FieldDescription>
</Field>
</FieldGroup>
</form>
</div>
)
}

View File

@@ -0,0 +1,121 @@
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
export function ShippingForm() {
return (
<FieldSet>
<FieldLegend>Shipping Details</FieldLegend>
<FieldDescription>
Please provide your shipping details so we can deliver your order.
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="street-address">Street Address</FieldLabel>
<Input id="street-address" autoComplete="off" />
</Field>
<Field>
<FieldLabel htmlFor="city">City</FieldLabel>
<Input id="city" />
</Field>
<FieldSet>
<FieldLegend variant="label">Shipping Method</FieldLegend>
<FieldDescription>
Please select the shipping method for your order.
</FieldDescription>
<RadioGroup>
<Field orientation="horizontal">
<RadioGroupItem value="standard" id="shipping-method-1" />
<FieldLabel htmlFor="shipping-method-1" className="font-normal">
Standard{" "}
<Badge className="rounded-full py-px" variant="outline">
Free
</Badge>
</FieldLabel>
</Field>
<Field orientation="horizontal">
<RadioGroupItem value="express" id="shipping-method-2" />
<FieldLabel htmlFor="shipping-method-2" className="font-normal">
Express
</FieldLabel>
</Field>
</RadioGroup>
</FieldSet>
<Field>
<FieldLabel htmlFor="message">Message</FieldLabel>
<Textarea id="message" />
<FieldDescription>Anything else you want to add?</FieldDescription>
</Field>
<FieldSet>
<FieldLegend>Additional Items</FieldLegend>
<FieldDescription>
Please select the additional items for your order.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<FieldLabel htmlFor="gift-wrapping">
<Field orientation="horizontal">
<Checkbox
value="gift-wrapping"
id="gift-wrapping"
aria-label="Gift Wrapping"
/>
<FieldContent>
<FieldTitle>Gift Wrapping</FieldTitle>
<FieldDescription>
Add elegant gift wrapping with a personalized message.
</FieldDescription>
</FieldContent>
</Field>
</FieldLabel>
<FieldLabel htmlFor="insurance">
<Field orientation="horizontal">
<Checkbox
value="insurance"
id="insurance"
aria-label="Package Insurance"
/>
<FieldContent>
<FieldTitle>Package Insurance</FieldTitle>
<FieldDescription>
Protect your shipment with comprehensive insurance coverage.
</FieldDescription>
</FieldContent>
</Field>
</FieldLabel>
<FieldLabel htmlFor="signature-confirmation">
<Field orientation="horizontal">
<Checkbox
value="signature-confirmation"
id="signature-confirmation"
aria-label="Signature Confirmation"
/>
<FieldContent>
<FieldTitle>Signature Confirmation</FieldTitle>
<FieldDescription>
Require recipient signature upon delivery for added
security.
</FieldDescription>
</FieldContent>
</Field>
</FieldLabel>
</FieldGroup>
</FieldSet>
</FieldGroup>
</FieldSet>
)
}

View File

@@ -0,0 +1,55 @@
"use server"
import { FormState } from "@/app/(internal)/sink/(pages)/next-form/example-form"
import { exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
export async function subscriptionAction(
_prevState: FormState,
formData: FormData
): Promise<FormState> {
// Simulate server processing
await new Promise((resolve) => setTimeout(resolve, 1000))
const values = {
name: formData.get("name") as string,
email: formData.get("email") as string,
plan: formData.get("plan") as "basic" | "pro",
billingPeriod: formData.get("billingPeriod") as string,
addons: formData.getAll("addons") as string[],
teamSize: parseInt(formData.get("teamSize") as string) || 1,
emailNotifications: formData.get("emailNotifications") === "on",
startDate: formData.get("startDate")
? new Date(formData.get("startDate") as string)
: new Date(),
theme: formData.get("theme") as string,
password: formData.get("password") as string,
comments: formData.get("comments") as string,
}
const result = exampleFormSchema.safeParse(values)
if (!result.success) {
return {
values,
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// Simulate some business logic validation
if (result.data.email.includes("invalid")) {
return {
values,
success: false,
errors: {
email: ["This email domain is not supported"],
},
}
}
return {
values,
errors: null,
success: true,
}
}

View File

@@ -0,0 +1,391 @@
"use client"
import * as React from "react"
import Form from "next/form"
import { z } from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/registry/new-york-v4/ui/dialog"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
import { subscriptionAction } from "./actions"
export type FormState = {
values: z.infer<typeof exampleFormSchema>
errors: null | Partial<
Record<keyof z.infer<typeof exampleFormSchema>, string[]>
>
success: boolean
}
export function ExampleForm() {
const formId = React.useId()
const [formKey, setFormKey] = React.useState(formId)
const [showResults, setShowResults] = React.useState(false)
const [formState, formAction, pending] = React.useActionState<
FormState,
FormData
>(subscriptionAction, {
values: {
name: "",
email: "",
plan: "basic",
billingPeriod: "",
addons: ["analytics"],
teamSize: 1,
emailNotifications: false,
comments: "",
startDate: new Date(),
theme: "system",
password: "",
},
errors: null,
success: false,
})
React.useEffect(() => {
if (formState.success) {
setShowResults(true)
}
}, [formState.success])
return (
<>
<Card className="w-full max-w-sm">
<CardHeader className="border-b">
<CardTitle>Subscription Form</CardTitle>
<CardDescription>
Create your subscription using server actions and useActionState.
</CardDescription>
</CardHeader>
<CardContent>
<Form action={formAction} id="subscription-form" key={formKey}>
<FieldGroup>
<Field data-invalid={!!formState.errors?.name?.length}>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input
id="name"
name="name"
defaultValue={formState.values.name}
disabled={pending}
aria-invalid={!!formState.errors?.name?.length}
autoComplete="off"
/>
<FieldDescription>Enter your name</FieldDescription>
{formState.errors?.name && (
<FieldError>{formState.errors.name[0]}</FieldError>
)}
</Field>
<Field data-invalid={!!formState.errors?.email?.length}>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
name="email"
type="email"
defaultValue={formState.values.email}
disabled={pending}
aria-invalid={!!formState.errors?.email?.length}
autoComplete="off"
/>
<FieldDescription>Enter your email address</FieldDescription>
{formState.errors?.email && (
<FieldError>{formState.errors.email[0]}</FieldError>
)}
</Field>
<FieldSeparator />
<FieldSet data-invalid={!!formState.errors?.plan?.length}>
<FieldLegend>Subscription Plan</FieldLegend>
<FieldDescription>
Choose your subscription plan.
</FieldDescription>
<RadioGroup
name="plan"
defaultValue={formState.values.plan}
disabled={pending}
aria-invalid={!!formState.errors?.plan?.length}
>
<FieldLabel htmlFor="basic">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Basic</FieldTitle>
<FieldDescription>
For individuals and small teams
</FieldDescription>
</FieldContent>
<RadioGroupItem value="basic" id="basic" />
</Field>
</FieldLabel>
<FieldLabel htmlFor="pro">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>
For businesses with higher demands
</FieldDescription>
</FieldContent>
<RadioGroupItem value="pro" id="pro" />
</Field>
</FieldLabel>
</RadioGroup>
{formState.errors?.plan && (
<FieldError>{formState.errors.plan[0]}</FieldError>
)}
</FieldSet>
<FieldSeparator />
<Field data-invalid={!!formState.errors?.billingPeriod?.length}>
<FieldLabel htmlFor="billingPeriod">Billing Period</FieldLabel>
<Select
name="billingPeriod"
defaultValue={formState.values.billingPeriod}
disabled={pending}
aria-invalid={!!formState.errors?.billingPeriod?.length}
>
<SelectTrigger id="billingPeriod">
<SelectValue placeholder="Select billing period" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="yearly">Yearly</SelectItem>
</SelectContent>
</Select>
<FieldDescription>
Choose how often you want to be billed.
</FieldDescription>
{formState.errors?.billingPeriod && (
<FieldError>{formState.errors.billingPeriod[0]}</FieldError>
)}
</Field>
<FieldSeparator />
<FieldSet data-invalid={!!formState.errors?.addons?.length}>
<FieldLegend>Add-ons</FieldLegend>
<FieldDescription>
Select additional features you&apos;d like to include.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{addons.map((addon) => (
<Field key={addon.id} orientation="horizontal">
<Checkbox
id={addon.id}
name="addons"
value={addon.id}
defaultChecked={formState.values.addons.includes(
addon.id
)}
disabled={pending}
aria-invalid={!!formState.errors?.addons?.length}
/>
<FieldContent>
<FieldLabel htmlFor={addon.id}>
{addon.title}
</FieldLabel>
<FieldDescription>{addon.description}</FieldDescription>
</FieldContent>
</Field>
))}
</FieldGroup>
{formState.errors?.addons && (
<FieldError>{formState.errors.addons[0]}</FieldError>
)}
</FieldSet>
<FieldSeparator />
<Field data-invalid={!!formState.errors?.teamSize?.length}>
<FieldLabel htmlFor="teamSize">Team Size</FieldLabel>
<Input
id="teamSize"
name="teamSize"
type="number"
min="1"
max="50"
defaultValue={formState.values.teamSize.toString()}
disabled={pending}
aria-invalid={!!formState.errors?.teamSize?.length}
/>
<FieldDescription>
How many people will be using the subscription? (1-50)
</FieldDescription>
{formState.errors?.teamSize && (
<FieldError>{formState.errors.teamSize[0]}</FieldError>
)}
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="emailNotifications">
Email Notifications
</FieldLabel>
<FieldDescription>
Receive email updates about your subscription
</FieldDescription>
</FieldContent>
<Switch
id="emailNotifications"
name="emailNotifications"
defaultChecked={formState.values.emailNotifications}
disabled={pending}
aria-invalid={!!formState.errors?.emailNotifications?.length}
/>
</Field>
<FieldSeparator />
<Field data-invalid={!!formState.errors?.startDate?.length}>
<FieldLabel htmlFor="startDate">Start Date</FieldLabel>
<Input
id="startDate"
name="startDate"
type="date"
defaultValue={
formState.values.startDate.toISOString().split("T")[0]
}
disabled={pending}
aria-invalid={!!formState.errors?.startDate?.length}
/>
<FieldDescription>
Choose when your subscription should start
</FieldDescription>
{formState.errors?.startDate && (
<FieldError>{formState.errors.startDate[0]}</FieldError>
)}
</Field>
<FieldSeparator />
<Field data-invalid={!!formState.errors?.theme?.length}>
<FieldLabel htmlFor="theme">Theme Preference</FieldLabel>
<Select
name="theme"
defaultValue={formState.values.theme}
disabled={pending}
aria-invalid={!!formState.errors?.theme?.length}
>
<SelectTrigger id="theme">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
<FieldDescription>
Choose your preferred color theme
</FieldDescription>
{formState.errors?.theme && (
<FieldError>{formState.errors.theme[0]}</FieldError>
)}
</Field>
<FieldSeparator />
<Field data-invalid={!!formState.errors?.password?.length}>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input
id="password"
name="password"
type="password"
defaultValue={formState.values.password}
placeholder="Enter your password"
disabled={pending}
aria-invalid={!!formState.errors?.password?.length}
/>
<FieldDescription>
Must contain uppercase, lowercase, number, and be 8+
characters
</FieldDescription>
{formState.errors?.password && (
<FieldError>{formState.errors.password[0]}</FieldError>
)}
</Field>
<FieldSeparator />
<Field data-invalid={!!formState.errors?.comments?.length}>
<FieldLabel htmlFor="comments">Additional Comments</FieldLabel>
<Textarea
id="comments"
name="comments"
defaultValue={formState.values.comments}
placeholder="Tell us more about your needs..."
rows={3}
disabled={pending}
aria-invalid={!!formState.errors?.comments?.length}
/>
<FieldDescription>
Share any additional requirements or feedback (10-240
characters)
</FieldDescription>
{formState.errors?.comments && (
<FieldError>{formState.errors.comments[0]}</FieldError>
)}
</Field>
</FieldGroup>
</Form>
</CardContent>
<CardFooter className="border-t">
<Field orientation="horizontal" className="justify-end">
<Button
type="button"
variant="outline"
disabled={pending}
form="subscription-form"
onClick={() => setFormKey(formKey + 1)}
>
Reset
</Button>
<Button type="submit" disabled={pending} form="subscription-form">
{pending && <Spinner />}
Create Subscription
</Button>
</Field>
</CardFooter>
</Card>
<Dialog open={showResults} onOpenChange={setShowResults}>
<DialogContent>
<DialogHeader>
<DialogTitle>Subscription Created!</DialogTitle>
<DialogDescription>
Here are the details of your subscription.
</DialogDescription>
</DialogHeader>
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
<code>{JSON.stringify(formState.values, null, 2)}</code>
</pre>
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -0,0 +1,9 @@
import { ExampleForm } from "@/app/(internal)/sink/(pages)/next-form/example-form"
export default function NextFormPage() {
return (
<div className="flex min-h-screen items-center justify-center p-4">
<ExampleForm />
</div>
)
}

View File

@@ -0,0 +1,490 @@
"use client"
import { useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { Controller, useForm } from "react-hook-form"
import z from "zod"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/registry/new-york-v4/ui/dialog"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Slider } from "@/registry/new-york-v4/ui/slider"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/registry/new-york-v4/ui/toggle-group"
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
export function ExampleForm() {
const [values, setValues] = useState<z.infer<typeof exampleFormSchema>>()
const [open, setOpen] = useState(false)
const form = useForm<z.infer<typeof exampleFormSchema>>({
resolver: zodResolver(exampleFormSchema),
mode: "onChange",
defaultValues: {
name: "",
email: "",
plan: "basic" as const,
billingPeriod: "",
addons: ["analytics"],
emailNotifications: false,
teamSize: 1,
comments: "",
startDate: new Date(),
theme: "system",
password: "",
},
})
function onSubmit(data: z.infer<typeof exampleFormSchema>) {
setValues(data)
setOpen(true)
}
return (
<>
<Card className="w-full max-w-sm">
<CardHeader className="border-b">
<CardTitle>React Hook Form</CardTitle>
<CardDescription>
This form uses React Hook Form with Zod validation.
</CardDescription>
</CardHeader>
<CardContent>
<form id="subscription-form" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="name"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
<Input
{...field}
id={field.name}
aria-invalid={isInvalid}
autoComplete="off"
/>
<FieldDescription>Enter your name</FieldDescription>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</Field>
)
}}
/>
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
{...field}
type="email"
id={field.name}
aria-invalid={isInvalid}
autoComplete="off"
/>
<FieldDescription>
Enter your email address
</FieldDescription>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</Field>
)
}}
/>
<FieldSeparator />
<Controller
name="plan"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<FieldSet data-invalid={isInvalid}>
<FieldLegend>Subscription Plan</FieldLegend>
<FieldDescription>
Choose your subscription plan.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.value}
onValueChange={field.onChange}
aria-invalid={isInvalid}
>
<FieldLabel htmlFor="basic">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Basic</FieldTitle>
<FieldDescription>
For individuals and small teams
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="basic"
id="basic"
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
<FieldLabel htmlFor="pro">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>
For businesses with higher demands
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="pro"
id="pro"
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
</RadioGroup>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</FieldSet>
)
}}
/>
<FieldSeparator />
<Controller
name="billingPeriod"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>
Billing Period
</FieldLabel>
<Select
name={field.name}
value={field.value}
onValueChange={field.onChange}
aria-invalid={isInvalid}
>
<SelectTrigger id={field.name}>
<SelectValue placeholder="Select billing period" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="yearly">Yearly</SelectItem>
</SelectContent>
</Select>
<FieldDescription>
Choose how often you want to be billed.
</FieldDescription>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</Field>
)
}}
/>
<FieldSeparator />
<Controller
name="addons"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<FieldSet data-invalid={isInvalid}>
<FieldLegend>Add-ons</FieldLegend>
<FieldDescription>
Select additional features you&apos;d like to include.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{addons.map((addon) => (
<Field key={addon.id} orientation="horizontal">
<Checkbox
id={addon.id}
name={field.name}
aria-invalid={isInvalid}
checked={field.value.includes(addon.id)}
onCheckedChange={(checked) => {
const newValue = checked
? [...field.value, addon.id]
: field.value.filter(
(value) => value !== addon.id
)
field.onChange(newValue)
field.onBlur()
}}
/>
<FieldContent>
<FieldLabel htmlFor={addon.id}>
{addon.title}
</FieldLabel>
<FieldDescription>
{addon.description}
</FieldDescription>
</FieldContent>
</Field>
))}
</FieldGroup>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</FieldSet>
)
}}
/>
<FieldSeparator />
<Controller
name="teamSize"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<Field data-invalid={isInvalid}>
<FieldTitle>Team Size</FieldTitle>
<FieldDescription>
How many people will be using the subscription?
</FieldDescription>
<Slider
id={field.name}
name={field.name}
value={[field.value]}
onValueChange={field.onChange}
min={1}
max={50}
step={1}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</Field>
)
}}
/>
<FieldSeparator />
<Controller
name="emailNotifications"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor={field.name}>
Email Notifications
</FieldLabel>
<FieldDescription>
Receive email updates about your subscription
</FieldDescription>
</FieldContent>
<Switch
id={field.name}
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</Field>
)
}}
/>
<FieldSeparator />
<Controller
name="startDate"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Start Date</FieldLabel>
<Popover>
<PopoverTrigger asChild>
<Button
id={field.name}
variant="outline"
className="justify-start"
aria-invalid={isInvalid}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
required
mode="single"
selected={field.value}
onSelect={field.onChange}
/>
</PopoverContent>
</Popover>
<FieldDescription>
Choose when your subscription should start
</FieldDescription>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</Field>
)
}}
/>
<FieldSeparator />
<Controller
name="theme"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<Field data-invalid={isInvalid}>
<FieldTitle>Theme Preference</FieldTitle>
<ToggleGroup
type="single"
variant="outline"
value={field.value}
onValueChange={(value) =>
value && field.onChange(value)
}
aria-invalid={isInvalid}
>
<ToggleGroupItem value="light">Light</ToggleGroupItem>
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
<ToggleGroupItem value="system">System</ToggleGroupItem>
</ToggleGroup>
<FieldDescription>
Choose your preferred color theme
</FieldDescription>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</Field>
)
}}
/>
<FieldSeparator />
<Controller
name="password"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
<Input
{...field}
type="password"
placeholder="Enter your password"
id={field.name}
aria-invalid={isInvalid}
/>
<FieldDescription>
Must contain uppercase, lowercase, number, and be 8+
characters
</FieldDescription>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</Field>
)
}}
/>
<FieldSeparator />
<Controller
name="comments"
control={form.control}
render={({ field, fieldState }) => {
const isInvalid = fieldState.invalid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>
Additional Comments
</FieldLabel>
<Textarea
{...field}
id={field.name}
placeholder="Tell us more about your needs..."
rows={3}
aria-invalid={isInvalid}
/>
<FieldDescription>
Share any additional requirements or feedback (10-240
characters)
</FieldDescription>
{isInvalid && <FieldError errors={[fieldState.error]} />}
</Field>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter className="border-t">
<Field orientation="horizontal" className="justify-end">
<Button
type="button"
variant="outline"
onClick={() => form.reset()}
>
Reset
</Button>
<Button type="submit" form="subscription-form">
Submit
</Button>
</Field>
</CardFooter>
</Card>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Submitted Values</DialogTitle>
<DialogDescription>
Here are the values you submitted.
</DialogDescription>
</DialogHeader>
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -0,0 +1,9 @@
import { ExampleForm } from "@/app/(internal)/sink/(pages)/react-hook-form/example-form"
export default function ReactHookFormPage() {
return (
<div className="flex min-h-screen items-center justify-center p-4">
<ExampleForm />
</div>
)
}

View File

@@ -0,0 +1,97 @@
import { z } from "zod"
export const addons = [
{
id: "analytics",
title: "Analytics",
description: "Advanced analytics and reporting",
},
{
id: "backup",
title: "Backup",
description: "Automated daily backups",
},
{
id: "support",
title: "Priority Support",
description: "24/7 premium customer support",
},
] as const
export const exampleFormSchema = z.object({
name: z
.string({
required_error: "Name is required",
invalid_type_error: "Name must be a string",
})
.min(2, "Name must be at least 2 characters")
.max(50, "Name must be less than 50 characters")
.refine((value) => !/\d/.test(value), {
message: "Name must not contain numbers",
}),
email: z
.string({
required_error: "Email is required",
})
.email("Please enter a valid email address"),
plan: z
.string({
required_error: "Please select a subscription plan",
})
.min(1, "Please select a subscription plan")
.refine((value) => value === "basic" || value === "pro", {
message: "Invalid plan selection. Please choose Basic or Pro",
}),
billingPeriod: z
.string({
required_error: "Please select a billing period",
})
.min(1, "Please select a billing period"),
addons: z
.array(z.string())
.min(1, "Please select at least one add-on")
.max(3, "You can select up to 3 add-ons"),
teamSize: z.number().min(1).max(10),
emailNotifications: z.boolean({
required_error: "Please choose email notification preference",
}),
comments: z
.string()
.min(10, "Comments must be at least 10 characters")
.max(240, "Comments must not exceed 240 characters"),
startDate: z
.date({
required_error: "Please select a start date",
invalid_type_error: "Invalid date format",
})
.min(new Date(), "Start date cannot be in the past")
.refine(
(date) => {
const now = new Date()
const oneWeekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
return date <= oneWeekFromNow
},
{
message: "Start date must be within the current week",
}
),
theme: z
.string({
required_error: "Please select a theme",
})
.min(1, "Please select a theme"),
password: z
.string({
required_error: "Password is required",
})
.min(8, "Password must be at least 8 characters")
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"Password must contain at least one uppercase letter, one lowercase letter, and one number"
),
})

View File

@@ -0,0 +1,532 @@
/* eslint-disable react/no-children-prop */
"use client"
import * as React from "react"
import { useForm } from "@tanstack/react-form"
import { format } from "date-fns"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/registry/new-york-v4/ui/dialog"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Slider } from "@/registry/new-york-v4/ui/slider"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/registry/new-york-v4/ui/toggle-group"
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
export function ExampleForm() {
const form = useForm({
defaultValues: {
name: "",
email: "",
plan: "",
billingPeriod: "",
addons: ["analytics"],
emailNotifications: false,
teamSize: 1,
comments: "",
startDate: new Date(),
theme: "system",
password: "",
},
validators: {
onChange: exampleFormSchema,
},
onSubmit: async ({ value }) => {
setValues(value)
setOpen(true)
},
})
const [values, setValues] = React.useState<typeof form.state.values>()
const [open, setOpen] = React.useState(false)
return (
<>
<Card className="w-full max-w-sm">
<CardHeader className="border-b">
<CardTitle>Example Form</CardTitle>
<CardDescription>
This is an example form using TanStack Form.
</CardDescription>
</CardHeader>
<CardContent>
<form
id="example-form"
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
void form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="name"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
autoComplete="off"
/>
<FieldDescription>Enter your name</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<form.Field
name="email"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
id={field.name}
name={field.name}
type="email"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
autoComplete="off"
/>
<FieldDescription>
Enter your email address
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<FieldSeparator />
<form.Field
name="plan"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet data-invalid={isInvalid}>
<FieldLegend>Subscription Plan</FieldLegend>
<FieldDescription>
Choose your subscription plan.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
aria-invalid={isInvalid}
>
<FieldLabel htmlFor="basic">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Basic</FieldTitle>
<FieldDescription>
For individuals and small teams
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="basic"
id="basic"
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
<FieldLabel htmlFor="pro">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Pro</FieldTitle>
<FieldDescription>
For businesses with higher demands
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="pro"
id="pro"
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
</RadioGroup>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldSet>
)
}}
/>
<FieldSeparator />
<form.Field
name="billingPeriod"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>
Billing Period
</FieldLabel>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
aria-invalid={isInvalid}
>
<SelectTrigger id={field.name}>
<SelectValue placeholder="Select billing period" />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="yearly">Yearly</SelectItem>
</SelectContent>
</Select>
<FieldDescription>
Choose how often you want to be billed.
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<FieldSeparator />
<form.Field
name="addons"
mode="array"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet data-invalid={isInvalid}>
<FieldLegend>Add-ons</FieldLegend>
<FieldDescription>
Select additional features you&apos;d like to include.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{addons.map((addon) => (
<Field key={addon.id} orientation="horizontal">
<Checkbox
id={addon.id}
name={field.name}
aria-invalid={isInvalid}
checked={field.state.value.includes(addon.id)}
onCheckedChange={(checked) => {
if (checked) {
field.pushValue(addon.id)
} else {
const index = field.state.value.indexOf(
addon.id
)
if (index > -1) {
field.removeValue(index)
}
}
}}
/>
<FieldContent>
<FieldLabel htmlFor={addon.id}>
{addon.title}
</FieldLabel>
<FieldDescription>
{addon.description}
</FieldDescription>
</FieldContent>
</Field>
))}
</FieldGroup>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldSet>
)
}}
/>
<FieldSeparator />
<form.Field
name="teamSize"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldTitle>Team Size</FieldTitle>
<FieldDescription>
How many people will be using the subscription?
</FieldDescription>
<Slider
id={field.name}
name={field.name}
value={[field.state.value]}
onValueChange={(value) => field.handleChange(value[0])}
min={1}
max={50}
step={10}
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<FieldSeparator />
<form.Field
name="emailNotifications"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor={field.name}>
Email Notifications
</FieldLabel>
<FieldDescription>
Receive email updates about your subscription
</FieldDescription>
</FieldContent>
<Switch
id={field.name}
name={field.name}
checked={field.state.value}
onCheckedChange={field.handleChange}
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<FieldSeparator />
<form.Field
name="startDate"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Start Date</FieldLabel>
<Popover>
<PopoverTrigger asChild>
<Button
id={field.name}
variant="outline"
className="justify-start"
>
{field.state.value ? (
format(field.state.value, "PPP")
) : (
<span>Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
required
mode="single"
selected={field.state.value}
onSelect={field.handleChange}
/>
</PopoverContent>
</Popover>
<FieldDescription>
Choose when your subscription should start
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<FieldSeparator />
<form.Field
name="theme"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldTitle>Theme Preference</FieldTitle>
<ToggleGroup
id={field.name}
type="single"
variant="outline"
value={field.state.value}
onValueChange={(value) =>
value && field.handleChange(value)
}
aria-invalid={isInvalid}
>
<ToggleGroupItem value="light">Light</ToggleGroupItem>
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
<ToggleGroupItem value="system">System</ToggleGroupItem>
</ToggleGroup>
<FieldDescription>
Choose your preferred color theme
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<FieldSeparator />
<form.Field
name="password"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
<Input
id={field.name}
name={field.name}
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
placeholder="Enter your password"
aria-invalid={isInvalid}
/>
<FieldDescription>
Must contain uppercase, lowercase, number, and be 8+
characters
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<FieldSeparator />
<form.Field
name="comments"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>
Additional Comments
</FieldLabel>
<Textarea
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
placeholder="Tell us more about your needs..."
rows={3}
aria-invalid={isInvalid}
/>
<FieldDescription>
Share any additional requirements or feedback (10-240
characters)
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter className="border-t">
<Field orientation="horizontal" className="justify-end">
<Button
type="button"
variant="outline"
onClick={() => form.reset()}
>
Reset
</Button>
<Button type="submit" form="example-form">
Submit
</Button>
</Field>
</CardFooter>
</Card>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Submitted Values</DialogTitle>
<DialogDescription>
Here are the values you submitted.
</DialogDescription>
</DialogHeader>
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
</DialogContent>
</Dialog>
</>
)
}

View File

@@ -0,0 +1,9 @@
import { ExampleForm } from "@/app/(internal)/sink/(pages)/tanstack-form/example-form"
export default function TanstackFormPage() {
return (
<div className="flex min-h-screen items-center justify-center p-4">
<ExampleForm />
</div>
)
}

View File

@@ -0,0 +1,54 @@
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
export const dynamic = "force-static"
export const revalidate = false
export async function generateStaticParams() {
return Object.keys(componentRegistry).map((name) => ({
name,
}))
}
export async function generateMetadata({
params,
}: {
params: Promise<{ name: string }>
}): Promise<Metadata> {
const { name } = await params
const component = componentRegistry[name as keyof typeof componentRegistry]
if (!component) {
return {
title: "Component Not Found",
}
}
return {
title: `${component.name} - Kitchen Sink`,
description: `Demo page for ${component.name} component`,
}
}
export default async function ComponentPage({
params,
}: {
params: Promise<{ name: string }>
}) {
const { name } = await params
const component = componentRegistry[name as keyof typeof componentRegistry]
if (!component) {
notFound()
}
const Component = component.component
return (
<div className="p-6">
<Component />
</div>
)
}

View File

@@ -0,0 +1,429 @@
import FormsPage from "@/app/(internal)/sink/(pages)/forms/page"
import NextFormPage from "./(pages)/next-form/page"
import ReactHookFormPage from "./(pages)/react-hook-form/page"
import TanstackFormPage from "./(pages)/tanstack-form/page"
import { AccordionDemo } from "./components/accordion-demo"
import { AlertDemo } from "./components/alert-demo"
import { AlertDialogDemo } from "./components/alert-dialog-demo"
import { AspectRatioDemo } from "./components/aspect-ratio-demo"
import { AvatarDemo } from "./components/avatar-demo"
import { BadgeDemo } from "./components/badge-demo"
import { BreadcrumbDemo } from "./components/breadcrumb-demo"
import { ButtonDemo } from "./components/button-demo"
import { ButtonGroupDemo } from "./components/button-group-demo"
import { CalendarDemo } from "./components/calendar-demo"
import { CardDemo } from "./components/card-demo"
import { CarouselDemo } from "./components/carousel-demo"
import { ChartDemo } from "./components/chart-demo"
import { CheckboxDemo } from "./components/checkbox-demo"
import { CollapsibleDemo } from "./components/collapsible-demo"
import { ComboboxDemo } from "./components/combobox-demo"
import { CommandDemo } from "./components/command-demo"
import { ContextMenuDemo } from "./components/context-menu-demo"
import { DatePickerDemo } from "./components/date-picker-demo"
import { DialogDemo } from "./components/dialog-demo"
import { DrawerDemo } from "./components/drawer-demo"
import { DropdownMenuDemo } from "./components/dropdown-menu-demo"
import { EmptyDemo } from "./components/empty-demo"
import { FieldDemo } from "./components/field-demo"
import { FormDemo } from "./components/form-demo"
import { HoverCardDemo } from "./components/hover-card-demo"
import { InputDemo } from "./components/input-demo"
import { InputGroupDemo } from "./components/input-group-demo"
import { InputOTPDemo } from "./components/input-otp-demo"
import { ItemDemo } from "./components/item-demo"
import { KbdDemo } from "./components/kbd-demo"
import { LabelDemo } from "./components/label-demo"
import { MenubarDemo } from "./components/menubar-demo"
import { NavigationMenuDemo } from "./components/navigation-menu-demo"
import { PaginationDemo } from "./components/pagination-demo"
import { PopoverDemo } from "./components/popover-demo"
import { ProgressDemo } from "./components/progress-demo"
import { RadioGroupDemo } from "./components/radio-group-demo"
import { ResizableDemo } from "./components/resizable-demo"
import { ScrollAreaDemo } from "./components/scroll-area-demo"
import { SelectDemo } from "./components/select-demo"
import { SeparatorDemo } from "./components/separator-demo"
import { SheetDemo } from "./components/sheet-demo"
import { SkeletonDemo } from "./components/skeleton-demo"
import { SliderDemo } from "./components/slider-demo"
import { SonnerDemo } from "./components/sonner-demo"
import { SpinnerDemo } from "./components/spinner-demo"
import { SwitchDemo } from "./components/switch-demo"
import { TableDemo } from "./components/table-demo"
import { TabsDemo } from "./components/tabs-demo"
import { TextareaDemo } from "./components/textarea-demo"
import { ToggleDemo } from "./components/toggle-demo"
import { ToggleGroupDemo } from "./components/toggle-group-demo"
import { TooltipDemo } from "./components/tooltip-demo"
type ComponentConfig = {
name: string
component: React.ComponentType
className?: string
type: "registry:ui" | "registry:page" | "registry:block"
href: string
label?: string
}
export const componentRegistry: Record<string, ComponentConfig> = {
accordion: {
name: "Accordion",
component: AccordionDemo,
type: "registry:ui",
href: "/sink/accordion",
},
alert: {
name: "Alert",
component: AlertDemo,
type: "registry:ui",
href: "/sink/alert",
},
"alert-dialog": {
name: "Alert Dialog",
component: AlertDialogDemo,
type: "registry:ui",
href: "/sink/alert-dialog",
},
"aspect-ratio": {
name: "Aspect Ratio",
component: AspectRatioDemo,
type: "registry:ui",
href: "/sink/aspect-ratio",
},
avatar: {
name: "Avatar",
component: AvatarDemo,
type: "registry:ui",
href: "/sink/avatar",
},
badge: {
name: "Badge",
component: BadgeDemo,
type: "registry:ui",
href: "/sink/badge",
},
breadcrumb: {
name: "Breadcrumb",
component: BreadcrumbDemo,
type: "registry:ui",
href: "/sink/breadcrumb",
},
button: {
name: "Button",
component: ButtonDemo,
type: "registry:ui",
href: "/sink/button",
},
"button-group": {
name: "Button Group",
component: ButtonGroupDemo,
type: "registry:ui",
href: "/sink/button-group",
label: "New",
},
calendar: {
name: "Calendar",
component: CalendarDemo,
type: "registry:ui",
href: "/sink/calendar",
},
card: {
name: "Card",
component: CardDemo,
type: "registry:ui",
href: "/sink/card",
},
carousel: {
name: "Carousel",
component: CarouselDemo,
type: "registry:ui",
href: "/sink/carousel",
},
chart: {
name: "Chart",
component: ChartDemo,
className: "w-full",
type: "registry:ui",
href: "/sink/chart",
},
checkbox: {
name: "Checkbox",
component: CheckboxDemo,
type: "registry:ui",
href: "/sink/checkbox",
},
collapsible: {
name: "Collapsible",
component: CollapsibleDemo,
type: "registry:ui",
href: "/sink/collapsible",
},
combobox: {
name: "Combobox",
component: ComboboxDemo,
type: "registry:ui",
href: "/sink/combobox",
},
command: {
name: "Command",
component: CommandDemo,
type: "registry:ui",
href: "/sink/command",
},
"context-menu": {
name: "Context Menu",
component: ContextMenuDemo,
type: "registry:ui",
href: "/sink/context-menu",
},
"date-picker": {
name: "Date Picker",
component: DatePickerDemo,
type: "registry:ui",
href: "/sink/date-picker",
},
dialog: {
name: "Dialog",
component: DialogDemo,
type: "registry:ui",
href: "/sink/dialog",
},
drawer: {
name: "Drawer",
component: DrawerDemo,
type: "registry:ui",
href: "/sink/drawer",
},
"dropdown-menu": {
name: "Dropdown Menu",
component: DropdownMenuDemo,
type: "registry:ui",
href: "/sink/dropdown-menu",
},
empty: {
name: "Empty",
component: EmptyDemo,
type: "registry:ui",
href: "/sink/empty",
label: "New",
},
field: {
name: "Field",
component: FieldDemo,
type: "registry:ui",
href: "/sink/field",
label: "New",
},
form: {
name: "Form",
component: FormDemo,
type: "registry:ui",
href: "/sink/form",
},
"hover-card": {
name: "Hover Card",
component: HoverCardDemo,
type: "registry:ui",
href: "/sink/hover-card",
},
input: {
name: "Input",
component: InputDemo,
type: "registry:ui",
href: "/sink/input",
},
"input-group": {
name: "Input Group",
component: InputGroupDemo,
type: "registry:ui",
href: "/sink/input-group",
label: "New",
},
"input-otp": {
name: "Input OTP",
component: InputOTPDemo,
type: "registry:ui",
href: "/sink/input-otp",
},
item: {
name: "Item",
component: ItemDemo,
type: "registry:ui",
href: "/sink/item",
label: "New",
},
kbd: {
name: "Kbd",
component: KbdDemo,
type: "registry:ui",
href: "/sink/kbd",
label: "New",
},
label: {
name: "Label",
component: LabelDemo,
type: "registry:ui",
href: "/sink/label",
},
menubar: {
name: "Menubar",
component: MenubarDemo,
type: "registry:ui",
href: "/sink/menubar",
},
"navigation-menu": {
name: "Navigation Menu",
component: NavigationMenuDemo,
type: "registry:ui",
href: "/sink/navigation-menu",
},
pagination: {
name: "Pagination",
component: PaginationDemo,
type: "registry:ui",
href: "/sink/pagination",
},
popover: {
name: "Popover",
component: PopoverDemo,
type: "registry:ui",
href: "/sink/popover",
},
progress: {
name: "Progress",
component: ProgressDemo,
type: "registry:ui",
href: "/sink/progress",
},
"radio-group": {
name: "Radio Group",
component: RadioGroupDemo,
type: "registry:ui",
href: "/sink/radio-group",
},
resizable: {
name: "Resizable",
component: ResizableDemo,
type: "registry:ui",
href: "/sink/resizable",
},
"scroll-area": {
name: "Scroll Area",
component: ScrollAreaDemo,
type: "registry:ui",
href: "/sink/scroll-area",
},
select: {
name: "Select",
component: SelectDemo,
type: "registry:ui",
href: "/sink/select",
},
separator: {
name: "Separator",
component: SeparatorDemo,
type: "registry:ui",
href: "/sink/separator",
},
sheet: {
name: "Sheet",
component: SheetDemo,
type: "registry:ui",
href: "/sink/sheet",
},
skeleton: {
name: "Skeleton",
component: SkeletonDemo,
type: "registry:ui",
href: "/sink/skeleton",
},
slider: {
name: "Slider",
component: SliderDemo,
type: "registry:ui",
href: "/sink/slider",
},
sonner: {
name: "Sonner",
component: SonnerDemo,
type: "registry:ui",
href: "/sink/sonner",
},
spinner: {
name: "Spinner",
component: SpinnerDemo,
type: "registry:ui",
href: "/sink/spinner",
label: "New",
},
switch: {
name: "Switch",
component: SwitchDemo,
type: "registry:ui",
href: "/sink/switch",
},
table: {
name: "Table",
component: TableDemo,
type: "registry:ui",
href: "/sink/table",
},
tabs: {
name: "Tabs",
component: TabsDemo,
type: "registry:ui",
href: "/sink/tabs",
},
textarea: {
name: "Textarea",
component: TextareaDemo,
type: "registry:ui",
href: "/sink/textarea",
},
toggle: {
name: "Toggle",
component: ToggleDemo,
type: "registry:ui",
href: "/sink/toggle",
},
"toggle-group": {
name: "Toggle Group",
component: ToggleGroupDemo,
type: "registry:ui",
href: "/sink/toggle-group",
},
tooltip: {
name: "Tooltip",
component: TooltipDemo,
type: "registry:ui",
href: "/sink/tooltip",
},
blocks: {
name: "Forms",
component: FormsPage,
type: "registry:page",
href: "/sink/forms",
},
"next-form": {
name: "Next.js Form",
component: NextFormPage,
type: "registry:page",
href: "/sink/next-form",
},
"tanstack-form": {
name: "Tanstack Form",
component: TanstackFormPage,
type: "registry:page",
href: "/sink/tanstack-form",
},
"react-hook-form": {
name: "React Hook Form",
component: ReactHookFormPage,
type: "registry:page",
href: "/sink/react-hook-form",
},
}
export type ComponentKey = keyof typeof componentRegistry

View File

@@ -0,0 +1,43 @@
"use client"
import { useParams } from "next/navigation"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/registry/new-york-v4/ui/breadcrumb"
export function AppBreadcrumbs() {
const params = useParams()
const { name } = params
if (!name) {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage>Kitchen Sink</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/sink">Kitchen Sink</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden sm:flex" />
<BreadcrumbItem className="hidden sm:block">
<BreadcrumbPage className="capitalize">{name}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@@ -2,6 +2,7 @@
import * as React from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
AudioWaveform,
BookOpen,
@@ -9,7 +10,7 @@ import {
ChevronRightIcon,
Command,
GalleryVerticalEnd,
Search,
SearchIcon,
Settings2,
SquareTerminal,
} from "lucide-react"
@@ -22,6 +23,11 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/registry/new-york-v4/ui/collapsible"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
Sidebar,
@@ -31,7 +37,6 @@ import {
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
@@ -40,6 +45,7 @@ import {
SidebarMenuSubItem,
SidebarRail,
} from "@/registry/new-york-v4/ui/sidebar"
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
// This is sample data.
const data = {
@@ -163,8 +169,9 @@ const data = {
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname()
return (
<Sidebar collapsible="icon" {...props}>
<Sidebar side="left" collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
<SidebarGroup className="py-0 group-data-[collapsible=icon]:hidden">
@@ -173,12 +180,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<Label htmlFor="search" className="sr-only">
Search
</Label>
<SidebarInput
id="search"
placeholder="Search the docs..."
className="pl-8"
/>
<Search className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
<InputGroup className="bg-background h-8 shadow-none">
<InputGroupInput
id="search"
placeholder="Search the docs..."
className="h-7"
data-slot="input-group-control"
/>
<InputGroupAddon>
<SearchIcon className="text-muted-foreground" />
</InputGroupAddon>
</InputGroup>
</form>
</SidebarGroupContent>
</SidebarGroup>
@@ -221,17 +233,58 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarMenu>
</SidebarGroup>
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Components</SidebarGroupLabel>
<SidebarMenu>
{data.components.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<Link href={`/sink#${item.name}`}>
<span>{getComponentName(item.name)}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
{["registry:ui", "registry:page", "registry:block"].map((type) => {
const typeComponents = Object.entries(componentRegistry).filter(
([, item]) => item.type === type
)
if (typeComponents.length === 0) {
return null
}
return (
<Collapsible
key={type}
asChild
defaultOpen={pathname.includes("/sink/")}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<span>
{type === "registry:ui"
? "Components"
: type === "registry:page"
? "Pages"
: "Blocks"}
</span>
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{typeComponents.map(([key, item]) => (
<SidebarMenuSubItem key={key}>
<SidebarMenuSubButton
asChild
isActive={pathname === item.href}
>
<Link href={item.href}>
<span>{item.name}</span>
{item.label && (
<span className="flex size-2 rounded-full bg-blue-500" />
)}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
})}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
@@ -242,8 +295,3 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</Sidebar>
)
}
function getComponentName(name: string) {
// convert kebab-case to title case
return name.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
}

View File

@@ -31,7 +31,10 @@ export function AvatarDemo() {
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
@@ -48,7 +51,10 @@ export function AvatarDemo() {
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
@@ -65,7 +71,10 @@ export function AvatarDemo() {
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>

View File

@@ -0,0 +1,581 @@
"use client"
import { useState } from "react"
import {
IconArrowRight,
IconBrandGithubCopilot,
IconChevronDown,
IconCircleCheck,
IconCloudCode,
IconHeart,
IconMinus,
IconPin,
IconPlus,
IconUserCircle,
} from "@tabler/icons-react"
import {
AlertTriangleIcon,
ArrowLeftIcon,
ArrowRightIcon,
AudioLinesIcon,
CheckIcon,
ChevronDownIcon,
CopyIcon,
FlipHorizontalIcon,
FlipVerticalIcon,
MoreHorizontalIcon,
PercentIcon,
RotateCwIcon,
SearchIcon,
ShareIcon,
TrashIcon,
UserRoundXIcon,
VolumeOffIcon,
} from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
} from "@/registry/new-york-v4/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import { Field, FieldGroup } from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function ButtonGroupDemo() {
const [currency, setCurrency] = useState("$")
return (
<div className="flex gap-12">
<div className="flex max-w-sm flex-col gap-6">
<ButtonGroup>
<Button>Button</Button>
<Button>
Get Started <IconArrowRight />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button>Button</Button>
<ButtonGroupSeparator className="bg-primary/80" />
<Button>
Get Started <IconArrowRight />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Button</Button>
<Input placeholder="Type something here..." />
</ButtonGroup>
<ButtonGroup>
<Input placeholder="Type something here..." />
<Button variant="outline">Button</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Button</Button>
<Button variant="outline">Another Button</Button>
</ButtonGroup>
<ButtonGroup>
<ButtonGroupText>Text</ButtonGroupText>
<Button variant="outline">Another Button</Button>
</ButtonGroup>
<ButtonGroup>
<ButtonGroupText asChild>
<Label htmlFor="input">
<IconCloudCode /> GPU Size
</Label>
</ButtonGroupText>
<Input id="input" placeholder="Type something here..." />
</ButtonGroup>
<ButtonGroup>
<ButtonGroupText>Prefix</ButtonGroupText>
<Input id="input" placeholder="Type something here..." />
<ButtonGroupText>Suffix</ButtonGroupText>
</ButtonGroup>
<div className="flex gap-4">
<ButtonGroup>
<Button variant="outline">Update</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Disable</DropdownMenuItem>
<DropdownMenuItem variant="destructive">
Uninstall
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
<ButtonGroup className="[--radius:9999px]">
<Button variant="outline">Follow</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="!pl-2">
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="[--radius:0.95rem]">
<DropdownMenuGroup>
<DropdownMenuItem>
<VolumeOffIcon />
Mute Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<CheckIcon />
Mark as Read
</DropdownMenuItem>
<DropdownMenuItem>
<AlertTriangleIcon />
Report Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<UserRoundXIcon />
Block User
</DropdownMenuItem>
<DropdownMenuItem>
<ShareIcon />
Share Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<CopyIcon />
Copy Conversation
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<TrashIcon />
Delete Conversation
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
<ButtonGroup className="[--radius:0.9rem]">
<Button variant="secondary">Actions</Button>
<ButtonGroupSeparator />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary">
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="[--radius:0.9rem]">
<DropdownMenuGroup>
<DropdownMenuItem>
<IconCircleCheck />
Select Messages
</DropdownMenuItem>
<DropdownMenuItem>
<IconPin />
Edit Pins
</DropdownMenuItem>
<DropdownMenuItem>
<IconUserCircle />
Set Up Name & Photo
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</div>
<Field>
<Label htmlFor="amount">Amount</Label>
<ButtonGroup>
<Select value={currency} onValueChange={setCurrency}>
<SelectTrigger className="font-mono">{currency}</SelectTrigger>
<SelectContent>
<SelectItem value="$">$</SelectItem>
<SelectItem value="€"></SelectItem>
<SelectItem value="£">£</SelectItem>
</SelectContent>
</Select>
<Input placeholder="Enter amount to send" />
<Button variant="outline">
<ArrowRightIcon />
</Button>
</ButtonGroup>
</Field>
</div>
<div className="flex max-w-xs flex-col gap-6">
<ButtonGroup className="[--spacing:0.2rem]">
<Button variant="outline">
<FlipHorizontalIcon />
</Button>
<Button variant="outline">
<FlipVerticalIcon />
</Button>
<Button variant="outline">
<RotateCwIcon />
</Button>
<InputGroup>
<InputGroupInput placeholder="0.00" />
<InputGroupAddon
align="inline-end"
className="text-muted-foreground"
>
<PercentIcon />
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
<div className="flex gap-2 [--radius:0.95rem] [--ring:var(--color-blue-300)] [--spacing:0.22rem] **:[.shadow-xs]:shadow-none">
<InputGroup>
<InputGroupInput placeholder="Type to search..." />
<InputGroupAddon
align="inline-start"
className="text-muted-foreground"
>
<SearchIcon />
</InputGroupAddon>
</InputGroup>
<ButtonGroup>
<Button variant="outline">
<IconBrandGithubCopilot />
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">
<IconCloudCode />
<IconChevronDown />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
<div className="px-4 py-3">
<div className="text-sm font-medium">Agent Tasks</div>
</div>
<Separator />
<div className="p-4 *:[p:not(:last-child)]:mb-2">
<Textarea
placeholder="Describe your task in natural language."
className="mb-4 resize-none"
/>
<p className="font-medium">Start a new task with Copilot</p>
<p className="text-muted-foreground">
Describe your task in natural language. Copilot will work in
the background and open a pull request for your review.
</p>
</div>
</PopoverContent>
</Popover>
</ButtonGroup>
</div>
<FieldGroup className="grid grid-cols-2 gap-4 [--spacing:0.22rem]">
<Field>
<Label htmlFor="width">Width</Label>
<ButtonGroup>
<InputGroup>
<InputGroupInput id="width" />
<InputGroupAddon className="text-muted-foreground">
W
</InputGroupAddon>
<InputGroupAddon
align="inline-end"
className="text-muted-foreground"
>
px
</InputGroupAddon>
</InputGroup>
<Button variant="outline" size="icon">
<IconMinus />
</Button>
<Button variant="outline" size="icon">
<IconPlus />
</Button>
</ButtonGroup>
</Field>
<Field className="w-full">
<Label htmlFor="color">Color</Label>
<ButtonGroup className="w-full">
<InputGroup>
<InputGroupInput id="color" />
<InputGroupAddon align="inline-start">
<Popover>
<PopoverTrigger asChild>
<InputGroupButton>
<span className="size-4 rounded-xs bg-blue-600" />
</InputGroupButton>
</PopoverTrigger>
<PopoverContent
align="start"
className="max-w-48 rounded-lg p-2"
alignOffset={-8}
sideOffset={8}
>
<div className="flex flex-wrap gap-1.5">
{[
"#EA4335", // Red
"#FBBC04", // Yellow
"#34A853", // Green
"#4285F4", // Blue
"#9333EA", // Purple
"#EC4899", // Pink
"#10B981", // Emerald
"#F97316", // Orange
"#6366F1", // Indigo
"#14B8A6", // Teal
"#8B5CF6", // Violet
"#F59E0B", // Amber
].map((color) => (
<div
key={color}
className="size-6 cursor-pointer rounded-sm transition-transform hover:scale-110"
style={{ backgroundColor: color }}
/>
))}
</div>
</PopoverContent>
</Popover>
</InputGroupAddon>
<InputGroupAddon
align="inline-end"
className="text-muted-foreground"
>
%
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
</Field>
</FieldGroup>
<ButtonGroup>
<Button variant="outline">
<IconHeart /> Like
</Button>
<Button
variant="outline"
asChild
className="text-muted-foreground pointer-events-none px-2"
>
<span>1.2K</span>
</Button>
</ButtonGroup>
<ExportButtonGroup />
<ButtonGroup>
<Select defaultValue="hours">
<SelectTrigger id="duration">
<SelectValue placeholder="Select duration" />
</SelectTrigger>
<SelectContent>
<SelectItem value="hours">Hours</SelectItem>
<SelectItem value="days">Days</SelectItem>
<SelectItem value="weeks">Weeks</SelectItem>
</SelectContent>
</Select>
<Input />
</ButtonGroup>
<ButtonGroup className="[--radius:9999rem]">
<ButtonGroup>
<Button variant="outline" size="icon">
<IconPlus />
</Button>
</ButtonGroup>
<ButtonGroup>
<InputGroup>
<InputGroupInput placeholder="Send a message..." />
<Tooltip>
<TooltipTrigger asChild>
<InputGroupAddon align="inline-end">
<AudioLinesIcon />
</InputGroupAddon>
</TooltipTrigger>
<TooltipContent>Voice Mode</TooltipContent>
</Tooltip>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
<ArrowLeftIcon />
Previous
</Button>
<Button variant="outline" size="sm">
1
</Button>
<Button variant="outline" size="sm">
2
</Button>
<Button variant="outline" size="sm">
3
</Button>
<Button variant="outline" size="sm">
4
</Button>
<Button variant="outline" size="sm">
5
</Button>
<Button variant="outline" size="sm">
Next
<ArrowRightIcon />
</Button>
</ButtonGroup>
<ButtonGroup className="[--radius:0.9rem] [--spacing:0.22rem]">
<ButtonGroup>
<Button variant="outline">1</Button>
<Button variant="outline">2</Button>
<Button variant="outline">3</Button>
<Button variant="outline">4</Button>
<Button variant="outline">5</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon">
<ArrowLeftIcon />
</Button>
<Button variant="outline" size="icon">
<ArrowRightIcon />
</Button>
</ButtonGroup>
</ButtonGroup>
<ButtonGroup>
<ButtonGroup>
<Button variant="outline">
<ArrowLeftIcon />
</Button>
<Button variant="outline">
<ArrowRightIcon />
</Button>
</ButtonGroup>
<ButtonGroup aria-label="Single navigation button">
<Button variant="outline" size="icon">
<ArrowLeftIcon />
</Button>
</ButtonGroup>
</ButtonGroup>
</div>
<div className="flex max-w-xs flex-col gap-6">
<Field>
<Label id="alignment-label">Text Alignment</Label>
<ButtonGroup aria-labelledby="alignment-label">
<Button variant="outline" size="sm">
Left
</Button>
<Button variant="outline" size="sm">
Center
</Button>
<Button variant="outline" size="sm">
Right
</Button>
<Button variant="outline" size="sm">
Justify
</Button>
</ButtonGroup>
</Field>
<div className="flex gap-6">
<ButtonGroup
orientation="vertical"
aria-label="Media controls"
className="h-fit"
>
<Button variant="outline" size="icon">
<IconPlus />
</Button>
<Button variant="outline" size="icon">
<IconMinus />
</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" aria-label="Design tools palette">
<ButtonGroup orientation="vertical">
<Button variant="outline" size="icon">
<SearchIcon />
</Button>
<Button variant="outline" size="icon">
<CopyIcon />
</Button>
<Button variant="outline" size="icon">
<ShareIcon />
</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical">
<Button variant="outline" size="icon">
<FlipHorizontalIcon />
</Button>
<Button variant="outline" size="icon">
<FlipVerticalIcon />
</Button>
<Button variant="outline" size="icon">
<RotateCwIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon">
<TrashIcon />
</Button>
</ButtonGroup>
</ButtonGroup>
<ButtonGroup orientation="vertical">
<Button variant="outline" size="sm">
<IconPlus /> Increase
</Button>
<Button variant="outline" size="sm">
<IconMinus /> Decrease
</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical">
<Button variant="secondary" size="sm">
<IconPlus /> Increase
</Button>
<ButtonGroupSeparator orientation="horizontal" />
<Button variant="secondary" size="sm">
<IconMinus /> Decrease
</Button>
</ButtonGroup>
</div>
</div>
</div>
)
}
function ExportButtonGroup() {
const [exportType, setExportType] = useState("pdf")
return (
<ButtonGroup>
<Input />
<Select value={exportType} onValueChange={setExportType}>
<SelectTrigger>
<SelectValue asChild>
<span>{exportType}</span>
</SelectValue>
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="pdf">pdf</SelectItem>
<SelectItem value="xlsx">xlsx</SelectItem>
<SelectItem value="csv">csv</SelectItem>
<SelectItem value="json">json</SelectItem>
</SelectContent>
</Select>
</ButtonGroup>
)
}

View File

@@ -98,7 +98,10 @@ export function CardDemo() {
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>

View File

@@ -62,7 +62,7 @@ const users = [
},
{
id: "2",
username: "leerob",
username: "maxleiter",
},
{
id: "3",

View File

@@ -274,7 +274,10 @@ function DropdownMenuAvatarOnly() {
className="size-8 rounded-full border-none p-0"
>
<Avatar>
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
<AvatarImage
src="https://github.com/maxleiter.png"
alt="maxleiter"
/>
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
</Avatar>
</Button>
@@ -286,13 +289,16 @@ function DropdownMenuAvatarOnly() {
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar>
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
<AvatarImage
src="https://github.com/maxleiter.png"
alt="maxleiter"
/>
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">leerob</span>
<span className="truncate font-semibold">maxleiter</span>
<span className="text-muted-foreground truncate text-xs">
leerob@example.com
maxleiter@example.com
</span>
</div>
</div>

View File

@@ -0,0 +1,250 @@
import { IconArrowUpRight, IconFolderCode } from "@tabler/icons-react"
import { PlusIcon, SearchIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/registry/new-york-v4/ui/dialog"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
export function EmptyDemo() {
return (
<div className="grid w-full gap-8">
<Empty className="min-h-[80svh]">
<EmptyHeader>
<EmptyMedia variant="icon">
<IconFolderCode />
</EmptyMedia>
<EmptyTitle>No projects yet</EmptyTitle>
<EmptyDescription>
You haven&apos;t created any projects yet. Get started by creating
your first project.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex gap-2">
<Button asChild>
<a href="#">Create project</a>
</Button>
<Button variant="outline">Import project</Button>
</div>
<Button variant="link" asChild className="text-muted-foreground">
<a href="#">
Learn more <IconArrowUpRight />
</a>
</Button>
</EmptyContent>
</Empty>
<Empty className="bg-muted min-h-[80svh]">
<EmptyHeader>
<EmptyTitle>No results found</EmptyTitle>
<EmptyDescription>
No results found for your search. Try adjusting your search terms.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>Try again</Button>
<Button variant="link" asChild className="text-muted-foreground">
<a href="#">
Learn more <IconArrowUpRight />
</a>
</Button>
</EmptyContent>
</Empty>
<Empty className="min-h-[80svh] border">
<EmptyHeader>
<EmptyTitle>404 - Not Found</EmptyTitle>
<EmptyDescription>
The page you&apos;re looking for doesn&apos;t exist. Try searching
for what you need below.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<InputGroup className="w-3/4">
<InputGroupInput placeholder="Try searching for pages..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>/</Kbd>
</InputGroupAddon>
</InputGroup>
<EmptyDescription>
Need help? <a href="#">Contact support</a>
</EmptyDescription>
</EmptyContent>
</Empty>
<Empty className="min-h-[80svh]">
<EmptyHeader>
<EmptyTitle>Nothing to see here</EmptyTitle>
<EmptyDescription>
No posts have been created yet. Get started by{" "}
<a href="#">creating your first post</a> to share with the
community.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline">
<PlusIcon />
New Post
</Button>
</EmptyContent>
</Empty>
<div className="bg-muted flex min-h-[800px] items-center justify-center rounded-lg p-20">
<Card className="max-w-sm">
<CardContent>
<Empty className="p-4">
<EmptyHeader>
<EmptyTitle>404 - Not Found</EmptyTitle>
<EmptyDescription>
The page you&apos;re looking for doesn&apos;t exist. Try
searching for what you need below.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<InputGroup className="w-3/4">
<InputGroupInput placeholder="Try searching for pages..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>/</Kbd>
</InputGroupAddon>
</InputGroup>
<EmptyDescription>
Need help? <a href="#">Contact support</a>
</EmptyDescription>
</EmptyContent>
</Empty>
</CardContent>
</Card>
</div>
<div className="bg-muted flex min-h-[800px] items-center justify-center rounded-lg p-20">
<Card className="max-w-sm">
<CardContent>
<Empty className="p-4">
<EmptyHeader>
<EmptyMedia variant="icon">
<IconFolderCode />
</EmptyMedia>
<EmptyTitle>No projects yet</EmptyTitle>
<EmptyDescription>
You haven&apos;t created any projects yet. Get started by
creating your first project.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex gap-2">
<Button asChild>
<a href="#">Create project</a>
</Button>
<Button variant="outline">Import project</Button>
</div>
<Button
variant="link"
asChild
className="text-muted-foreground"
>
<a href="#">
Learn more <IconArrowUpRight />
</a>
</Button>
</EmptyContent>
</Empty>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-3 gap-6">
<div className="flex gap-4">
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="sr-only">
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog Description</DialogDescription>
</DialogHeader>
<Empty className="p-4">
<EmptyHeader>
<EmptyMedia variant="icon">
<IconFolderCode />
</EmptyMedia>
<EmptyTitle>No projects yet</EmptyTitle>
<EmptyDescription>
You haven&apos;t created any projects yet. Get started by
creating your first project.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex gap-2">
<Button asChild>
<a href="#">Create project</a>
</Button>
<Button variant="outline">Import project</Button>
</div>
<Button
variant="link"
asChild
className="text-muted-foreground"
>
<a href="#">
Learn more <IconArrowUpRight />
</a>
</Button>
</EmptyContent>
</Empty>
</DialogContent>
</Dialog>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open Popover</Button>
</PopoverTrigger>
<PopoverContent className="rounded-2xl p-2">
<Empty className="rounded-sm p-6">
<EmptyHeader>
<EmptyTitle>Nothing to see here</EmptyTitle>
<EmptyDescription>
No posts have been created yet.{" "}
<a href="#">Create your first post</a> to share with the
community.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline">
<PlusIcon />
New Post
</Button>
</EmptyContent>
</Empty>
</PopoverContent>
</Popover>
</div>
</div>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,663 @@
"use client"
import { useState } from "react"
import {
IconBrandJavascript,
IconCheck,
IconChevronDown,
IconCopy,
IconInfoCircle,
IconLoader2,
IconMicrophone,
IconPlayerRecordFilled,
IconPlus,
IconRefresh,
IconSearch,
IconServerSpark,
IconStar,
IconTrash,
} from "@tabler/icons-react"
import {
ArrowRightIcon,
ArrowUpIcon,
ChevronDownIcon,
EyeClosedIcon,
FlipVerticalIcon,
SearchIcon,
} from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
ButtonGroup,
ButtonGroupText,
} from "@/registry/new-york-v4/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function InputGroupDemo() {
const [country, setCountry] = useState("+1")
return (
<div className="flex w-full flex-wrap gap-12 pb-72 *:[div]:w-full *:[div]:max-w-sm">
<div className="flex flex-col gap-10">
<Field>
<FieldLabel htmlFor="input-default-01">
Default (No Input Group)
</FieldLabel>
<Input placeholder="Default" id="input-default-01" />
</Field>
<Field>
<FieldLabel htmlFor="input-group-02">Input Group</FieldLabel>
<InputGroup>
<InputGroupInput id="input-group-02" placeholder="Default" />
</InputGroup>
</Field>
<Field data-disabled="true">
<FieldLabel htmlFor="input-disabled-03">Disabled</FieldLabel>
<InputGroup>
<InputGroupInput
id="input-disabled-03"
placeholder="This field is disabled"
disabled
/>
</InputGroup>
</Field>
<Field data-invalid="true">
<FieldLabel htmlFor="input-invalid-04">Invalid</FieldLabel>
<InputGroup>
<InputGroupInput
id="input-invalid-04"
placeholder="This field is invalid"
aria-invalid="true"
/>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-icon-left-05">Icon (left)</FieldLabel>
<InputGroup>
<InputGroupInput id="input-icon-left-05" />
<InputGroupAddon>
<SearchIcon className="text-muted-foreground" />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput id="input-icon-left-06" />
<InputGroupAddon>
<FlipVerticalIcon className="text-muted-foreground" />
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-icon-right-07">Icon (right)</FieldLabel>
<InputGroup>
<InputGroupInput id="input-icon-right-07" />
<InputGroupAddon align="inline-end">
<EyeClosedIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput id="input-icon-right-08" />
<InputGroupAddon align="inline-end">
<IconLoader2 className="text-muted-foreground animate-spin" />
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-icon-both-09">Icon (both)</FieldLabel>
<InputGroup>
<InputGroupInput id="input-icon-both-09" />
<InputGroupAddon>
<IconMicrophone className="text-muted-foreground" />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<IconPlayerRecordFilled className="animate-pulse text-red-500" />
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-icon-both-10">Multiple Icons</FieldLabel>
<InputGroup>
<InputGroupInput id="input-icon-both-10" />
<InputGroupAddon align="inline-end">
<IconStar />
<InputGroupButton
size="icon-xs"
onClick={() => toast.success("Copied to clipboard")}
>
<IconCopy />
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon>
<IconPlayerRecordFilled className="animate-pulse text-red-500" />
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-description-10">Description</FieldLabel>
<InputGroup>
<InputGroupInput id="input-description-10" />
<InputGroupAddon align="inline-end">
<IconInfoCircle />
</InputGroupAddon>
</InputGroup>
<FieldDescription>
This is a description of the input group.
</FieldDescription>
</Field>
<FieldGroup className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="input-group-11">First Name</FieldLabel>
<InputGroup>
<InputGroupInput id="input-group-11" placeholder="First Name" />
<InputGroupAddon align="inline-end">
<IconInfoCircle />
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-group-12">Last Name</FieldLabel>
<InputGroup>
<InputGroupInput id="input-group-12" placeholder="Last Name" />
<InputGroupAddon align="inline-end">
<IconInfoCircle />
</InputGroupAddon>
</InputGroup>
</Field>
</FieldGroup>
</div>
<div className="flex flex-col gap-10">
<Field>
<FieldLabel htmlFor="input-tooltip-20">Tooltip</FieldLabel>
<InputGroup>
<InputGroupInput id="input-tooltip-20" />
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton className="rounded-full" size="icon-xs">
<IconInfoCircle />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>This is content in a tooltip.</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-dropdown-21">Dropdown</FieldLabel>
<InputGroup>
<InputGroupInput id="input-dropdown-21" />
<InputGroupAddon>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<InputGroupButton className="text-muted-foreground tabular-nums">
{country} <ChevronDownIcon />
</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="min-w-16"
sideOffset={10}
alignOffset={-8}
>
<DropdownMenuItem onClick={() => setCountry("+1")}>
+1
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setCountry("+44")}>
+44
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setCountry("+46")}>
+46
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-label-10">Label</FieldLabel>
<InputGroup>
<InputGroupAddon>
<FieldLabel htmlFor="input-label-10">Label</FieldLabel>
</InputGroupAddon>
<InputGroupInput id="input-label-10" />
</InputGroup>
<InputGroup className="gap-0">
<InputGroupAddon>
<FieldLabel
htmlFor="input-prefix-11"
className="text-muted-foreground"
>
example.com/
</FieldLabel>
</InputGroupAddon>
<InputGroupInput id="input-prefix-11" />
</InputGroup>
<InputGroup>
<InputGroupInput id="input-optional-12" />
<InputGroupAddon align="inline-end">
<InputGroupText>(optional)</InputGroupText>
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-button-13">Button</FieldLabel>
<InputGroup>
<InputGroupInput id="input-button-13" />
<InputGroupAddon>
<InputGroupButton>Button</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput id="input-button-14" />
<InputGroupAddon>
<InputGroupButton variant="outline">Button</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput id="input-button-15" />
<InputGroupAddon>
<InputGroupButton variant="secondary">Button</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput id="input-button-16" />
<InputGroupAddon align="inline-end">
<InputGroupButton variant="secondary">Button</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput id="input-button-17" />
<InputGroupAddon align="inline-end">
<InputGroupButton size="icon-xs">
<IconCopy />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput id="input-button-18" />
<InputGroupAddon align="inline-end">
<InputGroupButton variant="secondary" size="icon-xs">
<IconTrash />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup className="[--radius:9999px]">
<Popover>
<PopoverTrigger asChild>
<InputGroupAddon>
<InputGroupButton variant="secondary" size="icon-xs">
<IconInfoCircle />
</InputGroupButton>
</InputGroupAddon>
</PopoverTrigger>
<PopoverContent
align="start"
className="flex flex-col gap-1 rounded-xl text-sm"
>
<p className="font-medium">Your connection is not secure.</p>
<p>
You should not enter any sensitive information on this site.
</p>
</PopoverContent>
</Popover>
<InputGroupAddon className="text-muted-foreground">
https://
</InputGroupAddon>
<InputGroupInput id="input-secure-19" />
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
onClick={() => toast.success("Added to favorites")}
>
<IconStar />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-addon-20">Addon (block-start)</FieldLabel>
<InputGroup className="h-auto">
<InputGroupInput id="input-addon-20" />
<InputGroupAddon align="block-start">
<InputGroupText>First Name</InputGroupText>
<IconInfoCircle className="text-muted-foreground ml-auto" />
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-addon-21">Addon (block-end)</FieldLabel>
<InputGroup className="h-auto">
<InputGroupInput id="input-addon-21" />
<InputGroupAddon align="block-end">
<InputGroupText>20/240 characters</InputGroupText>
<IconInfoCircle className="text-muted-foreground ml-auto" />
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="textarea-comment-33">Default Button</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="textarea-comment-33"
placeholder="Share your thoughts..."
className="py-2.5"
/>
<InputGroupAddon align="block-end">
<ButtonGroup>
<Button variant="outline" size="sm">
Button
</Button>
<Button variant="outline" size="icon" className="size-8">
<IconChevronDown />
</Button>
</ButtonGroup>
<Button variant="ghost" className="ml-auto" size="sm">
Cancel
</Button>
<Button variant="default" size="sm">
Post <ArrowRightIcon />
</Button>
</InputGroupAddon>
</InputGroup>
</Field>
</div>
<div className="flex flex-col gap-10">
<Field>
<FieldLabel htmlFor="input-kbd-22">Input Group with Kbd</FieldLabel>
<InputGroup>
<InputGroupInput id="input-kbd-22" />
<InputGroupAddon>
<Kbd>K</Kbd>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput id="input-kbd-23" />
<InputGroupAddon align="inline-end">
<Kbd>K</Kbd>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
id="input-search-apps-24"
placeholder="Search for Apps..."
/>
<InputGroupAddon align="inline-end">Ask AI</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>Tab</Kbd>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput
id="input-search-type-25"
placeholder="Type to search..."
/>
<InputGroupAddon align="inline-start">
<IconServerSpark />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>C</Kbd>
</KbdGroup>
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="input-username-26">Username</FieldLabel>
<InputGroup>
<InputGroupInput id="input-username-26" defaultValue="shadcn" />
<InputGroupAddon align="inline-end">
<div className="flex size-4 items-center justify-center rounded-full bg-green-500 dark:bg-green-800">
<IconCheck className="size-3 text-white" />
</div>
</InputGroupAddon>
</InputGroup>
<FieldDescription className="text-green-700">
This username is available.
</FieldDescription>
</Field>
<InputGroup>
<InputGroupInput
id="input-search-docs-27"
placeholder="Search documentation..."
/>
<InputGroupAddon>
<IconSearch />
</InputGroupAddon>
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
</InputGroup>
<InputGroup data-disabled="true">
<InputGroupInput
id="input-search-disabled-28"
placeholder="Search documentation..."
disabled
/>
<InputGroupAddon>
<IconSearch />
</InputGroupAddon>
<InputGroupAddon align="inline-end">Disabled</InputGroupAddon>
</InputGroup>
<Field>
<FieldLabel htmlFor="url">With Button Group</FieldLabel>
<ButtonGroup>
<ButtonGroupText>https://</ButtonGroupText>
<InputGroup>
<InputGroupInput id="url" />
<InputGroupAddon align="inline-end">
<IconInfoCircle />
</InputGroupAddon>
</InputGroup>
<ButtonGroupText>.com</ButtonGroupText>
</ButtonGroup>
<FieldDescription>
This is a description of the input group.
</FieldDescription>
</Field>
<Field data-disabled="true">
<FieldLabel htmlFor="input-group-29">Loading</FieldLabel>
<FieldDescription>
This is a description of the input group.
</FieldDescription>
<InputGroup>
<InputGroupInput
id="input-group-29"
disabled
defaultValue="shadcn"
/>
<InputGroupAddon align="inline-end">
<Spinner />
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="textarea-code-32">Code Editor</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="textarea-code-32"
placeholder="console.log('Hello, world!');"
className="min-h-[300px] py-3"
/>
<InputGroupAddon align="block-start" className="border-b">
<InputGroupText className="font-mono font-medium">
<IconBrandJavascript />
script.js
</InputGroupText>
<InputGroupButton size="icon-xs" className="ml-auto">
<IconRefresh />
</InputGroupButton>
<InputGroupButton size="icon-xs" variant="ghost">
<IconCopy />
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon align="block-end" className="border-t">
<InputGroupText>Line 1, Column 1</InputGroupText>
<InputGroupText className="ml-auto">JavaScript</InputGroupText>
</InputGroupAddon>
</InputGroup>
</Field>
</div>
<div className="flex flex-col gap-10">
<Field>
<FieldLabel htmlFor="textarea-header-footer-12">Default</FieldLabel>
<Textarea
id="textarea-header-footer-12"
placeholder="Enter your text here..."
/>
</Field>
<Field>
<FieldLabel htmlFor="textarea-header-footer-13">
Input Group
</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="textarea-header-footer-13"
placeholder="Enter your text here..."
/>
</InputGroup>
<FieldDescription>
This is a description of the input group.
</FieldDescription>
</Field>
<Field data-invalid="true">
<FieldLabel htmlFor="textarea-header-footer-14">Invalid</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="textarea-header-footer-14"
placeholder="Enter your text here..."
aria-invalid="true"
/>
</InputGroup>
<FieldDescription>
This is a description of the input group.
</FieldDescription>
</Field>
<Field data-disabled="true">
<FieldLabel htmlFor="textarea-header-footer-15">Disabled</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="textarea-header-footer-15"
placeholder="Enter your text here..."
disabled
/>
</InputGroup>
<FieldDescription>
This is a description of the input group.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="textarea-header-footer-30">Textarea</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="textarea-header-footer-30"
placeholder="Enter your text here..."
/>
<InputGroupAddon align="block-end">
<InputGroupText>0/280 characters</InputGroupText>
<InputGroupButton
variant="default"
size="icon-xs"
className="ml-auto rounded-full"
>
<ArrowUpIcon />
<span className="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="prompt-31">Enter your prompt</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="prompt-31"
placeholder="Ask, Search or Chat..."
/>
<InputGroupAddon align="block-end">
<InputGroupButton
variant="outline"
className="rounded-full"
size="icon-xs"
>
<IconPlus />
</InputGroupButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<InputGroupButton variant="ghost">Auto</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText className="ml-auto">
12 messages left
</InputGroupText>
<InputGroupButton
variant="default"
className="rounded-full"
size="icon-xs"
>
<ArrowUpIcon />
<span className="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
This is a description of the input group.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="textarea-comment-31">Comment Box</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="textarea-comment-31"
placeholder="Share your thoughts..."
/>
<InputGroupAddon align="block-end">
<InputGroupButton variant="ghost" className="ml-auto" size="sm">
Cancel
</InputGroupButton>
<InputGroupButton variant="default" size="sm">
Post Comment
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</div>
</div>
)
}

View File

@@ -0,0 +1,392 @@
import * as React from "react"
import Image from "next/image"
import { IconChevronRight, IconDownload } from "@tabler/icons-react"
import { PlusIcon, TicketIcon } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemFooter,
ItemGroup,
ItemHeader,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
import { Progress } from "@/registry/new-york-v4/ui/progress"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
const people = [
{
username: "shadcn",
avatar: "https://github.com/shadcn.png",
message: "Just shipped a component that fixes itself",
},
{
username: "pranathip",
avatar: "https://github.com/pranathip.png",
message: "My code is so clean, it does its own laundry",
},
{
username: "evilrabbit",
avatar: "https://github.com/evilrabbit.png",
message:
"Debugging is like being a detective in a crime movie where you're also the murderer",
},
{
username: "maxleiter",
avatar: "https://github.com/maxleiter.png",
message:
"I don't always test my code, but when I do, I test it in production",
},
]
const music = [
{
title: "Midnight City Lights",
artist: "Neon Dreams",
album: "Electric Nights",
duration: "3:45",
},
{
title: "Coffee Shop Conversations",
artist: "The Morning Brew",
album: "Urban Stories",
duration: "4:05",
},
{
title: "Digital Rain",
artist: "Cyber Symphony",
album: "Binary Beats",
duration: "3:30",
},
{
title: "Sunset Boulevard",
artist: "Golden Hour",
album: "California Dreams",
duration: "3:55",
},
{
title: "Neon Sign Romance",
artist: "Retro Wave",
album: "80s Forever",
duration: "4:10",
},
{
title: "Ocean Depths",
artist: "Deep Blue",
album: "Underwater Symphony",
duration: "3:40",
},
{
title: "Space Station Alpha",
artist: "Cosmic Explorers",
album: "Galactic Journey",
duration: "3:50",
},
{
title: "Forest Whispers",
artist: "Nature's Choir",
album: "Woodland Tales",
duration: "3:35",
},
]
const issues = [
{
number: 1247,
date: "March 15, 2024",
title:
"Button component doesn't respect disabled state when using custom variants",
description:
"When applying custom variants to the Button component, the disabled prop is ignored and the button remains clickable. This affects accessibility and user experience.",
},
{
number: 892,
date: "February 8, 2024",
title: "Dialog component causes scroll lock on mobile devices",
description:
"The Dialog component prevents scrolling on the background content but doesn't restore scroll position properly on mobile Safari and Chrome, causing layout shifts.",
},
{
number: 1156,
date: "January 22, 2024",
title: "TypeScript errors with Select component in strict mode",
description:
"Using the Select component with TypeScript strict mode enabled throws type errors related to generic constraints and value prop typing.",
},
{
number: 734,
date: "December 3, 2023",
title: "Dark mode toggle causes flash of unstyled content",
description:
"When switching between light and dark themes, there's a brief moment where components render with incorrect styling before the theme transition completes.",
},
{
number: 1389,
date: "April 2, 2024",
title: "Form validation messages overlap with floating labels",
description:
"Error messages in Form components with floating labels appear underneath the label text, making them difficult to read. Need better positioning logic for validation feedback.",
},
]
export function ItemDemo() {
return (
<div className="@container w-full">
<div className="flex flex-wrap gap-6 2xl:gap-12">
<div className="flex max-w-sm flex-col gap-6">
<Item>
<ItemContent>
<ItemTitle>Item Title</ItemTitle>
</ItemContent>
<ItemActions>
<Button variant="outline">Button</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemContent>
<ItemTitle>Item Title</ItemTitle>
</ItemContent>
<ItemActions>
<Button variant="outline">Button</Button>
</ItemActions>
</Item>
<Item>
<ItemContent>
<ItemTitle>Item Title</ItemTitle>
<ItemDescription>Item Description</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline">Button</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemContent>
<ItemTitle>Item Title</ItemTitle>
<ItemDescription>Item Description</ItemDescription>
</ItemContent>
</Item>
<Item variant="muted">
<ItemContent>
<ItemTitle>Item Title</ItemTitle>
<ItemDescription>Item Description</ItemDescription>
</ItemContent>
</Item>
<Item variant="muted">
<ItemContent>
<ItemTitle>Item Title</ItemTitle>
<ItemDescription>Item Description</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline">Button</Button>
<Button variant="outline">Button</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemMedia variant="icon">
<TicketIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Item Title</ItemTitle>
</ItemContent>
<ItemActions>
<Button size="sm">Purchase</Button>
</ItemActions>
</Item>
<Item variant="muted">
<ItemMedia variant="icon">
<TicketIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Item Title</ItemTitle>
<ItemDescription>Item Description</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm">Upgrade</Button>
</ItemActions>
</Item>
<FieldLabel>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Field Title</FieldTitle>
<FieldDescription>Field Description</FieldDescription>
</FieldContent>
<Button variant="outline">Button</Button>
</Field>
</FieldLabel>
</div>
<div className="flex max-w-sm flex-col gap-6">
<ItemGroup>
{people.map((person, index) => (
<React.Fragment key={person.username}>
<Item>
<ItemMedia>
<Avatar>
<AvatarImage src={person.avatar} />
<AvatarFallback>
{person.username.charAt(0)}
</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>{person.username}</ItemTitle>
<ItemDescription>{person.message}</ItemDescription>
</ItemContent>
<ItemActions>
<Button
variant="outline"
size="icon"
className="size-8 rounded-full"
>
<PlusIcon />
</Button>
</ItemActions>
</Item>
{index !== people.length - 1 && <ItemSeparator />}
</React.Fragment>
))}
</ItemGroup>
<Item variant="outline">
<ItemMedia>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<Avatar>
<AvatarImage
src="https://github.com/shadcn.png"
alt="@shadcn"
/>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
</ItemMedia>
<ItemContent>
<ItemTitle>Design Department</ItemTitle>
<ItemDescription>
Meet our team of designers, engineers, and researchers.
</ItemDescription>
</ItemContent>
<ItemActions className="self-start">
<Button
variant="outline"
size="icon"
className="size-8 rounded-full"
>
<IconChevronRight />
</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemHeader>Your download has started.</ItemHeader>
<ItemMedia variant="icon">
<Spinner />
</ItemMedia>
<ItemContent>
<ItemTitle>Downloading...</ItemTitle>
<ItemDescription>129 MB / 1000 MB</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm">
Cancel
</Button>
</ItemActions>
<ItemFooter>
<Progress value={50} />
</ItemFooter>
</Item>
</div>
<div className="flex max-w-lg flex-col gap-6">
<ItemGroup className="gap-4">
{music.map((song) => (
<Item key={song.title} variant="outline" asChild role="listitem">
<a href="#">
<ItemMedia variant="image">
<Image
src={`https://avatar.vercel.sh/${song.title}`}
alt={song.title}
width={32}
height={32}
className="grayscale"
/>
</ItemMedia>
<ItemContent>
<ItemTitle className="line-clamp-1">
{song.title} -{" "}
<span className="text-muted-foreground">
{song.album}
</span>
</ItemTitle>
<ItemDescription>{song.artist}</ItemDescription>
</ItemContent>
<ItemContent className="flex-none text-center">
<ItemDescription>{song.duration}</ItemDescription>
</ItemContent>
<ItemActions>
<Button
variant="ghost"
size="icon"
className="size-8 rounded-full"
aria-label="Download"
>
<IconDownload />
</Button>
</ItemActions>
</a>
</Item>
))}
</ItemGroup>
</div>
<div className="flex max-w-lg flex-col gap-6">
<ItemGroup>
{issues.map((issue) => (
<React.Fragment key={issue.number}>
<Item asChild className="rounded-none">
<a href="#">
<ItemContent>
<ItemTitle className="line-clamp-1">
{issue.title}
</ItemTitle>
<ItemDescription>{issue.description}</ItemDescription>
</ItemContent>
<ItemContent className="self-start">
#{issue.number}
</ItemContent>
</a>
</Item>
<ItemSeparator />
</React.Fragment>
))}
</ItemGroup>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,103 @@
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"
import { CommandIcon, WavesIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function KbdDemo() {
return (
<div className="flex max-w-xs flex-col items-start gap-4">
<div className="flex items-center gap-2">
<Kbd>Ctrl</Kbd>
<Kbd>K</Kbd>
<Kbd>Ctrl + B</Kbd>
</div>
<div className="flex items-center gap-2">
<Kbd></Kbd>
<Kbd>C</Kbd>
</div>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Shift</Kbd>
<Kbd>P</Kbd>
</KbdGroup>
<div className="flex items-center gap-2">
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
<Kbd></Kbd>
</div>
<KbdGroup>
<Kbd>
<CommandIcon />
</Kbd>
<Kbd>
<IconArrowLeft />
</Kbd>
<Kbd>
<IconArrowRight />
</Kbd>
</KbdGroup>
<KbdGroup>
<Kbd>
<IconArrowLeft />
Left
</Kbd>
<Kbd>
<WavesIcon />
Voice Enabled
</Kbd>
</KbdGroup>
<InputGroup>
<InputGroupInput />
<InputGroupAddon>
<Kbd>Space</Kbd>
</InputGroupAddon>
</InputGroup>
<ButtonGroup>
<Tooltip>
<TooltipTrigger asChild>
<Button size="sm" variant="outline">
Save
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="flex items-center gap-2">
Save Changes <Kbd>S</Kbd>
</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button size="sm" variant="outline">
Print
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="flex items-center gap-2">
Print Document{" "}
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>P</Kbd>
</KbdGroup>
</div>
</TooltipContent>
</Tooltip>
</ButtonGroup>
<Kbd>
<samp>File</samp>
</Kbd>
</div>
)
}

View File

@@ -0,0 +1,106 @@
import { ArrowRightIcon } from "lucide-react"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
export function SpinnerDemo() {
return (
<div className="flex w-full flex-col gap-12">
<div className="flex items-center gap-6">
<Spinner />
<Spinner className="size-8" />
</div>
<div className="flex items-center gap-6">
<Button>
<Spinner /> Submit
</Button>
<Button disabled>
<Spinner /> Disabled
</Button>
<Button size="sm">
<Spinner /> Small
</Button>
<Button variant="outline" disabled>
<Spinner /> Outline
</Button>
<Button variant="outline" size="icon" disabled>
<Spinner />
<span className="sr-only">Loading...</span>
</Button>
<Button variant="destructive" disabled>
<Spinner />
Remove
</Button>
</div>
<div className="flex items-center gap-6">
<Badge>
<Spinner />
Badge
</Badge>
<Badge variant="secondary">
<Spinner />
Badge
</Badge>
<Badge variant="destructive">
<Spinner />
Badge
</Badge>
<Badge variant="outline">
<Spinner />
Badge
</Badge>
</div>
<div className="flex max-w-xs items-center gap-6">
<Field>
<FieldLabel htmlFor="input-group-spinner">Input Group</FieldLabel>
<InputGroup>
<InputGroupInput id="input-group-spinner" />
<InputGroupAddon>
<Spinner />
</InputGroupAddon>
</InputGroup>
</Field>
</div>
<Empty className="min-h-[80svh]">
<EmptyHeader>
<EmptyMedia variant="icon">
<Spinner />
</EmptyMedia>
<EmptyTitle>No projects yet</EmptyTitle>
<EmptyDescription>
You haven&apos;t created any projects yet. Get started by creating
your first project.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex gap-2">
<Button asChild>
<a href="#">Create project</a>
</Button>
<Button variant="outline">Import project</Button>
</div>
<Button variant="link" asChild className="text-muted-foreground">
<a href="#">
Learn more <ArrowRightIcon />
</a>
</Button>
</EmptyContent>
</Empty>
</div>
)
}

View File

@@ -0,0 +1,147 @@
"use client"
import { cn } from "@/lib/utils"
import { useThemeConfig } from "@/components/active-theme"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
const THEMES = {
sizes: [
{
name: "Default",
value: "default",
},
{
name: "Scaled",
value: "scaled",
},
{
name: "Mono",
value: "mono",
},
],
colors: [
{
name: "Blue",
value: "blue",
},
{
name: "Green",
value: "green",
},
{
name: "Amber",
value: "amber",
},
{
name: "Rose",
value: "rose",
},
{
name: "Purple",
value: "purple",
},
{
name: "Orange",
value: "orange",
},
{
name: "Teal",
value: "teal",
},
],
fonts: [
{
name: "Inter",
value: "inter",
},
{
name: "Noto Sans",
value: "noto-sans",
},
{
name: "Nunito Sans",
value: "nunito-sans",
},
{
name: "Figtree",
value: "figtree",
},
],
radius: [
{
name: "None",
value: "rounded-none",
},
{
name: "Small",
value: "rounded-small",
},
{
name: "Medium",
value: "rounded-medium",
},
{
name: "Large",
value: "rounded-large",
},
{
name: "Full",
value: "rounded-full",
},
],
}
export function ThemeSelector({ className }: React.ComponentProps<"div">) {
const { activeTheme, setActiveTheme } = useThemeConfig()
return (
<div className={cn("flex items-center gap-2", className)}>
<Label htmlFor="theme-selector" className="sr-only">
Theme
</Label>
<Select value={activeTheme} onValueChange={setActiveTheme}>
<SelectTrigger
id="theme-selector"
size="sm"
className="bg-secondary text-secondary-foreground border-secondary justify-start shadow-none *:data-[slot=select-value]:w-16"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
{Object.entries(THEMES).map(
([key, themes], index) =>
themes.length > 0 && (
<div key={key}>
{index > 0 && <SelectSeparator />}
<SelectGroup>
<SelectLabel>
{key.charAt(0).toUpperCase() + key.slice(1)}
</SelectLabel>
{themes.map((theme) => (
<SelectItem
key={theme.name}
value={theme.value}
className="data-[state=checked]:opacity-50"
>
{theme.name}
</SelectItem>
))}
</SelectGroup>
</div>
)
)}
</SelectContent>
</Select>
</div>
)
}

View File

@@ -0,0 +1,66 @@
import { Figtree, Inter, Noto_Sans, Nunito_Sans } from "next/font/google"
import { cn } from "@/lib/utils"
import { ModeSwitcher } from "@/components/mode-switcher"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/registry/new-york-v4/ui/sidebar"
import { AppBreadcrumbs } from "@/app/(internal)/sink/components/app-breadcrumbs"
import { AppSidebar } from "@/app/(internal)/sink/components/app-sidebar"
import { ThemeSelector } from "@/app/(internal)/sink/components/theme-selector"
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
})
const notoSans = Noto_Sans({
subsets: ["latin"],
variable: "--font-noto-sans",
})
const nunitoSans = Nunito_Sans({
subsets: ["latin"],
variable: "--font-nunito-sans",
})
const figtree = Figtree({
subsets: ["latin"],
variable: "--font-figtree",
})
export default async function SinkLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<SidebarProvider
defaultOpen={true}
className={cn(
"theme-container",
inter.variable,
notoSans.variable,
nunitoSans.variable,
figtree.variable
)}
>
<AppSidebar />
<SidebarInset>
<header className="bg-background sticky top-0 z-10 flex h-14 items-center border-b p-4">
<SidebarTrigger />
<Separator orientation="vertical" className="mr-4 ml-2 !h-4" />
<AppBreadcrumbs />
<div className="ml-auto flex items-center gap-2">
<ModeSwitcher />
<ThemeSelector />
</div>
</header>
{children}
</SidebarInset>
</SidebarProvider>
)
}

View File

@@ -1,62 +1,7 @@
import { Metadata } from "next"
import { cookies } from "next/headers"
import { ThemeSelector } from "@/components/theme-selector"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/registry/new-york-v4/ui/sidebar"
import { AccordionDemo } from "@/app/(internal)/sink/components/accordion-demo"
import { AlertDemo } from "@/app/(internal)/sink/components/alert-demo"
import { AlertDialogDemo } from "@/app/(internal)/sink/components/alert-dialog-demo"
import { AppSidebar } from "@/app/(internal)/sink/components/app-sidebar"
import { AspectRatioDemo } from "@/app/(internal)/sink/components/aspect-ratio-demo"
import { AvatarDemo } from "@/app/(internal)/sink/components/avatar-demo"
import { BadgeDemo } from "@/app/(internal)/sink/components/badge-demo"
import { BreadcrumbDemo } from "@/app/(internal)/sink/components/breadcrumb-demo"
import { ButtonDemo } from "@/app/(internal)/sink/components/button-demo"
import { CalendarDemo } from "@/app/(internal)/sink/components/calendar-demo"
import { CardDemo } from "@/app/(internal)/sink/components/card-demo"
import { CarouselDemo } from "@/app/(internal)/sink/components/carousel-demo"
import { ChartDemo } from "@/app/(internal)/sink/components/chart-demo"
import { CheckboxDemo } from "@/app/(internal)/sink/components/checkbox-demo"
import { CollapsibleDemo } from "@/app/(internal)/sink/components/collapsible-demo"
import { ComboboxDemo } from "@/app/(internal)/sink/components/combobox-demo"
import { CommandDemo } from "@/app/(internal)/sink/components/command-demo"
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
import { ComponentWrapper } from "@/app/(internal)/sink/components/component-wrapper"
import { ContextMenuDemo } from "@/app/(internal)/sink/components/context-menu-demo"
import { DatePickerDemo } from "@/app/(internal)/sink/components/date-picker-demo"
import { DialogDemo } from "@/app/(internal)/sink/components/dialog-demo"
import { DrawerDemo } from "@/app/(internal)/sink/components/drawer-demo"
import { DropdownMenuDemo } from "@/app/(internal)/sink/components/dropdown-menu-demo"
import { FormDemo } from "@/app/(internal)/sink/components/form-demo"
import { HoverCardDemo } from "@/app/(internal)/sink/components/hover-card-demo"
import { InputDemo } from "@/app/(internal)/sink/components/input-demo"
import { InputOTPDemo } from "@/app/(internal)/sink/components/input-otp-demo"
import { LabelDemo } from "@/app/(internal)/sink/components/label-demo"
import { MenubarDemo } from "@/app/(internal)/sink/components/menubar-demo"
import { NavigationMenuDemo } from "@/app/(internal)/sink/components/navigation-menu-demo"
import { PaginationDemo } from "@/app/(internal)/sink/components/pagination-demo"
import { PopoverDemo } from "@/app/(internal)/sink/components/popover-demo"
import { ProgressDemo } from "@/app/(internal)/sink/components/progress-demo"
import { RadioGroupDemo } from "@/app/(internal)/sink/components/radio-group-demo"
import { ResizableDemo } from "@/app/(internal)/sink/components/resizable-demo"
import { ScrollAreaDemo } from "@/app/(internal)/sink/components/scroll-area-demo"
import { SelectDemo } from "@/app/(internal)/sink/components/select-demo"
import { SeparatorDemo } from "@/app/(internal)/sink/components/separator-demo"
import { SheetDemo } from "@/app/(internal)/sink/components/sheet-demo"
import { SkeletonDemo } from "@/app/(internal)/sink/components/skeleton-demo"
import { SliderDemo } from "@/app/(internal)/sink/components/slider-demo"
import { SonnerDemo } from "@/app/(internal)/sink/components/sonner-demo"
import { SwitchDemo } from "@/app/(internal)/sink/components/switch-demo"
import { TableDemo } from "@/app/(internal)/sink/components/table-demo"
import { TabsDemo } from "@/app/(internal)/sink/components/tabs-demo"
import { TextareaDemo } from "@/app/(internal)/sink/components/textarea-demo"
import { ToggleDemo } from "@/app/(internal)/sink/components/toggle-demo"
import { ToggleGroupDemo } from "@/app/(internal)/sink/components/toggle-group-demo"
import { TooltipDemo } from "@/app/(internal)/sink/components/tooltip-demo"
export const dynamic = "force-static"
export const revalidate = false
@@ -66,164 +11,25 @@ export const metadata: Metadata = {
description: "A page with all components for testing purposes.",
}
export default async function SinkPage() {
const cookieStore = await cookies()
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
export default function SinkPage() {
return (
<SidebarProvider defaultOpen={defaultOpen} className="theme-container">
<AppSidebar />
<SidebarInset>
<header className="bg-background sticky top-0 z-10 flex h-14 items-center border-b p-4">
<SidebarTrigger />
<Separator orientation="vertical" className="mr-4 ml-2 !h-4" />
<h1 className="text-base font-medium">Kitchen Sink</h1>
<ThemeSelector className="ml-auto" />
</header>
<div className="@container grid flex-1 gap-4 p-4">
<ComponentWrapper name="accordion">
<AccordionDemo />
</ComponentWrapper>
<ComponentWrapper name="alert">
<AlertDemo />
</ComponentWrapper>
<ComponentWrapper name="alert-dialog">
<AlertDialogDemo />
</ComponentWrapper>
<ComponentWrapper name="aspect-ratio">
<AspectRatioDemo />
</ComponentWrapper>
<ComponentWrapper name="avatar">
<AvatarDemo />
</ComponentWrapper>
<ComponentWrapper name="badge">
<BadgeDemo />
</ComponentWrapper>
<ComponentWrapper name="breadcrumb">
<BreadcrumbDemo />
</ComponentWrapper>
<ComponentWrapper name="button">
<ButtonDemo />
</ComponentWrapper>
<ComponentWrapper name="calendar">
<CalendarDemo />
</ComponentWrapper>
<ComponentWrapper name="card">
<CardDemo />
</ComponentWrapper>
<ComponentWrapper name="carousel">
<CarouselDemo />
</ComponentWrapper>
<ComponentWrapper name="chart" className="w-full">
<ChartDemo />
</ComponentWrapper>
<ComponentWrapper name="checkbox">
<CheckboxDemo />
</ComponentWrapper>
<ComponentWrapper name="collapsible">
<CollapsibleDemo />
</ComponentWrapper>
<ComponentWrapper name="combobox">
<ComboboxDemo />
</ComponentWrapper>
<ComponentWrapper name="command">
<CommandDemo />
</ComponentWrapper>
<ComponentWrapper name="context-menu">
<ContextMenuDemo />
</ComponentWrapper>
<ComponentWrapper name="date-picker">
<DatePickerDemo />
</ComponentWrapper>
<ComponentWrapper name="dialog">
<DialogDemo />
</ComponentWrapper>
<ComponentWrapper name="drawer">
<DrawerDemo />
</ComponentWrapper>
<ComponentWrapper name="dropdown-menu">
<DropdownMenuDemo />
</ComponentWrapper>
<ComponentWrapper name="form">
<FormDemo />
</ComponentWrapper>
<ComponentWrapper name="hover-card">
<HoverCardDemo />
</ComponentWrapper>
<ComponentWrapper name="input">
<InputDemo />
</ComponentWrapper>
<ComponentWrapper name="input-otp">
<InputOTPDemo />
</ComponentWrapper>
<ComponentWrapper name="label">
<LabelDemo />
</ComponentWrapper>
<ComponentWrapper name="menubar">
<MenubarDemo />
</ComponentWrapper>
<ComponentWrapper name="navigation-menu">
<NavigationMenuDemo />
</ComponentWrapper>
<ComponentWrapper name="pagination">
<PaginationDemo />
</ComponentWrapper>
<ComponentWrapper name="popover">
<PopoverDemo />
</ComponentWrapper>
<ComponentWrapper name="progress">
<ProgressDemo />
</ComponentWrapper>
<ComponentWrapper name="radio-group">
<RadioGroupDemo />
</ComponentWrapper>
<ComponentWrapper name="resizable">
<ResizableDemo />
</ComponentWrapper>
<ComponentWrapper name="scroll-area">
<ScrollAreaDemo />
</ComponentWrapper>
<ComponentWrapper name="select">
<SelectDemo />
</ComponentWrapper>
<ComponentWrapper name="separator">
<SeparatorDemo />
</ComponentWrapper>
<ComponentWrapper name="sheet">
<SheetDemo />
</ComponentWrapper>
<ComponentWrapper name="skeleton">
<SkeletonDemo />
</ComponentWrapper>
<ComponentWrapper name="slider">
<SliderDemo />
</ComponentWrapper>
<ComponentWrapper name="sonner">
<SonnerDemo />
</ComponentWrapper>
<ComponentWrapper name="switch">
<SwitchDemo />
</ComponentWrapper>
<ComponentWrapper name="table">
<TableDemo />
</ComponentWrapper>
<ComponentWrapper name="tabs">
<TabsDemo />
</ComponentWrapper>
<ComponentWrapper name="textarea">
<TextareaDemo />
</ComponentWrapper>
<ComponentWrapper name="toggle">
<ToggleDemo />
</ComponentWrapper>
<ComponentWrapper name="toggle-group">
<ToggleGroupDemo />
</ComponentWrapper>
<ComponentWrapper name="tooltip">
<TooltipDemo />
</ComponentWrapper>
</div>
</SidebarInset>
</SidebarProvider>
<div className="@container grid flex-1 gap-4 p-4">
{Object.entries(componentRegistry)
.filter(([, component]) => {
return component.type === "registry:ui"
})
.map(([key, component]) => {
const Component = component.component
return (
<ComponentWrapper
key={key}
name={key}
className={component.className || ""}
>
<Component />
</ComponentWrapper>
)
})}
</div>
)
}

View File

@@ -84,13 +84,13 @@ export default function RootLayout({
</head>
<body
className={cn(
"text-foreground group/body overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
"text-foreground group/body theme-blue overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
fontVariables
)}
>
<ThemeProvider>
<LayoutProvider>
<ActiveThemeProvider>
<ActiveThemeProvider initialTheme="blue">
{children}
<TailwindIndicator />
<Toaster position="top-center" />

View File

@@ -8,7 +8,7 @@ import {
useState,
} from "react"
const DEFAULT_THEME = "default"
const DEFAULT_THEME = "blue"
type ThemeContextType = {
activeTheme: string

View File

@@ -6,8 +6,9 @@ import { Badge } from "@/registry/new-york-v4/ui/badge"
export function Announcement() {
return (
<Badge asChild variant="secondary" className="rounded-full">
<Link href="/docs/components/calendar">
New Calendar Component <ArrowRightIcon />
<Link href="/docs/changelog">
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
New Components: Field, Input Group, Item and more <ArrowRightIcon />
</Link>
</Badge>
)

View File

@@ -10,12 +10,17 @@ export function Callout({
children,
icon,
className,
variant = "default",
...props
}: React.ComponentProps<typeof Alert> & { icon?: React.ReactNode }) {
}: React.ComponentProps<typeof Alert> & {
icon?: React.ReactNode
variant?: "default" | "info" | "warning"
}) {
return (
<Alert
data-variant={variant}
className={cn(
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-1",
"bg-background text-foreground mt-6 w-auto border md:-mx-1",
className
)}
{...props}

View File

@@ -32,7 +32,12 @@ import {
DialogHeader,
DialogTitle,
} from "@/registry/new-york-v4/ui/dialog"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import {
Tooltip,
TooltipContent,
@@ -159,23 +164,25 @@ export function CardsChat() {
}}
className="relative w-full"
>
<Input
id="message"
placeholder="Type your message..."
className="flex-1 pr-10"
autoComplete="off"
value={input}
onChange={(event) => setInput(event.target.value)}
/>
<Button
type="submit"
size="icon"
className="absolute top-1/2 right-2 size-6 -translate-y-1/2 rounded-full"
disabled={inputLength === 0}
>
<ArrowUpIcon className="size-3.5" />
<span className="sr-only">Send</span>
</Button>
<InputGroup>
<InputGroupInput
id="message"
placeholder="Type your message..."
autoComplete="off"
value={input}
onChange={(event) => setInput(event.target.value)}
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
type="submit"
size="icon-xs"
className="rounded-full"
>
<ArrowUpIcon />
<span className="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</form>
</CardFooter>
</Card>

View File

@@ -5,11 +5,15 @@ import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
} from "@/registry/new-york-v4/ui/field"
import { Switch } from "@/registry/new-york-v4/ui/switch"
export function CardsCookieSettings() {
@@ -20,32 +24,20 @@ export function CardsCookieSettings() {
<CardDescription>Manage your cookie settings here.</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<div className="flex items-center justify-between gap-4">
<Label htmlFor="necessary" className="flex flex-col items-start">
<span>Strictly Necessary</span>
<span className="text-muted-foreground leading-snug font-normal">
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="necessary">Strictly Necessary</FieldLabel>
<FieldDescription>
These cookies are essential in order to use the website and use
its features.
</span>
</Label>
</FieldDescription>
</FieldContent>
<Switch id="necessary" defaultChecked aria-label="Necessary" />
</div>
<div className="flex items-center justify-between gap-4">
<Label htmlFor="functional" className="flex flex-col items-start">
<span>Functional Cookies</span>
<span className="text-muted-foreground leading-snug font-normal">
These cookies allow the website to provide personalized
functionality.
</span>
</Label>
<Switch id="functional" aria-label="Functional" />
</div>
</Field>
<Field>
<Button variant="outline">Save preferences</Button>
</Field>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full">
Save preferences
</Button>
</CardFooter>
</Card>
)
}

View File

@@ -5,12 +5,16 @@ import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Field,
FieldGroup,
FieldLabel,
FieldSeparator,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import { Label } from "@/registry/new-york-v4/ui/label"
export function CardsCreateAccount() {
return (
@@ -21,53 +25,48 @@ export function CardsCreateAccount() {
Enter your email below to create your account
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="grid grid-cols-2 gap-6">
<Button variant="outline">
<svg viewBox="0 0 438.549 438.549">
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
GitHub
</Button>
<Button variant="outline">
<svg role="img" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
/>
</svg>
Google
</Button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-card text-muted-foreground px-2">
Or continue with
</span>
</div>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="email-create-account">Email</Label>
<Input
id="email-create-account"
type="email"
placeholder="m@example.com"
/>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="password-create-account">Password</Label>
<Input id="password-create-account" type="password" />
</div>
<CardContent>
<FieldGroup>
<Field className="grid grid-cols-2 gap-6">
<Button variant="outline">
<svg viewBox="0 0 438.549 438.549">
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
GitHub
</Button>
<Button variant="outline">
<svg role="img" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
/>
</svg>
Google
</Button>
</Field>
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
Or continue with
</FieldSeparator>
<Field>
<FieldLabel htmlFor="email-create-account">Email</FieldLabel>
<Input
id="email-create-account"
type="email"
placeholder="m@example.com"
/>
</Field>
<Field>
<FieldLabel htmlFor="password-create-account">Password</FieldLabel>
<Input id="password-create-account" type="password" />
</Field>
<Field>
<Button>Create Account</Button>
</Field>
</FieldGroup>
</CardContent>
<CardFooter>
<Button className="w-full">Create account</Button>
</CardFooter>
</Card>
)
}

View File

@@ -5,13 +5,21 @@ import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
RadioGroup,
RadioGroupItem,
@@ -22,7 +30,7 @@ const plans = [
{
id: "starter",
name: "Starter Plan",
description: "Perfect for small businesses.",
description: "For small businesses.",
price: "$10",
},
{
@@ -37,91 +45,96 @@ export function CardsForms() {
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Upgrade your subscription</CardTitle>
<CardTitle className="text-lg">Upgrade your Subscription</CardTitle>
<CardDescription className="text-balance">
You are currently on the free plan. Upgrade to the pro plan to get
access to all features.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3 md:flex-row">
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Evil Rabbit" />
</div>
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor="email">Email</Label>
<Input id="email" placeholder="example@acme.com" />
</div>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="card-number">Card Number</Label>
<div className="grid grid-cols-2 gap-3 md:grid-cols-[1fr_80px_60px]">
<Input
id="card-number"
placeholder="1234 1234 1234 1234"
className="col-span-2 md:col-span-1"
/>
<Input id="card-number-expiry" placeholder="MM/YY" />
<Input id="card-number-cvc" placeholder="CVC" />
</div>
</div>
<fieldset className="flex flex-col gap-3">
<legend className="text-sm font-medium">Plan</legend>
<p className="text-muted-foreground text-sm">
Select the plan that best fits your needs.
</p>
<RadioGroup
defaultValue="starter"
className="grid gap-3 md:grid-cols-2"
>
{plans.map((plan) => (
<Label
className="has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-input/20 flex items-start gap-3 rounded-lg border p-3"
key={plan.id}
>
<RadioGroupItem
value={plan.id}
id={plan.name}
className="data-[state=checked]:border-primary"
/>
<div className="grid gap-1 font-normal">
<div className="font-medium">{plan.name}</div>
<div className="text-muted-foreground text-xs leading-snug text-balance">
{plan.description}
</div>
</div>
</Label>
))}
</RadioGroup>
</fieldset>
<div className="flex flex-col gap-2">
<Label htmlFor="notes">Notes</Label>
<Textarea id="notes" placeholder="Enter notes" />
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Checkbox id="terms" />
<Label htmlFor="terms" className="font-normal">
I agree to the terms and conditions
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="newsletter" defaultChecked />
<Label htmlFor="newsletter" className="font-normal">
Allow us to send you emails
</Label>
</div>
</div>
</div>
<form>
<FieldGroup>
<FieldGroup className="grid grid-cols-2">
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input id="name" placeholder="Max Leiter" />
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" placeholder="mail@acme.com" />
</Field>
</FieldGroup>
<FieldGroup className="grid grid-cols-2 gap-3 md:grid-cols-[1fr_80px_60px]">
<Field>
<FieldLabel htmlFor="card-number">Card Number</FieldLabel>
<Input
id="card-number"
placeholder="1234 1234 1234 1234"
className="col-span-2 md:col-span-1"
/>
</Field>
<Field>
<FieldLabel htmlFor="card-number-expiry">
Expiry Date
</FieldLabel>
<Input id="card-number-expiry" placeholder="MM/YY" />
</Field>
<Field>
<FieldLabel htmlFor="card-number-cvc">CVC</FieldLabel>
<Input id="card-number-cvc" placeholder="CVC" />
</Field>
</FieldGroup>
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
Select the plan that best fits your needs.
</FieldDescription>
<RadioGroup
defaultValue="starter"
className="grid grid-cols-2 gap-2"
>
{plans.map((plan) => (
<FieldLabel key={plan.id}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{plan.name}</FieldTitle>
<FieldDescription className="text-xs">
{plan.description}
</FieldDescription>
</FieldContent>
<RadioGroupItem value={plan.id} id={plan.name} />
</Field>
</FieldLabel>
))}
</RadioGroup>
</FieldSet>
<Field>
<FieldLabel htmlFor="notes">Notes</FieldLabel>
<Textarea id="notes" placeholder="Enter notes" />
</Field>
<Field>
<Field orientation="horizontal">
<Checkbox id="terms" />
<FieldLabel htmlFor="terms" className="font-normal">
I agree to the terms and conditions
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="newsletter" defaultChecked />
<FieldLabel htmlFor="newsletter" className="font-normal">
Allow us to send you emails
</FieldLabel>
</Field>
</Field>
<Field orientation="horizontal">
<Button variant="outline" size="sm">
Cancel
</Button>
<Button size="sm">Upgrade Plan</Button>
</Field>
</FieldGroup>
</form>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" size="sm">
Cancel
</Button>
<Button size="sm">Upgrade Plan</Button>
</CardFooter>
</Card>
)
}

View File

@@ -7,12 +7,11 @@ import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Field, FieldGroup, FieldLabel } from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
Select,
SelectContent,
@@ -33,65 +32,69 @@ export function CardsReportIssue() {
What area are you having problems with?
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
<div className="grid gap-4 sm:grid-cols-2">
<div className="flex flex-col gap-3">
<Label htmlFor={`area-${id}`}>Area</Label>
<Select defaultValue="billing">
<SelectTrigger
id={`area-${id}`}
aria-label="Area"
className="w-full"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="team">Team</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
<SelectItem value="account">Account</SelectItem>
<SelectItem value="deployments">Deployments</SelectItem>
<SelectItem value="support">Support</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor={`security-level-${id}`}>Security Level</Label>
<Select defaultValue="2">
<SelectTrigger
id={`security-level-${id}`}
className="w-full [&_span]:!block [&_span]:truncate"
aria-label="Security Level"
>
<SelectValue placeholder="Select level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Severity 1 (Highest)</SelectItem>
<SelectItem value="2">Severity 2</SelectItem>
<SelectItem value="3">Severity 3</SelectItem>
<SelectItem value="4">Severity 4 (Lowest)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor={`subject-${id}`}>Subject</Label>
<Input id={`subject-${id}`} placeholder="I need help with..." />
</div>
<div className="flex flex-col gap-3">
<Label htmlFor={`description-${id}`}>Description</Label>
<Textarea
id={`description-${id}`}
placeholder="Please include all information relevant to your issue."
className="min-h-28"
/>
</div>
<CardContent>
<FieldGroup>
<FieldGroup className="grid gap-4 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor={`area-${id}`}>Area</FieldLabel>
<Select defaultValue="billing">
<SelectTrigger
id={`area-${id}`}
aria-label="Area"
className="w-full"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="team">Team</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
<SelectItem value="account">Account</SelectItem>
<SelectItem value="deployments">Deployments</SelectItem>
<SelectItem value="support">Support</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor={`security-level-${id}`}>
Security Level
</FieldLabel>
<Select defaultValue="2">
<SelectTrigger
id={`security-level-${id}`}
className="w-full [&_span]:!block [&_span]:truncate"
aria-label="Security Level"
>
<SelectValue placeholder="Select level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Severity 1 (Highest)</SelectItem>
<SelectItem value="2">Severity 2</SelectItem>
<SelectItem value="3">Severity 3</SelectItem>
<SelectItem value="4">Severity 4 (Lowest)</SelectItem>
</SelectContent>
</Select>
</Field>
</FieldGroup>
<Field>
<FieldLabel htmlFor={`subject-${id}`}>Subject</FieldLabel>
<Input id={`subject-${id}`} placeholder="I need help with..." />
</Field>
<Field>
<FieldLabel htmlFor={`description-${id}`}>Description</FieldLabel>
<Textarea
id={`description-${id}`}
placeholder="Please include all information relevant to your issue."
className="min-h-24"
/>
</Field>
<Field orientation="horizontal" className="justify-end">
<Button variant="ghost" size="sm">
Cancel
</Button>
<Button size="sm">Submit</Button>
</Field>
</FieldGroup>
</CardContent>
<CardFooter className="justify-end gap-2">
<Button variant="ghost" size="sm">
Cancel
</Button>
<Button size="sm">Submit</Button>
</CardFooter>
</Card>
)
}

View File

@@ -14,6 +14,14 @@ import {
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { Input } from "@/registry/new-york-v4/ui/input"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
Select,
@@ -73,42 +81,35 @@ export function CardsShare() {
<Separator className="my-4" />
<div className="flex flex-col gap-4">
<div className="text-sm font-medium">People with access</div>
<div className="grid gap-6">
<ItemGroup>
{people.map((person) => (
<div
key={person.email}
className="flex items-center justify-between gap-4"
>
<div className="flex items-center gap-4">
<Avatar>
<AvatarImage src={person.avatar} alt="Image" />
<AvatarFallback>{person.name.charAt(0)}</AvatarFallback>
</Avatar>
<div>
<p className="text-sm leading-none font-medium">
{person.name}
</p>
<p className="text-muted-foreground text-sm">
{person.email}
</p>
</div>
</div>
<Select defaultValue="edit">
<SelectTrigger
className="ml-auto pr-2"
aria-label="Edit"
size="sm"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="edit">Can edit</SelectItem>
<SelectItem value="view">Can view</SelectItem>
</SelectContent>
</Select>
</div>
<Item key={person.email} className="px-0 py-2">
<Avatar>
<AvatarImage src={person.avatar} alt="Image" />
<AvatarFallback>{person.name.charAt(0)}</AvatarFallback>
</Avatar>
<ItemContent>
<ItemTitle>{person.name}</ItemTitle>
<ItemDescription>{person.email}</ItemDescription>
</ItemContent>
<ItemActions>
<Select defaultValue="edit">
<SelectTrigger
className="ml-auto pr-2"
aria-label="Edit"
size="sm"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="edit">Can edit</SelectItem>
<SelectItem value="view">Can view</SelectItem>
</SelectContent>
</Select>
</ItemActions>
</Item>
))}
</div>
</ItemGroup>
</div>
</CardContent>
</Card>

View File

@@ -23,6 +23,13 @@ import {
CommandItem,
CommandList,
} from "@/registry/new-york-v4/ui/command"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
import {
Popover,
PopoverContent,
@@ -71,63 +78,58 @@ const roles = [
export function CardsTeamMembers() {
return (
<Card>
<Card className="gap-4">
<CardHeader>
<CardTitle>Team Members</CardTitle>
<CardDescription>
Invite your team members to collaborate.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-6">
<CardContent>
{teamMembers.map((member) => (
<div
key={member.name}
className="flex items-center justify-between gap-4"
>
<div className="flex items-center gap-4">
<Avatar className="border">
<AvatarImage src={member.avatar} alt="Image" />
<AvatarFallback>{member.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-0.5">
<p className="text-sm leading-none font-medium">
{member.name}
</p>
<p className="text-muted-foreground text-xs">{member.email}</p>
</div>
</div>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="ml-auto shadow-none"
>
{member.role} <ChevronDown />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="end">
<Command>
<CommandInput placeholder="Select role..." />
<CommandList>
<CommandEmpty>No roles found.</CommandEmpty>
<CommandGroup>
{roles.map((role) => (
<CommandItem key={role.name}>
<div className="flex flex-col">
<p className="text-sm font-medium">{role.name}</p>
<p className="text-muted-foreground">
{role.description}
</p>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<Item key={member.name} size="sm" className="gap-4 px-0">
<Avatar className="shrink-0 self-start border">
<AvatarImage src={member.avatar} alt="Image" />
<AvatarFallback>{member.name.charAt(0)}</AvatarFallback>
</Avatar>
<ItemContent>
<ItemTitle>{member.name}</ItemTitle>
<ItemDescription>{member.email}</ItemDescription>
</ItemContent>
<ItemActions>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="ml-auto shadow-none"
>
{member.role} <ChevronDown />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="end">
<Command>
<CommandInput placeholder="Select role..." />
<CommandList>
<CommandEmpty>No roles found.</CommandEmpty>
<CommandGroup>
{roles.map((role) => (
<CommandItem key={role.name}>
<div className="flex flex-col">
<p className="text-sm font-medium">{role.name}</p>
<p className="text-muted-foreground">
{role.description}
</p>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</ItemActions>
</Item>
))}
</CardContent>
</Card>

View File

@@ -7,6 +7,7 @@ import { IconArrowRight } from "@tabler/icons-react"
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
import { type Color, type ColorPalette } from "@/lib/colors"
import { showMcpDocs } from "@/lib/flags"
import { source } from "@/lib/source"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
@@ -30,6 +31,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/registry/new-york-v4/ui/dialog"
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
import { Separator } from "@/registry/new-york-v4/ui/separator"
export function CommandMenu({
@@ -143,7 +145,7 @@ export function CommandMenu({
<Button
variant="secondary"
className={cn(
"bg-surface text-surface-foreground/60 dark:bg-card relative h-8 w-full justify-start pl-2.5 font-normal shadow-none sm:pr-12 md:w-40 lg:w-56 xl:w-64"
"bg-surface text-foreground dark:bg-card relative h-8 w-full justify-start pl-3 font-medium shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64"
)}
onClick={() => setOpen(true)}
{...props}
@@ -151,8 +153,10 @@ export function CommandMenu({
<span className="hidden lg:inline-flex">Search documentation...</span>
<span className="inline-flex lg:hidden">Search...</span>
<div className="absolute top-1.5 right-1.5 hidden gap-1 sm:flex">
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
<CommandMenuKbd className="aspect-square">K</CommandMenuKbd>
<KbdGroup>
<Kbd className="border">{isMac ? "⌘" : "Ctrl"}</Kbd>
<Kbd className="border">K</Kbd>
</KbdGroup>
</div>
</Button>
</DialogTrigger>
@@ -214,6 +218,10 @@ export function CommandMenu({
if (item.type === "page") {
const isComponent = item.url.includes("/components/")
if (!showMcpDocs && item.url.includes("/mcp")) {
return null
}
return (
<CommandMenuItem
key={item.url}

View File

@@ -9,12 +9,14 @@ export function ComponentPreviewTabs({
className,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
component,
source,
...props
}: React.ComponentProps<"div"> & {
align?: "center" | "start" | "end"
hideCode?: boolean
chromeLessOnMobile?: boolean
component: React.ReactNode
source: React.ReactNode
}) {
@@ -51,7 +53,8 @@ export function ComponentPreviewTabs({
</Tabs>
<div
data-tab={tab}
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-1"
data-chrome-less-on-mobile={chromeLessOnMobile}
className="data-[tab=code]:border-code relative rounded-lg border data-[chrome-less-on-mobile=true]:border-0 sm:data-[chrome-less-on-mobile=true]:border md:-mx-1"
>
<div
data-slot="preview"
@@ -61,7 +64,8 @@ export function ComponentPreviewTabs({
<div
data-align={align}
className={cn(
"preview flex h-[450px] w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start"
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
)}
>
{component}

View File

@@ -10,6 +10,7 @@ export function ComponentPreview({
className,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
...props
}: React.ComponentProps<"div"> & {
name: string
@@ -17,12 +18,13 @@ export function ComponentPreview({
description?: string
hideCode?: boolean
type?: "block" | "component" | "example"
chromeLessOnMobile?: boolean
}) {
const Component = Index[name]?.component
if (!Component) {
return (
<p className="text-muted-foreground text-sm">
<p className="text-muted-foreground mt-6 text-sm">
Component{" "}
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
@@ -63,6 +65,7 @@ export function ComponentPreview({
hideCode={hideCode}
component={<Component />}
source={<ComponentSource name={name} collapsible={false} />}
chromeLessOnMobile={chromeLessOnMobile}
{...props}
/>
)

View File

@@ -43,6 +43,14 @@ export async function ComponentSource({
return null
}
// Fix imports.
// Replace @/registry/new-york-v4/ with @/components/.
code = code.replaceAll("@/registry/new-york-v4/", "@/components/")
// Replace export default with export.
code = code.replaceAll("export default", "export")
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
const lang = language ?? title?.split(".").pop() ?? "tsx"
const highlightedCode = await highlightCode(code, lang)

View File

@@ -1,5 +1,6 @@
import Link from "next/link"
import { PAGES_NEW } from "@/lib/docs"
import { source } from "@/lib/source"
export function ComponentsList() {
@@ -21,9 +22,15 @@ export function ComponentsList() {
<Link
key={component.$id}
href={component.url}
className="text-lg font-medium underline-offset-4 hover:underline md:text-base"
className="inline-flex items-center gap-2 text-lg font-medium underline-offset-4 hover:underline md:text-base"
>
{component.name}
{PAGES_NEW.includes(component.url) && (
<span
className="flex size-2 rounded-full bg-blue-500"
title="New"
/>
)}
</Link>
))}
</div>

View File

@@ -3,6 +3,8 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { PAGES_NEW } from "@/lib/docs"
import { showMcpDocs } from "@/lib/flags"
import type { source } from "@/lib/source"
import {
Sidebar,
@@ -15,6 +17,32 @@ import {
SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar"
const TOP_LEVEL_SECTIONS = [
{ name: "Get Started", href: "/docs" },
{
name: "Components",
href: "/docs/components",
},
{
name: "Registry",
href: "/docs/registry",
},
{
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Forms",
href: "/docs/forms",
},
{
name: "Changelog",
href: "/docs/changelog",
},
]
const EXCLUDED_SECTIONS = ["installation", "dark-mode"]
const EXCLUDED_PAGES = ["/docs", "/docs/changelog"]
export function DocsSidebar({
tree,
...props
@@ -23,40 +51,96 @@ export function DocsSidebar({
return (
<Sidebar
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--header-height)-var(--footer-height))] bg-transparent lg:flex"
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)+2rem)] bg-transparent lg:flex"
collapsible="none"
{...props}
>
<SidebarContent className="no-scrollbar px-2 pb-12">
<SidebarContent className="no-scrollbar overflow-x-hidden px-2 pb-12">
<div className="h-(--top-spacing) shrink-0" />
{tree.children.map((item) => (
<SidebarGroup key={item.$id}>
<SidebarGroupLabel className="text-muted-foreground font-medium">
{item.name}
</SidebarGroupLabel>
<SidebarGroupContent>
{item.type === "folder" && (
<SidebarMenu className="gap-0.5">
{item.children.map((item) => {
return (
item.type === "page" && (
<SidebarMenuItem key={item.url}>
<SidebarMenuButton
asChild
isActive={item.url === pathname}
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={item.url}>{item.name}</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarGroup>
<SidebarGroupLabel className="text-muted-foreground font-medium">
Sections
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{TOP_LEVEL_SECTIONS.map(({ name, href }) => {
if (!showMcpDocs && href.includes("/mcp")) {
return null
}
return (
<SidebarMenuItem key={name}>
<SidebarMenuButton
asChild
isActive={
href === "/docs"
? pathname === href
: pathname.startsWith(href)
}
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={href}>
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
{name}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{tree.children.map((item) => {
if (EXCLUDED_SECTIONS.includes(item.$id ?? "")) {
return null
}
return (
<SidebarGroup key={item.$id}>
<SidebarGroupLabel className="text-muted-foreground font-medium">
{item.name}
</SidebarGroupLabel>
<SidebarGroupContent>
{item.type === "folder" && (
<SidebarMenu className="gap-0.5">
{item.children.map((item) => {
if (
!showMcpDocs &&
item.type === "page" &&
item.url?.includes("/mcp")
) {
return null
}
return (
item.type === "page" &&
!EXCLUDED_PAGES.includes(item.url) && (
<SidebarMenuItem key={item.url}>
<SidebarMenuButton
asChild
isActive={item.url === pathname}
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={item.url}>
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
{item.name}
{PAGES_NEW.includes(item.url) && (
<span
className="flex size-2 rounded-full bg-blue-500"
title="New"
/>
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
)
)
})}
</SidebarMenu>
)}
</SidebarGroupContent>
</SidebarGroup>
))}
})}
</SidebarMenu>
)}
</SidebarGroupContent>
</SidebarGroup>
)
})}
</SidebarContent>
</Sidebar>
)

View File

@@ -4,6 +4,8 @@ import * as React from "react"
import Link, { LinkProps } from "next/link"
import { useRouter } from "next/navigation"
import { PAGES_NEW } from "@/lib/docs"
import { showMcpDocs } from "@/lib/flags"
import { source } from "@/lib/source"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york-v4/ui/button"
@@ -13,6 +15,30 @@ import {
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
const TOP_LEVEL_SECTIONS = [
{ name: "Get Started", href: "/docs" },
{
name: "Components",
href: "/docs/components",
},
{
name: "Registry",
href: "/docs/registry",
},
{
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Forms",
href: "/docs/forms",
},
{
name: "Changelog",
href: "/docs/changelog",
},
]
export function MobileNav({
tree,
items,
@@ -79,6 +105,23 @@ export function MobileNav({
))}
</div>
</div>
<div className="flex flex-col gap-4">
<div className="text-muted-foreground text-sm font-medium">
Sections
</div>
<div className="flex flex-col gap-3">
{TOP_LEVEL_SECTIONS.map(({ name, href }) => {
if (!showMcpDocs && href.includes("/mcp")) {
return null
}
return (
<MobileLink key={name} href={href} onOpenChange={setOpen}>
{name}
</MobileLink>
)
})}
</div>
</div>
<div className="flex flex-col gap-8">
{tree?.children?.map((group, index) => {
if (group.type === "folder") {
@@ -90,13 +133,20 @@ export function MobileNav({
<div className="flex flex-col gap-3">
{group.children.map((item) => {
if (item.type === "page") {
if (!showMcpDocs && item.url.includes("/mcp")) {
return null
}
return (
<MobileLink
key={`${item.url}-${index}`}
href={item.url}
onOpenChange={setOpen}
className="flex items-center gap-2"
>
{item.name}
{item.name}{" "}
{PAGES_NEW.includes(item.url) && (
<span className="flex size-2 rounded-full bg-blue-500" />
)}
</MobileLink>
)
}

View File

@@ -1,4 +1,4 @@
const SHOW = false
const SHOW = true
export function TailwindIndicator() {
if (process.env.NODE_ENV === "production" || !SHOW) {

View File

@@ -4,6 +4,627 @@ description: Latest updates and announcements.
toc: false
---
## October 2025 - New Components
For this round of components, I looked at what we build every day, the boring stuff we rebuild over and over, and made reusable abstractions you can actually use.
**These components work with every component library, Radix, Base UI, React Aria, you name it. Copy and paste to your projects.**
- [Spinner](#spinner): An indicator to show a loading state.
- [Kbd](#kbd): Display a keyboard key or group of keys.
- [Button Group](#button-group): A group of buttons for actions and split buttons.
- [Input Group](#input-group): Input with icons, buttons, labels and more.
- [Field](#field): One component. All your forms.
- [Item](#item): Display lists of items, cards, and more.
- [Empty](#empty): Use this one for empty states.
### Spinner
Okay let's start with the easiest ones: **Spinner** and **Kbd**. Pretty basic. We all know what they do.
Here's how you render a spinner:
```tsx
import { Spinner } from "@/components/ui/spinner"
```
```tsx
<Spinner />
```
Here's what it looks like:
<ComponentPreview
name="spinner-basic"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
Here's what it looks like in a button:
<ComponentPreview
name="spinner-button"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
You can edit the code and replace it with your own spinner.
<ComponentPreview
name="spinner-custom"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
### Kbd
Kbd is a component that renders a keyboard key.
```tsx
import { Kbd, KbdGroup } from "@/components/ui/kbd"
```
```tsx
<Kbd>Ctrl</Kbd>
```
Use `KbdGroup` to group keyboard keys together.
```tsx showLineNumbers
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>B</Kbd>
</KbdGroup>
```
<ComponentPreview
name="kbd-demo"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
You can add it to buttons, tooltips, input groups, and more.
### Button Group
I got a lot of requests for this one: Button Group. It's a container that groups related buttons together with consistent styling. Great for action groups, split buttons, and more.
<ComponentPreview
name="button-group-demo"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
Here's the code:
```tsx
import { ButtonGroup } from "@/components/ui/button-group"
```
```tsx showLineNumbers
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
```
You can nest button groups to create more complex layouts with spacing.
```tsx showLineNumbers
<ButtonGroup>
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
<ButtonGroup>
<Button>Button 3</Button>
<Button>Button 4</Button>
</ButtonGroup>
</ButtonGroup>
```
Use `ButtonGroupSeparator` to create split buttons. Classic dropdown pattern.
<ComponentPreview
name="button-group-dropdown"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
You can also use it to add prefix or suffix buttons and text to inputs.
<ComponentPreview
name="button-group-select"
className="[&_.preview]:h-[250px] [&_pre]:!h-[250px]"
/>
```tsx showLineNumbers
<ButtonGroup>
<ButtonGroupText>Prefix</ButtonGroupText>
<Input placeholder="Type something here..." />
<Button>Button</Button>
</ButtonGroup>
```
### Input Group
Input Group lets you add icons, buttons, and more to your inputs. You know, all those little bits you always need around your inputs.
```tsx
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group"
```
```tsx showLineNumbers
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>
```
Here's a preview with icons:
<ComponentPreview
name="input-group-icon"
className="[&_.preview]:h-[300px] [&_pre]:!h-[300px]"
/>
You can also add buttons to the input group.
<ComponentPreview
name="input-group-button"
className="[&_.preview]:h-[300px] [&_pre]:!h-[300px]"
/>
Or text, labels, tooltips,...
<ComponentPreview
name="input-group-text"
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
/>
It also works with textareas so you can build really complex components with lots of knobs and dials or yet another prompt form.
<ComponentPreview
name="input-group-textarea"
className="[&_.preview]:h-[450px] [&_pre]:!h-[450px]"
/>
Oh here are some cool ones with spinners:
<ComponentPreview
name="input-group-spinner"
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
/>
### Field
Introducing **Field**, a component for building really complex forms. The abstraction here is beautiful.
It took me a long time to get it right but I made it work with all your form libraries: Server Actions, React Hook Form, TanStack Form, Bring Your Own Form.
```tsx
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field"
```
Here's a basic field with an input:
```tsx showLineNumbers
<Field>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input id="username" placeholder="Max Leiter" />
<FieldDescription>
Choose a unique username for your account.
</FieldDescription>
</Field>
```
<ComponentPreview
name="field-input"
className="[&_.preview]:h-[350px] [&_pre]:!h-[350px]"
/>
It works with all form controls. Inputs, textareas, selects, checkboxes, radios, switches, sliders, you name it. Here's a full example:
<ComponentPreview
name="field-demo"
className="[&_.preview]:h-[850px] [&_pre]:!h-[850px]"
/>
Here are some checkbox fields:
<ComponentPreview
name="field-checkbox"
className="[&_.preview]:h-[500px] [&_pre]:!h-[500px]"
/>
You can group fields together using `FieldGroup` and `FieldSet`. Perfect for
multi-section forms.
```tsx showLineNumbers
<FieldSet>
<FieldLegend />
<FieldGroup>
<Field />
<Field />
</FieldGroup>
</FieldSet>
```
<ComponentPreview
name="field-fieldset"
className="[&_.preview]:h-[500px] [&_pre]:!h-[500px]"
/>
Making it responsive is easy. Use `orientation="responsive"` and it switches
between vertical and horizontal layouts based on container width. Done.
<ComponentPreview
name="field-responsive"
className="[&_.preview]:h-[600px] [&_pre]:!h-[600px]"
/>
Wait here's more. Wrap your fields in `FieldLabel` to create a selectable field group. Really easy. And it looks great.
<ComponentPreview
name="field-choice-card"
className="[&_.preview]:h-[600px] [&_pre]:!h-[600px]"
/>
### Item
This one is a straightforward flex container that can house nearly any type of content.
I've built this so many times that I decided to create a component for it. Now I use it all the time. I use it to display lists of items, cards, and more.
```tsx
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"
```
Here's a basic item:
```tsx showLineNumbers
<Item>
<ItemMedia variant="icon">
<HomeIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Dashboard</ItemTitle>
<ItemDescription>Overview of your account and activity.</ItemDescription>
</ItemContent>
</Item>
```
<ComponentPreview
name="item-demo"
className="[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
/>
You can add icons, avatars, or images to the item.
<ComponentPreview
name="item-icon"
className="[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
/>
<ComponentPreview
name="item-avatar"
className="[&_.preview]:h-[300px] [&_.preview]:p-4 [&_pre]:!h-[300px]"
/>
And here's what a list of items looks like with `ItemGroup`:
<ComponentPreview
name="item-group"
className="[&_.preview]:h-[500px] [&_.preview]:p-4 [&_pre]:!h-[500px]"
/>
Need it as a link? Use the `asChild` prop:
```tsx showLineNumbers
<Item asChild>
<a href="/dashboard">
<ItemMedia variant="icon">
<HomeIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Dashboard</ItemTitle>
<ItemDescription>Overview of your account and activity.</ItemDescription>
</ItemContent>
</a>
</Item>
```
<ComponentPreview
name="item-link"
className="[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
/>
### Empty
Okay last one: **Empty**. Use this to display empty states in your app.
```tsx
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
```
Here's how you use it:
```tsx showLineNumbers
<Empty>
<EmptyMedia variant="icon">
<InboxIcon />
</EmptyMedia>
<EmptyTitle>No messages</EmptyTitle>
<EmptyDescription>You don't have any messages yet.</EmptyDescription>
<EmptyContent>
<Button>Send a message</Button>
</EmptyContent>
</Empty>
```
<ComponentPreview
name="empty-demo"
className="[&_.preview]:h-[400px] [&_.preview]:p-4 [&_pre]:!h-[400px]"
/>
You can use it with avatars:
<ComponentPreview
name="empty-avatar"
className="[&_.preview]:h-[400px] [&_pre]:!h-[400px]"
/>
Or with input groups for things like search results or email subscriptions:
<ComponentPreview
name="empty-input-group"
className="[&_.preview]:h-[450px] [&_pre]:!h-[450px]"
/>
That's it. Seven new components. Works with all your libraries. Ready for your projects.
---
## September 2025 - Registry Index
We've created an index of open source registries that you can install items from.
You can search, view and add items from the registry index without configuring the `.components.json` file.
They'll be automatically added to your `components.json` file for you.
```bash
npx shadcn add @ai-elements/prompt-input
```
The full list of registries is available at [https://ui.shadcn.com/r/registries.json](https://ui.shadcn.com/r/registries.json).
To add a registry to the index, submit a PR to the `shadcn/ui` repository. See the [registry index documentation](/docs/registry/registry-index) for more details.
---
## August 2025 - shadcn CLI 3.0 and MCP Server
We just shipped shadcn CLI 3.0 with support for namespaced registries, advanced authentication, new commands and a completely rewritten registry engine.
### What's New
- [Namespaced Registries](#namespaced-registries) - Install components using `@registry/name` format.
- [Private Registries](#private-registries) - Secure your registry with advanced authentication.
- [Search & Discovery](#search--discovery) - New commands to find and view code before installing.
- [MCP Server](#mcp-server) - MCP server for all registries.
- [Faster Everything](#faster-everything) - Completely rewritten registry resolution.
- [Improved Error Handling](#improved-error-handling) - Better error messages for users and LLMs.
- [Upgrade Guide](#upgrade-guide) - Migration notes for existing users.
### Namespaced Registries
The biggest change in 3.0 is namespaced registries. You can now install components from registries: a community registry, your company's private registry or internal registry, using the `@registry/name` format.
This makes it easier to distribute code across teams and projects.
Configure registries in your `components.json`:
```json title="components.json"
{
"registries": {
"@acme": "https://acme.com/r/{name}.json",
"@internal": {
"url": "https://registry.company.com/{name}",
"headers": {
"Authorization": "Bearer ${REGISTRY_TOKEN}"
}
}
}
}
```
Then use the `@registry/name` format to install components:
```bash
npx shadcn add @acme/button @internal/auth-system
```
It's completely decentralized. There's no central registrar. Create any namespace you want and organize components however makes sense for your team.
```json title="components.json" showLineNumbers
{
"registries": {
"@design": "https://registry.company.com/design/{name}.json",
"@engineering": "https://registry.company.com/eng/{name}.json",
"@marketing": "https://registry.company.com/marketing/{name}.json"
}
}
```
Components can even depend on resources from different registries. Everything gets resolved and installed automatically from the right sources.
```json title="registry-item.json" showLineNumbers
{
"name": "dashboard",
"type": "registry:block",
"registryDependencies": [
"@shadcn/card", // From default registry
"@v0/chart", // From v0 registry
"@acme/data-table", // From acme registry
"@lib/data-fetcher", // Utility library
"@ai/analytics-prompt" // AI prompt resource
]
}
```
### Private Registries
Need to keep your components private? We've got you covered. Configure authentication with tokens, API keys, or custom headers:
```json title="components.json"
{
"registries": {
"@private": {
"url": "https://registry.company.com/{name}.json",
"headers": {
"Authorization": "Bearer ${REGISTRY_TOKEN}"
}
}
}
}
```
Your private components stay private. Perfect for enterprise teams with proprietary UI libraries.
We support all major authentication methods: basic auth, bearer token, api key query params and custom headers.
See the [authentication docs](/docs/registry/authentication) for more details.
### Search & Discovery
Three new commands make it easy to find exactly what you need:
1. View items from the registry before installing
```bash
npx shadcn view @acme/auth-system
```
2. Search items from registries
```bash
npx shadcn search @tweakcn -q "dark"
```
3. List all items from a registry
```bash
npx shadcn list @acme
```
Preview components before installing them. Search across multiple registries. See the code and all dependencies upfront.
### MCP Server
<Image
src="/images/mcp.jpeg"
width="1432"
height="1050"
alt="Lift Mode"
className="mt-6 w-full overflow-hidden rounded-lg border"
/>
Back in April, we [introduced](https://x.com/shadcn/status/1917597228513853603) the first version of the MCP server. Since then, we've taken everything we learned and built a better MCP server.
Here's what's new:
- Works with all registries. Zero config
- One command to add to your favorite MCP clients
- We improved the underlying tools
- Better integration with the CLI and registries
- Support for multiple registries in the same project
Add the MCP server to your project:
```bash
npx shadcn@latest mcp init
```
See the [docs](/docs/mcp) for more details.
### Faster Everything
We completely rewrote the registry resolution engine from scratch. It's faster, smarter, and handles even the trickiest dependency trees.
- Up to 3x faster dependency resolution
- Smarter file deduplication and merging
- Better monorepo support out of the box
- Updated `build` command for registry authors
### Improved Error Handling
Registry developers can now provide custom error messages to help guide users (and LLMs) when things go wrong. The CLI displays helpful, actionable errors for common issues:
```txt
Unknown registry "@acme". Make sure it is defined in components.json as follows:
{
"registries": {
"@acme": "[URL_TO_REGISTRY]"
}
}
```
Missing environment variables? The CLI tells you exactly what's needed:
```txt
Registry "@private" requires the following environment variables:
• REGISTRY_TOKEN
Set the required environment variables to your .env or .env.local file.
```
Registry authors can provide custom error messages in their responses to help users and AI agents understand and fix issues quickly.
```txt
Error:
You are not authorized to access the item at http://example.com/r/component.
Message:
[Unauthorized] Your API key has expired. Renew it at https://example.com/api/renew-key.
```
### Upgrade Guide
Here's the best part: there are no breaking changes for users. Your existing `components.json` works exactly the same. All your installed components work exactly the same.
For developers, if you're using the programmatic APIs directly, we've deprecated a few functions in favor of better ones:
- `fetchRegistry` → `getRegistry`
- `resolveRegistryTree` → `resolveRegistryItems`
- Schema moved from `shadcn/registry` to `shadcn/schema` package
```diff
- import { registryItemSchema } from "shadcn/registry"
+ import { registryItemSchema } from "shadcn/schema"
```
That's it. Seriously. Everything else just works.
---
## July 2025 - Universal Registry Items
We've added support for universal registry items. This allows you to create registry items that can be distributed to any project i.e. no framework, no components.json, no tailwind, no react required.
@@ -12,6 +633,8 @@ This new registry item type unlocks a lot of new workflows. You can now distribu
See the [docs](/docs/registry/examples) for more details and examples.
---
## July 2025 - Local File Support
The shadcn CLI now supports local files. Initialize projects and add components, themes, hooks, utils and more from local JSON files.
@@ -31,6 +654,8 @@ This feature enables powerful new workflows:
- **Enhanced workflow for agents and MCP** - Generate and run registry items locally
- **Private components** - Keep proprietary components local and private.
---
## June 2025 - `radix-ui`
We've added a new command to migrate to the new `radix-ui` package. This command will replace all `@radix-ui/react-*` imports with `radix-ui`.
@@ -82,7 +707,7 @@ We're working on zero-config MCP support for shadcn/ui registry. One command `np
className="mt-6 w-full overflow-hidden rounded-lg border"
/>
Learn more in the thread here: https://x.com/shadcn/status/1917597228513853603
Learn more in the [thread here](https://x.com/shadcn/status/1917597228513853603).
## March 2025 - shadcn 2.5.0

View File

@@ -13,7 +13,7 @@ The `init` command installs dependencies, adds the `cn` util and configures CSS
npx shadcn@latest init
```
### Options
**Options**
```bash
Usage: shadcn init [options] [components...]
@@ -34,9 +34,12 @@ Options:
--no-src-dir do not use the src directory when creating a new project.
--css-variables use css variables for theming. (default: true)
--no-css-variables do not use css variables for theming.
--no-base-style do not install the base shadcn style
-h, --help display help for command
```
---
## add
Use the `add` command to add components and dependencies to your project.
@@ -45,7 +48,7 @@ Use the `add` command to add components and dependencies to your project.
npx shadcn@latest add [component]
```
### Options
**Options**
```bash
Usage: shadcn add [options] [components...]
@@ -69,6 +72,126 @@ Options:
-h, --help display help for command
```
---
## view
Use the `view` command to view items from the registry before installing them.
```bash
npx shadcn@latest view [item]
```
You can view multiple items at once:
```bash
npx shadcn@latest view button card dialog
```
Or view items from namespaced registries:
```bash
npx shadcn@latest view @acme/auth @v0/dashboard
```
**Options**
```bash
Usage: shadcn view [options] <items...>
view items from the registry
Arguments:
items the item names or URLs to view
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
-h, --help display help for command
```
---
## search
Use the `search` command to search for items from registries.
```bash
npx shadcn@latest search [registry]
```
You can search with a query:
```bash
npx shadcn@latest search @shadcn -q "button"
```
Or search multiple registries at once:
```bash
npx shadcn@latest search @shadcn @v0 @acme
```
The `list` command is an alias for `search`:
```bash
npx shadcn@latest list @acme
```
**Options**
```bash
Usage: shadcn search|list [options] <registries...>
search items from registries
Arguments:
registries the registry names or urls to search items from. Names
must be prefixed with @.
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
-q, --query <query> query string
-l, --limit <number> maximum number of items to display per registry (default: "100")
-o, --offset <number> number of items to skip (default: "0")
-h, --help display help for command
```
---
## list
Use the `list` command to list all items from a registry.
```bash
npx shadcn@latest list @acme
```
**Options**
```bash
Usage: shadcn list [options] <registries...>
list items from registries
Arguments:
registries the registry names or urls to list items from. Names
must be prefixed with @.
```
**Options**
```bash
Usage: shadcn list [options] <registries...>
list items from registries
Arguments:
registries the registry names or urls to list items from. Names
must be prefixed with @.
```
---
## build
Use the `build` command to generate the registry JSON files.
@@ -79,7 +202,7 @@ npx shadcn@latest build
This command reads the `registry.json` file and generates the registry JSON files in the `public/r` directory.
### Options
**Options**
```bash
Usage: shadcn build [options] [registry]

View File

@@ -210,3 +210,94 @@ Import alias for `hooks` such as `use-media-query` or `use-toast`.
}
}
```
## registries
Configure multiple resource registries for your project. This allows you to install components, libraries, utilities, and other resources from various sources including private registries.
See the <Link href="/docs/registry/namespace">Namespaced Registries</Link> documentation for detailed information.
### Basic Configuration
Configure registries with URL templates:
```json title="components.json"
{
"registries": {
"@v0": "https://v0.dev/chat/b/{name}",
"@acme": "https://registry.acme.com/{name}.json",
"@internal": "https://internal.company.com/{name}.json"
}
}
```
The `{name}` placeholder is replaced with the resource name when installing.
### Advanced Configuration with Authentication
For private registries that require authentication:
```json title="components.json"
{
"registries": {
"@private": {
"url": "https://api.company.com/registry/{name}.json",
"headers": {
"Authorization": "Bearer ${REGISTRY_TOKEN}",
"X-API-Key": "${API_KEY}"
},
"params": {
"version": "latest"
}
}
}
}
```
Environment variables in the format `${VAR_NAME}` are automatically expanded from your environment.
### Using Namespaced Registries
Once configured, install resources using the namespace syntax:
```bash
# Install from a configured registry
npx shadcn@latest add @v0/dashboard
# Install from private registry
npx shadcn@latest add @private/button
# Install multiple resources
npx shadcn@latest add @acme/header @internal/auth-utils
```
### Example: Multiple Registry Setup
```json title="components.json"
{
"registries": {
"@shadcn": "https://ui.shadcn.com/r/{name}.json",
"@company-ui": {
"url": "https://registry.company.com/ui/{name}.json",
"headers": {
"Authorization": "Bearer ${COMPANY_TOKEN}"
}
},
"@team": {
"url": "https://team.company.com/{name}.json",
"params": {
"team": "frontend",
"version": "${REGISTRY_VERSION}"
}
}
}
}
```
This configuration allows you to:
- Install public components from shadcn/ui
- Access private company UI components with authentication
- Use team-specific resources with versioning
For more information about authentication, see the <Link href="/docs/registry/authentication">Authentication</Link> documentation.

View File

@@ -11,6 +11,7 @@ description: Every component recreated in Figma. With customizable props, typogr
## Paid
- [shadcn/ui kit](https://shadcndesign.com) by [ Matt Wierzbicki](https://x.com/matsugfx) - A premium, always up-to-date UI kit for Figma - shadcn/ui compatible and optimized for smooth design-to-dev handoff.
- [Shadcraft UI Kit](https://shadcraft.com) - The most advanced shadcn-compatible kit with instant theming via [tweakcn](https://tweakcn.com), a pro library of components and templates, and complete coverage of shadcn components and blocks.
## Free

View File

@@ -0,0 +1,324 @@
---
title: MCP Server
description: Use the shadcn MCP server to browse, search, and install components from registries.
---
The shadcn MCP Server allows AI assistants to interact with items from registries. You can browse available components, search for specific ones, and install them directly into your project using natural language.
For example, you can ask an AI assistant to "Build a landing page using components from the acme registry" or "Find me a login form from the shadcn registry".
Registries are configured in your project's `components.json` file.
```json title="components.json" showLineNumbers
{
"registries": {
"@acme": "https://acme.com/r/{name}.json"
}
}
```
---
## Quick Start
Select your MCP client and follow the instructions to configure the shadcn MCP server. If you'd like to do it manually, see the [Configuration](#configuration) section.
<Tabs defaultValue="claude">
<TabsList>
<TabsTrigger value="claude">Claude Code</TabsTrigger>
<TabsTrigger value="cursor">Cursor</TabsTrigger>
<TabsTrigger value="vscode">VS Code</TabsTrigger>
<TabsTrigger value="codex">Codex</TabsTrigger>
</TabsList>
<TabsContent value="claude" className="mt-4">
**Run the following command** in your project:
```bash
npx shadcn@latest mcp init --client claude
```
**Restart Claude Code** and try the following prompts:
- Show me all available components in the shadcn registry
- Add the button, dialog and card components to my project
- Create a contact form using components from the shadcn registry
**Note:** You can use `/mcp` command in Claude Code to debug the MCP server.
</TabsContent>
<TabsContent value="cursor" className="mt-4">
**Run the following command** in your project:
```bash
npx shadcn@latest mcp init --client cursor
```
Open **Cursor Settings** and **Enable the MCP server** for shadcn. Then try the following prompts:
- Show me all available components in the shadcn registry
- Add the button, dialog and card components to my project
- Create a contact form using components from the shadcn registry
</TabsContent>
<TabsContent value="vscode" className="mt-4">
**Run the following command** in your project:
```bash
npx shadcn@latest mcp init --client vscode
```
Open `.vscode/mcp.json` and click **Start** next to the shadcn server. Then try the following prompts with GitHub Copilot:
- Show me all available components in the shadcn registry
- Add the button, dialog and card components to my project
- Create a contact form using components from the shadcn registry
</TabsContent>
<TabsContent value="codex" className="mt-4">
<Callout className="mt-0">
**Note:** The `shadcn` CLI cannot automatically update `~/.codex/config.toml`.
You'll need to add the configuration manually for Codex.
</Callout>
**Run the following command** in your project:
```bash
npx shadcn@latest mcp init --client codex
```
**Then, add the following configuration** to `~/.codex/config.toml`:
```toml
[mcp_servers.shadcn]
command = "npx"
args = ["shadcn@latest", "mcp"]
```
**Restart Codex** and try the following prompts:
- Show me all available components in the shadcn registry
- Add the button, dialog and card components to my project
- Create a contact form using components from the shadcn registry
</TabsContent>
</Tabs>
---
## What is MCP?
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open protocol that enables AI assistants to securely connect to external data sources and tools. With the shadcn MCP server, your AI assistant gains direct access to:
- **Browse Components** - List all available components, blocks, and templates from any configured registry
- **Search Across Registries** - Find specific components by name or functionality across multiple sources
- **Install with Natural Language** - Add components using simple conversational prompts like "add a login form"
- **Support for Multiple Registries** - Access public registries, private company libraries, and third-party sources
---
## How It Works
The MCP server acts as a bridge between your AI assistant, component registries and the shadcn CLI.
1. **Registry Connection** - MCP connects to configured registries (shadcn/ui, private registries, third-party sources)
2. **Natural Language** - You describe what you need in plain English
3. **AI Processing** - The assistant translates your request into registry commands
4. **Component Delivery** - Resources are fetched and installed in your project
---
## Supported Registries
The shadcn MCP server works out of the box with any shadcn-compatible registry.
- **shadcn/ui Registry** - The default registry with all shadcn/ui components
- **Third-Party Registries** - Any registry following the shadcn registry specification
- **Private Registries** - Your company's internal component libraries
- **Namespaced Registries** - Multiple registries configured with `@namespace` syntax
---
## Configuration
You can use any MCP client to interact with the shadcn MCP server. Here are the instructions for the most popular ones.
### Claude Code
To use the shadcn MCP server with Claude Code, add the following configuration to your project's `.mcp.json` file:
```json title=".mcp.json" showLineNumbers
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}
```
After adding the configuration, restart Claude Code and run `/mcp` to see the shadcn MCP server in the list. If you see `Connected`, you're good to go.
See the [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp) for more details.
### Cursor
To configure MCP in Cursor, add the shadcn server to your project's `.cursor/mcp.json` configuration file:
```json title=".cursor/mcp.json" showLineNumbers
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}
```
After adding the configuration, enable the shadcn MCP server in Cursor Settings.
Once enabled, you should see a green dot next to the shadcn server in the MCP server list and a list of available tools.
See the [Cursor MCP documentation](https://docs.cursor.com/en/context/mcp#using-mcp-json) for more details.
### VS Code
To configure MCP in VS Code with GitHub Copilot, add the shadcn server to your project's `.vscode/mcp.json` configuration file:
```json title=".vscode/mcp.json" showLineNumbers
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}
```
After adding the configuration, open `.vscode/mcp.json` and click **Start** next to the shadcn server.
See the [VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more details.
### Codex
<Callout>
**Note:** The `shadcn` CLI cannot automatically update `~/.codex/config.toml`.
You'll need to add the configuration manually.
</Callout>
To configure MCP in Codex, add the shadcn server to `~/.codex/config.toml`:
```toml title="~/.codex/config.toml" showLineNumbers
[mcp_servers.shadcn]
command = "npx"
args = ["shadcn@latest", "mcp"]
```
After adding the configuration, restart Codex to load the MCP server.
---
## Configuring Registries
The MCP server supports multiple registries through your project's `components.json` configuration. This allows you to access components from various sources including private registries and third-party providers.
Configure additional registries in your `components.json`:
```json title="components.json" showLineNumbers
{
"registries": {
"@acme": "https://registry.acme.com/{name}.json",
"@internal": {
"url": "https://internal.company.com/{name}.json",
"headers": {
"Authorization": "Bearer ${REGISTRY_TOKEN}"
}
}
}
}
```
<Callout>
**Note:** No configuration is needed to access the standard shadcn/ui
registry.
</Callout>
---
## Authentication
For private registries requiring authentication, set environment variables in your `.env.local`:
```bash title=".env.local"
REGISTRY_TOKEN=your_token_here
API_KEY=your_api_key_here
```
For more details on registry authentication, see the [Authentication documentation](/docs/registry/authentication).
---
## Example Prompts
Once the MCP server is configured, you can use natural language to interact with registries. Try one of the following prompts:
### Browse & Search
- Show me all available components in the shadcn registry
- Find me a login form from the shadcn registry
### Install Items
- Add the button component to my project
- Create a login form using shadcn components
- Install the Cursor rules from the acme registry
### Work with Namespaces
- Show me components from acme registry
- Install @internal/auth-form
- Build me a landing page using hero, features and testimonials sections from the acme registry
---
## Troubleshooting
### MCP Not Responding
If the MCP server isn't responding to prompts:
1. **Check Configuration** - Verify the MCP server is properly configured and enabled in your MCP client
2. **Restart MCP Client** - Restart your MCP client after configuration changes
3. **Verify Installation** - Ensure `shadcn` is installed in your project
4. **Check Network** - Confirm you can access the configured registries
### Registry Access Issues
If components aren't loading from registries:
1. **Check components.json** - Verify registry URLs are correct
2. **Test Authentication** - Ensure environment variables are set for private registries
3. **Verify Registry** - Confirm the registry is online and accessible
4. **Check Namespace** - Ensure namespace syntax is correct (`@namespace/component`)
### Installation Failures
If components fail to install:
1. **Check Project Setup** - Ensure you have a valid `components.json` file
2. **Verify Paths** - Confirm the target directories exist
3. **Check Permissions** - Ensure write permissions for component directories
4. **Review Dependencies** - Check that required dependencies are installed
### No Tools or Prompts
If you see the `No tools or prompts` message, try the following:
1. **Clear the npx cache** - Run `npx clear-npx-cache`
2. **Re-enable the MCP server** - Try to re-enable the MCP server in your MCP client
3. **Check Logs** - In Cursor, you can see the logs under View -> Output and select `MCP: project-*` in the dropdown.
---
## Learn More
- [Registry Documentation](/docs/registry) - Complete guide to shadcn registries
- [Namespaces](/docs/registry/namespace) - Configure multiple registry sources
- [Authentication](/docs/registry/authentication) - Secure your private registries
- [MCP Specification](https://modelcontextprotocol.io) - Learn about Model Context Protocol

View File

@@ -14,6 +14,7 @@
"blocks",
"figma",
"changelog",
"[llms.txt](/llms.txt)",
"legacy"
]
}

View File

@@ -170,7 +170,7 @@ To use CSS variables for theming set `tailwind.cssVariables` to `true` in your `
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/registry/new-york-v4/ui",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
@@ -199,7 +199,7 @@ To use utility classes for theming set `tailwind.cssVariables` to `false` in you
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/registry/new-york-v4/ui",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},

View File

@@ -57,7 +57,7 @@ import { Badge } from "@/components/ui/badge"
You can use the `asChild` prop to make another component look like a badge. Here's an example of a link that looks like a badge.
```tsx showLineNumbers
import { Link } from "next/link"
import Link from "next/link"
import { Badge } from "@/components/ui/badge"

View File

@@ -180,7 +180,7 @@ To use a custom link component from your routing library, you can use the `asChi
/>
```tsx showLineNumbers {1,8-10}
import { Link } from "next/link"
import Link from "next/link"
...

View File

@@ -0,0 +1,219 @@
---
title: Button Group
description: A container that groups related buttons together with consistent styling.
component: true
---
<ComponentPreview name="button-group-demo" />
## Installation
<CodeTabs>
<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">
```bash
npx shadcn@latest add button-group
```
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Install the following dependencies:</Step>
```bash
npm install @radix-ui/react-slot
```
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource name="button-group" title="components/ui/button-group.tsx" />
<Step>Update the import paths to match your project setup.</Step>
</Steps>
</TabsContent>
</CodeTabs>
## Usage
```tsx
import {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
} from "@/components/ui/button-group"
```
```tsx
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
```
## Accessibility
- The `ButtonGroup` component has the `role` attribute set to `group`.
- Use <Kbd>Tab</Kbd> to navigate between the buttons in the group.
- Use `aria-label` or `aria-labelledby` to label the button group.
```tsx showLineNumbers
<ButtonGroup aria-label="Button group">
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
```
## ButtonGroup vs ToggleGroup
- Use the `ButtonGroup` component when you want to group buttons that perform an action.
- Use the `ToggleGroup` component when you want to group buttons that toggle a state.
## Examples
### Orientation
Set the `orientation` prop to change the button group layout.
<ComponentPreview name="button-group-orientation" />
### Size
Control the size of buttons using the `size` prop on individual buttons.
<ComponentPreview name="button-group-size" />
### Nested
Nest `<ButtonGroup>` components to create button groups with spacing.
<ComponentPreview name="button-group-nested" />
### Separator
The `ButtonGroupSeparator` component visually divides buttons within a group.
Buttons with variant `outline` do not need a separator since they have a border. For other variants, a separator is recommended to improve the visual hierarchy.
<ComponentPreview name="button-group-separator" />
### Split
Create a split button group by adding two buttons separated by a `ButtonGroupSeparator`.
<ComponentPreview name="button-group-split" />
### Input
Wrap an `Input` component with buttons.
<ComponentPreview name="button-group-input" />
### Input Group
Wrap an `InputGroup` component to create complex input layouts.
<ComponentPreview name="button-group-input-group" />
### Dropdown Menu
Create a split button group with a `DropdownMenu` component.
<ComponentPreview name="button-group-dropdown" />
### Select
Pair with a `Select` component.
<ComponentPreview name="button-group-select" />
### Popover
Use with a `Popover` component.
<ComponentPreview name="button-group-popover" />
## API Reference
### ButtonGroup
The `ButtonGroup` component is a container that groups related buttons together with consistent styling.
| Prop | Type | Default |
| ------------- | ---------------------------- | -------------- |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` |
```tsx
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>
```
Nest multiple button groups to create complex layouts with spacing. See the [nested](#nested) example for more details.
```tsx
<ButtonGroup>
<ButtonGroup />
<ButtonGroup />
</ButtonGroup>
```
### ButtonGroupSeparator
The `ButtonGroupSeparator` component visually divides buttons within a group.
| Prop | Type | Default |
| ------------- | ---------------------------- | ------------ |
| `orientation` | `"horizontal" \| "vertical"` | `"vertical"` |
```tsx
<ButtonGroup>
<Button>Button 1</Button>
<ButtonGroupSeparator />
<Button>Button 2</Button>
</ButtonGroup>
```
### ButtonGroupText
Use this component to display text within a button group.
| Prop | Type | Default |
| --------- | --------- | ------- |
| `asChild` | `boolean` | `false` |
```tsx
<ButtonGroup>
<ButtonGroupText>Text</ButtonGroupText>
<Button>Button</Button>
</ButtonGroup>
```
Use the `asChild` prop to render a custom component as the text, for example a label.
```tsx showLineNumbers
import { ButtonGroupText } from "@/components/ui/button-group"
import { Label } from "@/components/ui/label"
export function ButtonGroupTextDemo() {
return (
<ButtonGroup>
<ButtonGroupText asChild>
<Label htmlFor="name">Text</Label>
</ButtonGroupText>
<Input placeholder="Type something here..." id="name" />
</ButtonGroup>
)
}
```

View File

@@ -5,7 +5,23 @@ featured: true
component: true
---
<ComponentPreview name="button-demo" description="A button" />
import { InfoIcon } from "lucide-react"
<Callout variant="info" icon={<InfoIcon />}>
**Updated:** We have updated the button component to add new sizes: `icon-sm` and `icon-lg`. See the
[changelog](/docs/components/button#changelog) for more details. Follow the
instructions to update your project.
</Callout>
<ComponentPreview name="button-demo" description="A button" className="mb-4" />
```tsx showLineNumbers
<Button variant="outline">Button</Button>
<Button variant="outline" size="icon" aria-label="Submit">
<ArrowUpIcon />
</Button>
```
## Installation
@@ -55,12 +71,214 @@ import { Button } from "@/components/ui/button"
<Button variant="outline">Button</Button>
```
## Link
## Cursor
Tailwind v4 [switched](https://tailwindcss.com/docs/upgrade-guide#buttons-use-the-default-cursor) from `cursor: pointer` to `cursor: default` for the button component.
If you want to keep the `cursor: pointer` behavior, add the following code to your CSS file:
```css showLineNumbers title="globals.css"
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
```
## Examples
### Size
<ComponentPreview name="button-size" className="mb-4" />
```tsx
// Small
<Button size="sm" variant="outline">Small</Button>
<Button size="icon-sm" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
// Medium
<Button variant="outline">Default</Button>
<Button size="icon" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
// Large
<Button size="lg" variant="outline">Large</Button>
<Button size="icon-lg" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
```
### Default
<ComponentPreview
name="button-default"
description="A primary button"
className="mb-4"
/>
```tsx
<Button>Button</Button>
```
### Outline
<ComponentPreview
name="button-outline"
description="A button using the outline variant."
className="mb-4"
/>
```tsx
<Button variant="outline">Outline</Button>
```
### Secondary
<ComponentPreview
name="button-secondary"
description="A secondary button"
className="mb-4"
/>
```tsx
<Button variant="secondary">Secondary</Button>
```
### Ghost
<ComponentPreview
name="button-ghost"
description="A button using the ghost variant"
className="mb-4"
/>
```tsx
<Button variant="ghost">Ghost</Button>
```
### Destructive
<ComponentPreview
name="button-destructive"
description="A destructive button"
className="mb-4"
/>
```tsx
<Button variant="destructive">Destructive</Button>
```
### Link
<ComponentPreview
name="button-link"
description="A button using the link variant."
className="mb-4"
/>
```tsx
<Button variant="link">Link</Button>
```
### Icon
<ComponentPreview
name="button-icon"
description="An icon button"
className="mb-4"
/>
```tsx showLineNumbers
<Button variant="outline" size="icon" aria-label="Submit">
<CircleFadingArrowUpIcon />
</Button>
```
### With Icon
The spacing between the icon and the text is automatically adjusted
based on the size of the button. You do not need any margin on the icon.
<ComponentPreview
name="button-with-icon"
description="A button with an icon"
className="mb-4"
/>
```tsx
<Button variant="outline" size="sm">
<IconGitBranch /> New Branch
</Button>
```
### Rounded
Use the `rounded-full` class to make the button rounded.
<ComponentPreview name="button-rounded" className="mb-4" />
```tsx
<Button variant="outline" size="icon" className="rounded-full">
<ArrowUpRightIcon />
</Button>
```
### Spinner
<ComponentPreview
name="button-loading"
description="A button with a loading state."
className="mb-4"
/>
```tsx showLineNumbers
<Button size="sm" variant="outline" disabled>
<Spinner />
Submit
</Button>
```
### Button Group
To create a button group, use the `ButtonGroup` component. See the [Button Group](/docs/components/button-group) documentation for more details.
<ComponentPreview name="button-group-demo" className="mb-4" />
```tsx showLineNumbers
<ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon" aria-label="Go Back">
<ArrowLeftIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Archive</Button>
<Button variant="outline">Report</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Snooze</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" aria-label="More Options">
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent />
</DropdownMenu>
</ButtonGroup>
</ButtonGroup>
```
### Link
You can use the `asChild` prop to make another component look like a button. Here's an example of a link that looks like a button.
```tsx showLineNumbers
import { Link } from "next/link"
import Link from "next/link"
import { Button } from "@/components/ui/button"
@@ -73,55 +291,33 @@ export function LinkAsButton() {
}
```
## Examples
## API Reference
### Default
### Button
<ComponentPreview name="button-demo" description="A primary button" />
The `Button` component is a wrapper around the `button` element that adds a variety of styles and functionality.
### Secondary
| Prop | Type | Default |
| --------- | ----------------------------------------------------------------------------- | ----------- |
| `variant` | `"default" \| "outline" \| "ghost" \| "destructive" \| "secondary" \| "link"` | `"default"` |
| `size` | `"default" \| "sm" \| "lg" \| "icon" \| "icon-sm" \| "icon-lg"` | `"default"` |
| `asChild` | `boolean` | `false` |
<ComponentPreview name="button-secondary" description="A secondary button" />
## Changelog
### Destructive
### 2025-09-24 New sizes
<ComponentPreview
name="button-destructive"
description="A destructive button"
/>
We have added two new sizes to the button component: `icon-sm` and `icon-lg`. These sizes are used to create icon buttons. To add them, edit `button.tsx` and add the following code under `size` in `buttonVariants`:
### Outline
<ComponentPreview
name="button-outline"
description="A button using the outline variant."
/>
### Ghost
<ComponentPreview
name="button-ghost"
description="A button using the ghost variant"
/>
### Link
<ComponentPreview
name="button-link"
description="A button using the link variant."
/>
### Icon
<ComponentPreview name="button-icon" description="An icon button" />
### With Icon
<ComponentPreview name="button-with-icon" description="A button with an icon" />
### Loading
<ComponentPreview
name="button-loading"
description="A button with a loading state."
/>
```tsx showLineNumbers title="components/ui/button.tsx"
const buttonVariants = cva("...", {
variants: {
size: {
// ...
"icon-sm": "size-8",
"icon-lg": "size-10",
// ...
},
},
})
```

View File

@@ -56,9 +56,3 @@ import { Checkbox } from "@/components/ui/checkbox"
```tsx
<Checkbox />
```
## Examples
### Form
<ComponentPreview name="checkbox-form-multiple" />

View File

@@ -143,7 +143,3 @@ export function ExampleCombobox() {
You can create a responsive combobox by using the `<Popover />` on desktop and the `<Drawer />` components on mobile.
<ComponentPreview name="combobox-responsive" />
### Form
<ComponentPreview name="combobox-form" />

View File

@@ -94,7 +94,3 @@ This component uses the `chrono-node` library to parse natural language dates.
title="Natural Language Picker"
description="A calendar with natural language picker."
/>
### Form
<ComponentPreview name="date-picker-form" />

View File

@@ -93,3 +93,19 @@ import {
name="dropdown-menu-radio-group"
description="A dropdown menu with radio items."
/>
### Dialog
This example shows how to open a dialog from a dropdown menu.
Use `modal={false}` on the `DropdownMenu` component.
```tsx showLineNumbers
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="outline">Actions</Button>
</DropdownMenuTrigger>
</DropdownMenu>
```
<ComponentPreview name="dropdown-menu-dialog" />

Some files were not shown because too many files have changed in this diff Show More