Compare commits

...

197 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
github-actions[bot]
b34f3fdc4f chore(release): version packages (#7941)
* 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-08-27 19:10:35 +04:00
shadcn
2ecf876fa1 chore: beta to latest 2025-08-27 12:35:02 +04:00
shadcn
dcd2c3ef14 chore: beta to latest 2025-08-27 12:27:10 +04:00
shadcn
17422714f6 feat(shadcn): mcp init (#8086) 2025-08-27 12:05:21 +04:00
shadcn
fc27ba2692 fix(shadcn): fix --defaults options (#8081)
* fix(shadcn): fix defaults options

* chore: changeset
2025-08-26 15:49:43 +04:00
shadcn
f854190b53 fix(shadcn): load env (#8061) 2025-08-25 14:05:12 +04:00
shadcn
396275e46a feat(www): switch to md from mdx (#8019)
* feat(www): switch to md from mdx

* feat(www): update url
2025-08-13 16:08:20 +04:00
shadcn
296feb28a2 feat(shadcn): new mcp server (#8012)
* feat(shadcn): add getRegistriesConfig api

* feat(shadcn): add new mcp command

* feat(shadcn): add get_item_examples_from_registries and get_add_command_for_items

* feat: remove getRegistriesConfig

* chore: changeset
2025-08-13 11:15:26 +04:00
shadcn
a941287411 deps(shadcn): bump all dependencies (#8004)
* deps(shadcn): bump all dependencies

* chore: pin deps for fumadocs
2025-08-11 20:21:40 +04:00
shadcn
2e34c95c4e feat(shadcn): update search results format (#8003) 2025-08-11 15:52:02 +04:00
shadcn
fed7e3bfdc feat(shadcn): update signatures of apis (#8001)
* feat(shadcn): improve apis signature

* feat(shadcn): update signature of apis

* fix: tests snapshot
2025-08-11 14:34:10 +04:00
shadcn
4f5333ea7a feat(shadcn): add view command (#7994)
* feat(shadcn): add view command

* test(shadcn): add tests for view command

* feat(shadcn): allow shadow config in view command

* chore: changeset

* test(shadcn): skip view

* test(shadcn): update view port number

* feat(shadcn): add list command

* fix

* feat(shadcn): implement search command

* fix: tests

* fix

* chore: update changesets
2025-08-11 13:01:05 +04:00
shadcn
b5b8deedde chore: update changesets 2025-08-10 18:48:43 +04:00
shadcn
7d71b02fb1 feat(shadcn): add getRegistry (#7992) 2025-08-10 18:13:50 +04:00
shadcn
b3639227d0 feat(shadcn): deprecate fetchRegistry and resolveRegistryTree (#7990) 2025-08-10 16:25:04 +04:00
shadcn
a4a3600757 feat: move schema exports to shadcn/schema (#7989) 2025-08-10 16:05:53 +04:00
shadcn
a426fea941 refactor(shadcn): add getRegistryItems and resolveRegistryItems (#7983)
* feat(shadcn): refactor fetchFromRegistry

* refactor(shadcn): better api

* chore: changeset

* fix

* fix

* refactor

* refactor(shadcn): update getRegistryItems

* refactor(shadcn): error handling

* fix: getRegistryItems header context

* fix: tests

* feat(shadcn): export errors

* refactor(shadcn): getRegistryItems getRegistry

* fix

* fix

* fix

* fix

* chore: changeset

* chore: remove minor changeset
2025-08-10 15:20:38 +04:00
shadcn
6e870c3993 feat(shadcn): copy registry.json for build command (#7972)
* feat(shadcn): copy registry.json on build

* chore: changeset
2025-08-07 20:48:59 +04:00
shadcn
68aa3389de tests(shadcn): add more tests to cover registryResolveItemTree (#7971)
* tests(shadcn): add more registryResolveItemTree tests

* tests(shadcn): add more tests to cover registryResolveItemTree
2025-08-07 15:44:23 +04:00
shadcn
2e9ccede8f feat(shadcn): deduplicate files by target (#7969) 2025-08-07 14:33:17 +04:00
shadcn
fc8927a1f9 fix(shadcn): monorepo in nix system (#7962)
* debug: do not cd after init

* chore: changeset
2025-08-07 14:25:23 +04:00
shadcn
ccfd14946b feat: update schema.json to allow registries field (#7959) 2025-08-06 16:55:02 +04:00
shadcn
01c02b289a feat: add registry.json for all styles (#7958)
* feat: add style to registry

* feat: build registry.json for all styles
2025-08-06 16:22:32 +04:00
shadcn
a80ab37483 feat(shadcn): update file handling for monorepo (#7955)
* feat(shadcn): update monorepo handling

* feat(shadcn): update file handling for monorepo

* chore: changeset
2025-08-06 15:13:51 +04:00
shadcn
469250115f feat: update dependencies in monorepo (#7956) 2025-08-06 15:03:28 +04:00
shadcn
2c164b0f22 feat(shadcn): update registry dependencies resolution algorithm (#7948)
* feat(shadcn): update dependency resolution algorithm

* feat(shadcn): rename style to base-style

* feat(shadcn): init from namespaced

* fix(shadcn): force validation early

* chore: changeset

* fix(shadcn): headers

* fix: smh

* fix(shadcn): restore backup on exit and error
2025-08-06 13:38:08 +04:00
shadcn
578f83cbef chore: changeset (#7940) 2025-08-04 14:40:36 +04:00
shadcn
07eda36b13 feat(shadcn): add namespaced registries support (#7919)
* chore(shadcn): implement registries poc

* feat(shadcn): refactor our initial implementation

* feat(shadcn): properly resolve namespaced registryDependencies

* feat(shadcn): resolve namespaced registries recursively

* fix

* feat(shadcn): implement dotenv support

* test(shadcn): mock shadcn registry

* fix

* fix

* fix

* refactor(shadcn): update functions and tests

* refactor(shadcn): add fetchFromRegistry (#7937)

* fix

* feat(shadcn): add shadcn as a built-in registry

* fix

* feat(shadcn): update no framework and shadcn
2025-08-04 14:35:41 +04:00
shadcn
0eccdc9c5f docs: add docs for envVars 2025-07-30 12:22:40 +04:00
shadcn
0940c6aec7 chore: update deps 2025-07-30 12:11:45 +04:00
github-actions[bot]
e244952500 chore(release): version packages (#7909)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-30 12:10:45 +04:00
shadcn
0e3d6b24d3 test: fix flaky remote registry test (#7910)
* test: fix flaky remote registry test

* fix

* fix: test

* fix

* fix

* fix

* fix

* fix

* tests: cleanup
2025-07-30 12:06:13 +04:00
shadcn
cef5af9ed3 ci: bump version for changeset action 2025-07-29 17:08:48 +04:00
shadcn
6deb0fdbb6 chore: remove tests form changesets 2025-07-29 17:01:45 +04:00
shadcn
e9ae79f874 ci: fix 2025-07-29 16:57:16 +04:00
shadcn
d891132f2a test: remove init tests (#7908)
* test(shadcn): remove init test

* chore: changeset
2025-07-29 16:45:52 +04:00
shadcn
873f7f2773 feat: add tests package (#7907)
* feat: add tests package

* fix

* fix

* debug

* debug

* debug

* fix

* debug

* fix: no concurrent

* fix

* test: add vite-app tests

* test: add tests
2025-07-29 16:31:10 +04:00
shadcn
e6778dee87 feat(shadcn): add envVars to schema (#7902)
* feat(shadcn): add envVars to schema

* fix(shadcn): tests

* chore: changeset
2025-07-28 12:14:46 +04:00
shadcn
97a8de1c1b feat: update handling of env files in registry (#7896)
* feat: handle env update

* tests(shadcn): add tests for env helpers

* test(shadcn): update files test

* feat(shadcn): implement file alternatives

* test(shadcn): fix alternative handling

* fix(shadcn): env var logging

* test(shadcn): add tests for multi line env

* chore: changeset

* ci: update
2025-07-27 12:28:39 +04:00
shadcn
19d7fbb731 Use v4 blocks for Open in v0 (#7898)
* feat(www): use v4 blocks for v0

* fix: defaultIndex
2025-07-27 11:34:07 +04:00
shadcn
a9ab05ad83 Merge branch 'main' of github.com:shadcn-ui/ui 2025-07-23 11:57:19 +04:00
shadcn
6ac114ae68 feat: update hero 2025-07-23 11:57:05 +04:00
Mohit Khatri
d5770e4350 fix: resolve table overflow styling issues (#7874) 2025-07-23 11:32:05 +04:00
shadcn
4730276256 fix: spacing 2025-07-23 11:30:23 +04:00
shadcn
4e04567b07 deps: lock file 2025-07-23 10:58:45 +04:00
github-actions[bot]
6f63b04d28 chore(release): version packages (#7866)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-23 10:57:12 +04:00
shadcn
e38228b574 feat: implement open in (#7875)
* feat: implement open in

* fix: display
2025-07-23 10:30:32 +04:00
shadcn
8807103586 fix: index 2025-07-22 21:18:33 +04:00
shadcn
3424ab709e feat: add docs copy page (#7872) 2025-07-22 21:06:27 +04:00
shadcn
4a86a55cac feat(www): implement llm routes (#7868) 2025-07-22 19:59:38 +04:00
shadcn
2926574d0e fix(shadcn): universal item files type (#7867)
* fix(shadcn): universal item files type

* chore: changeset

* fix: style
2025-07-22 14:58:54 +04:00
shadcn
20e913d8e1 fix: handling of themes in registry (#7837) 2025-07-22 11:13:37 +04:00
github-actions[bot]
3433aaffaa chore(release): version packages (#7834)
* chore(release): version packages

* deps: lock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-07-16 18:26:52 +04:00
shadcn
d9cdc3f7ae Revert "fix: handling of shouldOverwriteCssVars" (#7833)
* Revert "fix: handling of shouldOverwriteCssVars (#7829)"

This reverts commit ed5237c231.

* fix: revert
2025-07-16 18:22:40 +04:00
github-actions[bot]
e75e7b3866 chore(release): version packages (#7830)
* chore(release): version packages

* deps: update lock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-07-16 17:52:23 +04:00
shadcn
ed5237c231 fix: handling of shouldOverwriteCssVars (#7829)
* fix(shadcn): handling of shouldOverwriteCssVars

* chore: changeset
2025-07-16 17:44:25 +04:00
shadcn
f85ca066dc deps: update 2025-07-11 18:41:06 +04:00
github-actions[bot]
54e66d4450 chore(release): version packages (#7758)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-11 18:23:59 +04:00
shadcn
6c341c16ae feat: fix safe target and add docs (#7795)
* feat: fix safe target and add docs

* chore: add changeset

* fix: changelog
2025-07-11 18:19:34 +04:00
shadcn
06d03d64f4 feat(shadcn): add support for universal registry item (#7782)
* feat(shadcn): add support for universal registry item

* chore: changeset
2025-07-10 20:17:45 +04:00
shadcn
6407a3b330 fix: chart 2025-07-09 15:52:31 +04:00
shadcn
96b15f6090 feat: update command menu pages and filtering (#7771) 2025-07-09 12:43:50 +04:00
shadcn
2fe9cf6d26 fix: pin recharts until we upgrade to v3 (#7769)
* fix: pin recharts

* docs: add v3 callout
2025-07-09 12:10:40 +04:00
Ehsanullah Haidary
728cb4cfa5 fix(component) Date Picker Dropdown values not visible in chrome, opera and edge (#7724)
* fix(component) Date Picker Dropdown values not visible in chrome, opera and edge

* fix(component) bg-popover added to dropdown in calendar.tsx to fix Date Picker Dropdown values not visible

* chore: build registry

* style: fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-07-09 11:43:15 +04:00
shadcn
db93787712 feat(shadcn): implement registry safe path (#7757)
* feat(shadcn): implement registry safe path

* chore: changeset

* style(shadcn): formatting

* fix
2025-07-08 20:25:14 +04:00
shadcn
1cdd6c1645 Merge branch 'main' of github.com:shadcn-ui/ui 2025-07-07 21:03:25 +04:00
shadcn
4983c6e1f4 chore: changelog 2025-07-07 21:03:06 +04:00
github-actions[bot]
7443edcfb0 chore(release): version packages (#7719)
* chore(release): version packages

* deps: lock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-07-07 20:52:36 +04:00
shadcn
9d9a33be52 fix: margin 2025-07-07 15:10:32 +04:00
shadcn
d544a7f7a5 feat: refactor registryDependencies resolution (#7720)
* feat(shadcn): refactor registry dependencies resolution

* chore: changeset

* fix

* style: fix some code style
2025-07-01 17:56:50 +04:00
shadcn
48fe0d709f feat(shadcn): add file support (#7717)
* feat(shadcn): add file support

* fix: format

* fix: types

* feat(shadcn): update init and add description

* docs: update docs for cli

* chore: add changeset
2025-07-01 17:06:17 +04:00
Kitsune
ed244ea0b5 fix(cli): detect vinxi-based frameworks (@tanstack/start, SolidStart, ...) (#6330)
* fix(cli): detect vinxi-based frameworks

* chore: add changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-07-01 16:58:07 +04:00
Wolfr
b8fede1742 docs(v4): link to obra figma kit (#7643) 2025-06-30 11:45:49 +04:00
github-actions[bot]
84d6c83bad chore(release): version packages (#7626)
* chore(release): version packages

* chore(release): version packages

* deps: update lock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-06-18 16:01:20 +04:00
xabierlameiro.com
5b8ee41511 fix(cli): correct function name typo unnsetSpreadElements to unsetSpreadElements (#7609)
* fix(cli): correct function name typo unnsetSpreadElements to unsetSpreadElements

* chore: add changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-06-18 11:44:59 +04:00
shadcn
7c3d34cdc9 chore: fix changeset (#7640)
* fix(shadcn): update plugin handling

* style(shadcn): format fix

* docs(www): add docs for plugins

* chore: add changeset
2025-06-18 11:29:23 +04:00
shadcn
56c4c83511 fix(shadcn): update plugin handling (#7632)
* fix(shadcn): update plugin handling

* style(shadcn): format fix

* docs(www): add docs for plugins
2025-06-18 11:03:36 +04:00
shadcn
2821cb0e39 chore: move cli to deprecated (#7631) 2025-06-17 12:32:10 +04:00
Wolfr
3c87402de2 Add newly available Figma kit to docs (#7604)
Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 16:21:26 +04:00
xabierlameiro.com
20a88e1f15 fix(components): resolve duplicate id conflict in calendar-24 component (#7611)
* fix(components): resolve duplicate id conflict in calendar-24 component

- Changed Button id from 'date' to 'date-picker'
- Changed Input id from 'time' to 'time-picker'
- Updated corresponding Label htmlFor attributes to match new unique IDs

Fixes #7561

* chore: rebuild registry after calendar-24 fixes
2025-06-16 16:13:51 +04:00
Zach Nugent
cb19ab8464 feat(shadcn): add support for updating dependencies with expo-cli for RN compatibility (#7540)
* feat(shadcn): add support for updating dependencies with expo-cli for RN compatibility

* feat(shadcn): add expo as a framework

* fix: update the contributing command for registry

* refactor(shadcn): update dependencies install functionality

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 16:05:41 +04:00
github-actions[bot]
cf1851ca09 chore(release): version packages (#7625)
* chore(release): version packages

* deps: update lock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 15:45:23 +04:00
Manuel Schiller
c86c27a2ff fix TanStack Start detection (#7601)
* fix tanstack start detection

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 15:24:41 +04:00
Gaëtan H
8847126c65 chore(vscode): set custom Tailwind config path for monorepo UI (#7618)
Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 15:18:27 +04:00
shadcn
65350857a4 ci: fix stale bot (#7624) 2025-06-16 15:02:53 +04:00
shadcn
40c7473c7e fix(www): update open-in-v0-cta.tsx 2025-06-14 06:20:16 +04:00
Taesu
4698ee960f chore: update react-day-picker version to match updated calendar component (#7585)
Co-authored-by: shadcn <m@shadcn.com>
2025-06-12 15:44:40 +04:00
920 changed files with 119575 additions and 10189 deletions

View File

@@ -7,5 +7,5 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["www", "v4"]
"ignore": ["www", "v4", "tests"]
}

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

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(npm run typecheck:*)"
],
"deny": []
}
}

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

@@ -18,15 +18,15 @@ jobs:
repo-token: ${{ secrets.STALE_TOKEN }}
ascending: true
days-before-issue-close: 7
days-before-issue-stale: 365 # ~2 years
days-before-issue-stale: 365
days-before-pr-stale: -1
days-before-pr-close: -1
remove-issue-stale-when-updated: true
stale-issue-label: "stale?"
exempt-issue-labels: "roadmap,next,bug"
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you."
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If youre still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding!"
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
exempt-issue-labels: "roadmap,next"
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If youre still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding! (This is an automated message)"
operations-per-run: 300
- uses: actions/stale@v9
id: pr-state
name: "Mark stale PRs, close stale PRs"
@@ -36,10 +36,10 @@ jobs:
days-before-issue-close: -1
days-before-issue-stale: -1
days-before-pr-close: 7
days-before-pr-stale: 365 # PRs with no activity in over 90 days will be marked as stale
days-before-pr-stale: 365
remove-pr-stale-when-updated: true
exempt-pr-labels: "roadmap,nex,awaiting-approval,work-in-progress"
exempt-pr-labels: "roadmap,next,bug"
stale-pr-label: "stale?"
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you."
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding!"
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding! (This is an automated message)"
operations-per-run: 300

View File

@@ -41,7 +41,7 @@ jobs:
- name: Create Version PR or Publish to NPM
id: changesets
uses: changesets/action@v1.4.1
uses: changesets/action@v1
with:
commit: "chore(release): version packages"
title: "chore(release): version packages"

View File

@@ -8,6 +8,9 @@ jobs:
test:
runs-on: ubuntu-latest
name: pnpm test
env:
NEXT_PUBLIC_APP_URL: http://localhost:4000
NEXT_PUBLIC_V0_URL: https://v0.dev
steps:
- uses: actions/checkout@v3
with:
@@ -39,4 +42,7 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm build --filter=shadcn
- run: pnpm test

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

@@ -98,7 +98,7 @@ To run the CLI locally, you can follow the workflow:
1. Start by running the registry (main site) to make sure the components are up to date:
```bash
pnpm www:dev
pnpm v4:dev
```
2. Run the development script for the CLI:

2
apps/v4/.env.example Normal file
View File

@@ -0,0 +1,2 @@
NEXT_PUBLIC_V0_URL=https://v0.dev
NEXT_PUBLIC_APP_URL=http://localhost:4000

1
apps/v4/.gitignore vendored
View File

@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel

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,9 +14,11 @@ import { PageNav } from "@/components/page-nav"
import { ThemeSelector } from "@/components/theme-selector"
import { Button } from "@/registry/new-york-v4/ui/button"
const title = "Build your Component Library"
import { RootComponents } from "./components"
const title = "The Foundation for your Design System"
const description =
"A set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks. Open Source. Open Code."
"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."
export const dynamic = "force-static"
export const revalidate = false
@@ -51,14 +52,14 @@ export default function IndexPage() {
<div className="flex flex-1 flex-col">
<PageHeader>
<Announcement />
<PageHeaderHeading>{title}</PageHeaderHeading>
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
<PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions>
<Button asChild size="sm">
<Link href="/docs/installation">Get Started</Link>
</Button>
<Button asChild size="sm" variant="ghost">
<Link href="/blocks">Browse Blocks</Link>
<Link href="/docs/components">View Components</Link>
</Button>
</PageActions>
</PageHeader>
@@ -87,7 +88,7 @@ export default function IndexPage() {
/>
</section>
<section className="theme-container hidden md:block">
<CardsDemo />
<RootComponents />
</section>
</div>
</div>

View File

@@ -10,6 +10,7 @@ import { findNeighbour } from "fumadocs-core/server"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
import { DocsCopyPage } from "@/components/docs-copy-page"
import { DocsTableOfContents } from "@/components/docs-toc"
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
import { Badge } from "@/registry/new-york-v4/ui/badge"
@@ -102,12 +103,17 @@ export default async function Page(props: {
<h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
{doc.title}
</h1>
<div className="flex items-center gap-2 pt-1.5">
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:pt-1.5 sm:backdrop-blur-none">
<DocsCopyPage
// @ts-expect-error - revisit fumadocs types.
page={doc.content}
url={absoluteUrl(page.url)}
/>
{neighbours.previous && (
<Button
variant="secondary"
size="icon"
className="extend-touch-target size-8 shadow-none md:size-7"
className="extend-touch-target ml-auto size-8 shadow-none md:size-7"
asChild
>
<Link href={neighbours.previous.url}>
@@ -138,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>
@@ -160,7 +166,7 @@ export default async function Page(props: {
<MDX components={mdxComponents} />
</div>
</div>
<div className="mx-auto flex h-16 w-full max-w-2xl items-center gap-2 px-4 md:px-0">
<div className="mx-auto hidden h-16 w-full max-w-2xl items-center gap-2 px-4 sm:flex md:px-0">
{neighbours.previous && (
<Button
variant="secondary"
@@ -187,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

@@ -75,7 +75,7 @@ export function DataTable<TData, TValue>({
return (
<div className="flex flex-col gap-4">
<DataTableToolbar table={table} />
<div className="rounded-md border">
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (

View File

@@ -0,0 +1,32 @@
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
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
const slug = (await params).slug
const page = source.getPage(slug)
if (!page) {
notFound()
}
// @ts-expect-error - revisit fumadocs types.
const processedContent = processMdxForLLMs(page.data.content)
return new NextResponse(processedContent, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},
})
}
export function generateStaticParams() {
return source.generateParams()
}

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

@@ -1,7 +1,7 @@
import * as React from "react"
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { registryItemSchema } from "shadcn/registry"
import { registryItemSchema } from "shadcn/schema"
import { z } from "zod"
import { siteConfig } from "@/lib/config"

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

@@ -1,5 +1,5 @@
import * as React from "react"
import { registryItemFileSchema } from "shadcn/registry"
import { registryItemFileSchema } from "shadcn/schema"
import { z } from "zod"
import { highlightCode } from "@/lib/highlight-code"

View File

@@ -17,7 +17,7 @@ import {
Terminal,
} from "lucide-react"
import { ImperativePanelHandle } from "react-resizable-panels"
import { registryItemFileSchema, registryItemSchema } from "shadcn/registry"
import { registryItemFileSchema, registryItemSchema } from "shadcn/schema"
import { z } from "zod"
import { trackEvent } from "@/lib/events"

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-4",
"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

@@ -209,7 +209,7 @@ export function CardsPayments() {
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="rounded-md border">
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (

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

@@ -1,5 +1,5 @@
import * as React from "react"
import { registryItemSchema } from "shadcn/registry"
import { registryItemSchema } from "shadcn/schema"
import { z } from "zod"
import { highlightCode } from "@/lib/highlight-code"

View File

@@ -22,7 +22,7 @@ export function CodeCollapsibleWrapper({
<Collapsible
open={isOpened}
onOpenChange={setIsOpened}
className={cn("group/collapsible relative md:-mx-4", className)}
className={cn("group/collapsible relative md:-mx-1", className)}
{...props}
>
<CollapsibleTrigger asChild>

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,17 +31,20 @@ 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({
tree,
colors,
blocks,
navItems,
...props
}: DialogProps & {
tree: typeof source.pageTree
colors: ColorPalette[]
blocks?: { name: string; description: string; categories: string[] }[]
navItems?: { href: string; label: string }[]
}) {
const router = useRouter()
const isMac = useIsMac()
@@ -141,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}
@@ -149,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>
@@ -162,12 +168,45 @@ export function CommandMenu({
<DialogTitle>Search documentation...</DialogTitle>
<DialogDescription>Search for a command to run...</DialogDescription>
</DialogHeader>
<Command className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border">
<Command
className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border"
filter={(value, search, keywords) => {
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
return 1
}
return 0
}}
>
<CommandInput placeholder="Search documentation..." />
<CommandList className="no-scrollbar min-h-80 scroll-pt-2 scroll-pb-1.5">
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
No results found.
</CommandEmpty>
{navItems && navItems.length > 0 && (
<CommandGroup
heading="Pages"
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{navItems.map((item) => (
<CommandMenuItem
key={item.href}
value={`Navigation ${item.label}`}
keywords={["nav", "navigation", item.label.toLowerCase()]}
onHighlight={() => {
setSelectedType("page")
setCopyPayload("")
}}
onSelect={() => {
runCommand(() => router.push(item.href))
}}
>
<IconArrowRight />
{item.label}
</CommandMenuItem>
))}
</CommandGroup>
)}
{tree.children.map((group) => (
<CommandGroup
key={group.$id}
@@ -179,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-4"
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}
@@ -34,7 +36,7 @@ export function ComponentPreview({
if (type === "block") {
return (
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-4">
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
<Image
src={`/r/styles/new-york-v4/${name}-light.png`}
alt={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

@@ -1,33 +1,156 @@
"use client"
import { IconCheck, IconCopy } from "@tabler/icons-react"
import { IconCheck, IconChevronDown, IconCopy } from "@tabler/icons-react"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import {
Popover,
PopoverAnchor,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Separator } from "@/registry/new-york-v4/ui/separator"
export function DocsCopyPage({ page }: { page: string }) {
function getPromptUrl(baseURL: string, url: string) {
return `${baseURL}?q=${encodeURIComponent(
`Im looking at this shadcn/ui documentation: ${url}.
Help me understand how to use it. Be ready to explain concepts, give examples, or help debug based on it.
`
)}`
}
const menuItems = {
markdown: (url: string) => (
<a href={`${url}.md`} target="_blank" rel="noopener noreferrer">
<svg strokeLinejoin="round" viewBox="0 0 22 16">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.5 2.25H2.5C1.80964 2.25 1.25 2.80964 1.25 3.5V12.5C1.25 13.1904 1.80964 13.75 2.5 13.75H19.5C20.1904 13.75 20.75 13.1904 20.75 12.5V3.5C20.75 2.80964 20.1904 2.25 19.5 2.25ZM2.5 1C1.11929 1 0 2.11929 0 3.5V12.5C0 13.8807 1.11929 15 2.5 15H19.5C20.8807 15 22 13.8807 22 12.5V3.5C22 2.11929 20.8807 1 19.5 1H2.5ZM3 4.5H4H4.25H4.6899L4.98715 4.82428L7 7.02011L9.01285 4.82428L9.3101 4.5H9.75H10H11V5.5V11.5H9V7.79807L7.73715 9.17572L7 9.97989L6.26285 9.17572L5 7.79807V11.5H3V5.5V4.5ZM15 8V4.5H17V8H19.5L17 10.5L16 11.5L15 10.5L12.5 8H15Z"
fill="currentColor"
/>
</svg>
View as Markdown
</a>
),
v0: (url: string) => (
<a
href={getPromptUrl("https://v0.dev", url)}
target="_blank"
rel="noopener noreferrer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 147 70"
className="size-4.5 -translate-x-px"
>
<path d="M56 50.203V14h14v46.156C70 65.593 65.593 70 60.156 70c-2.596 0-5.158-1-7-2.843L0 14h19.797L56 50.203ZM147 56h-14V23.953L100.953 56H133v14H96.687C85.814 70 77 61.186 77 50.312V14h14v32.156L123.156 14H91V0h36.312C138.186 0 147 8.814 147 19.688V56Z" />
</svg>
<span className="-translate-x-[2px]">Open in v0</span>
</a>
),
chatgpt: (url: string) => (
<a
href={getPromptUrl("https://chatgpt.com", url)}
target="_blank"
rel="noopener noreferrer"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
fill="currentColor"
/>
</svg>
Open in ChatGPT
</a>
),
claude: (url: string) => (
<a
href={getPromptUrl("https://claude.ai/new", url)}
target="_blank"
rel="noopener noreferrer"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z"
fill="currentColor"
/>
</svg>
Open in Claude
</a>
),
}
export function DocsCopyPage({ page, url }: { page: string; url: string }) {
const { copyToClipboard, isCopied } = useCopyToClipboard()
const trigger = (
<Button
variant="secondary"
size="sm"
className="peer -ml-0.5 size-8 shadow-none md:size-7 md:text-[0.8rem]"
>
<IconChevronDown className="rotate-180 sm:rotate-0" />
</Button>
)
return (
<Tooltip>
<TooltipTrigger asChild>
<Popover>
<div className="bg-secondary group/buttons relative flex rounded-lg *:[[data-slot=button]]:focus-visible:relative *:[[data-slot=button]]:focus-visible:z-10">
<PopoverAnchor />
<Button
variant="outline"
variant="secondary"
size="sm"
className="h-8 pl-1.5 md:h-7 [&>svg]:size-3.5"
className="h-8 shadow-none md:h-7 md:text-[0.8rem]"
onClick={() => copyToClipboard(page)}
>
{isCopied ? <IconCheck /> : <IconCopy />} Copy Page
{isCopied ? <IconCheck /> : <IconCopy />}
Copy Page
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy as Markdown</p>
</TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild className="hidden sm:flex">
{trigger}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="shadow-none">
{Object.entries(menuItems).map(([key, value]) => (
<DropdownMenuItem key={key} asChild>
{value(url)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Separator
orientation="vertical"
className="!bg-foreground/10 absolute top-0 right-8 z-0 !h-8 peer-focus-visible:opacity-0 sm:right-7 sm:!h-7"
/>
<PopoverTrigger asChild className="flex sm:hidden">
{trigger}
</PopoverTrigger>
<PopoverContent
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-sm backdrop-blur-sm"
align="start"
>
{Object.entries(menuItems).map(([key, value]) => (
<Button
variant="ghost"
size="lg"
asChild
key={key}
className="*:[svg]:text-muted-foreground w-full justify-start text-base font-normal"
>
{value(url)}
</Button>
))}
</PopoverContent>
</div>
</Popover>
)
}

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>
)
}

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