Compare commits

...

257 Commits

Author SHA1 Message Date
shadcn
f1131db844 fix 2025-12-15 14:44:43 +04:00
Gravityy
b15d7e8221 feat : add animbits to trusted registries (#8910)
* Add @animbits registry URL to registries.json and directory.json

* Add @animbits registry URL to registries.json and directory.json

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-15 14:38:24 +04:00
shadcn
46e3c26a6e chore: registry build 2025-12-15 14:33:37 +04:00
Bundui.io
f36e25f703 feat(registry): Add BundUI Components to the registry index (#8473)
* feat(registry): Add BundUI Components to the registry index

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-15 14:19:50 +04:00
MUDAVATH KUMAR
55f5d1c7cc feat: add Ein UI to registry directory (#9068)
Add Ein UI registry entry with:
- Registry URL: https://ui.eindev.ir/r/{name}.json
- Homepage: https://ui.eindev.ir
- Description: Beautiful, responsive Shadcn components with frosted glass morphism
- Custom gradient logo with glass theme

Resolves: #9048

Co-authored-by: Clacky <develop@clacky.ai>
2025-12-15 13:44:50 +04:00
Tham Kei Lok
db19605996 feat: add @8starlabs-ui to directory.json and registries.json (#8976) 2025-12-15 13:44:24 +04:00
Agustín Mayol
40012adb14 feat: add @optics to directory.json and registries.json (#8971) 2025-12-15 13:44:10 +04:00
Ali Hussein
ad8104e473 feat: add @cardcn registry #8902 (#8919) 2025-12-15 13:43:52 +04:00
Gildas Garcia
5fb0c4d19a Add shadcn-admin-kit to the registry index (#9018)
This PR adds https://marmelab.com/shadcn-admin-kit to the trusted registries.
2025-12-15 13:43:01 +04:00
Bruno Pérez
31c86f9fd5 feat: add @manifest registry (#9010) (#9011) 2025-12-15 13:42:11 +04:00
Gxuri
aad175ff87 feat: add @skiper-ui to directory.json (#9007)
* feat: add @skiper-ui to directory.json

* fix: update URL format for @skiper-ui in directory.json
2025-12-15 13:41:48 +04:00
Muskri
081c91c461 feat: add new registry entry for @pureui with logo and description (#8911) 2025-12-15 13:40:01 +04:00
Dominik K.
7dbf3688fb fix: rename hugeicons dot icon (#9033)
Renames the hugeicons icon from MoreHorizontalIcon to the actual component name MoreHorizontalCircle01Icon
2025-12-15 13:35:56 +04:00
Manuel
99ad18b389 fix: remove duplicate 'text-left' class from SelectValue component (#9045)
* fix: remove duplicate 'text-left' class from SelectValue component

* fix: select for base

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-15 13:33:27 +04:00
Junaid Anjum
fabb886de9 minor typo in the recent changelog (#9024) 2025-12-15 13:29:04 +04:00
ateeb a.
4b561cf050 fix:select color-format component and color copy-to-clipboard (#9056)
* fix:Nuqs adapter scope, select color-format component and color copy-to-clipboard

* chore: remove changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-15 13:27:10 +04:00
Mert
0c2373f592 fix(lint): resolve @typescript-eslint/no-unused-vars warns in combobox and scroll-area (#9060) 2025-12-15 13:24:30 +04:00
François Best
ff42c27d41 refactor: nuqs APIs (#9055)
* ref: replace cache with loader + inferParserType

* ref: expose useDesignSystemSearchParams hook

* ref: use loader & exported hook to simplify iframe sync

* ref: remove unused code

* fix: params.size is non-nullable

* ref: move items shallow option to the parser level

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-15 13:19:32 +04:00
github-actions[bot]
075b6aef97 chore(release): version packages (#9057)
* chore(release): version packages

* chore(release): version packages

* fix

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-12-14 02:26:21 +04:00
Mert
f4e0f671de fix: add missing options to schema.json (#9051)
* Add new style options to schema.json

The schema at ui.shadcn.com/schema.json is missing newly introduced style variants used by ui.shadcn.com/create.

This causes JSON schema validation errors when generating configs with the new styles.

This updates the style enum to include all currently supported style combinations.

* Add missing menuColor and menuAccent options to schema.json
2025-12-14 02:18:41 +04:00
shadcn
d3156c09ae fix(shadcn): resolver for url (#9054) 2025-12-14 02:16:26 +04:00
KapishDima
46bf4a0f06 Fix Base UI pagination crash (#9027)
* fix(pagination): Added nativeButton=false for a PaginationLink

* feat(registry): run registry:build

---------

Co-authored-by: dev_wandry <dima.development@wandry.com.ua>
2025-12-13 00:40:15 +04:00
shadcn
b61b718727 fix: add use-mobile to registry 2025-12-13 00:20:24 +04:00
shadcn
ee9b6b36ec fix: typo 2025-12-12 21:47:37 +04:00
shadcn
33de348d41 fix 2025-12-12 21:43:50 +04:00
shadcn
edcc96fc73 fix 2025-12-12 21:13:29 +04:00
github-actions[bot]
ef90a97e72 chore(release): version packages (#9023)
* chore(release): version packages

* fix

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-12-12 21:04:25 +04:00
shadcn
86d9b00084 chore: update deps (#9022)
* feat: init

* fix

* fix

* fix

* feat

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: implement icons

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: update init command

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: dialog

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add registry:base item type

* feat: rename frame to canva

* fix

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fi

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add all colors

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add outfit font

* fix

* fix

* fix

* fix

* fix

* chore: changeset

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-12-12 21:01:44 +04:00
shadcn
672f845322 Merge branch 'main' of github.com:shadcn-ui/ui
# Conflicts:
#	pnpm-lock.yaml
2025-12-12 17:40:57 +04:00
shadcn
d01074deed chore: update 2025-12-12 17:40:21 +04:00
dependabot[bot]
321ceaf1c4 chore(deps): bump next from 16.0.7 to 16.0.9 in /templates/monorepo-next (#9014)
Bumps [next](https://github.com/vercel/next.js) from 16.0.7 to 16.0.9.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.0.7...v16.0.9)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.0.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 17:39:03 +04:00
vercel[bot]
32a972f4ce Fix React Server Components CVE vulnerabilities (#9016)
Updated dependencies to fix Next.js and React CVE vulnerabilities.

The fix-react2shell-next tool automatically updated the following packages to their secure versions:
- next
- react-server-dom-webpack
- react-server-dom-parcel  
- react-server-dom-turbopack

All package.json files have been scanned and vulnerable versions have been patched to the correct fixed versions based on the official React advisory.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
2025-12-12 08:37:44 +04:00
github-actions[bot]
d28e02be1b chore(release): version packages (#8998)
* chore(release): version packages

* 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-12-09 21:25:59 +04:00
shadcn
6699158a22 fix: handling of base style for add command (#8997)
* fix: handling of base style for add command

* chore: changeset

* fix: shadow config
2025-12-09 20:55:58 +04:00
Pasquale Vitiello
142cd8ef13 Prevent duplicate keyframes when adding components (#8993)
* fix: prevent duplicate keyframes when adding components

- Check for existing keyframes in @theme inline before adding
- Replace existing keyframes instead of creating duplicates
- Add test to verify keyframe replacement behavior

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-09 13:01:34 +04:00
shadcn
bdedce2750 feat: add start-app (#8992) 2025-12-09 01:54:22 +04:00
shadcn
4cb283d68e fix 2025-12-09 00:43:17 +04:00
shadcn
480a6cdb37 fix: layout 2025-12-09 00:40:19 +04:00
shadcn
8ba883738e chore: update monorepo-next 2025-12-09 00:31:33 +04:00
shadcn
b022c24825 chore: remove pnpm lock file 2025-12-09 00:27:17 +04:00
shadcn
3587477865 feat: add vite-app template (#8989) 2025-12-08 23:03:13 +04:00
Tommy Lundy
05143a80e6 Add @doras-ui registry (#8966)
* Add @doras-ui registry URL to registries.json

* Add @doras-ui component block details to directory
2025-12-08 23:01:20 +04:00
Jarrod Watts
728d2003b7 Add new component registry entries for @abstract (#8962) 2025-12-08 23:00:58 +04:00
Jarrod Watts
12c9e6b0b5 Add @abstract registry URL to registries.json (#8536)
Added new registry entry for @abstract.

Co-authored-by: shadcn <m@shadcn.com>
2025-12-05 15:39:49 +04:00
I Plan Websites
56cd757c45 @ai-blocks registry (#8956)
* Add AI Blocks registry URL to registries.json

* Add AI components for web to directory.json
2025-12-05 15:37:53 +04:00
Ella
9eb784054f feat: add @lucide-animated to directory.json and registries.json (#8937) 2025-12-05 15:37:41 +04:00
Hin
824577692b feat: add tour to registries and directory (#8917)
Co-authored-by: shadcn <m@shadcn.com>
2025-12-05 15:37:21 +04:00
Antonio Brandao
6be68df08c Add @abui registry to directory and index (#8908) 2025-12-05 15:35:50 +04:00
Admin Mart
cc48808a0d feat: add my custom registry (#8879)
Co-authored-by: wrappixelTeam <wrappixelteam.2016@gmail.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-12-05 15:35:26 +04:00
Admin Mart
a56b3720d1 feat/added my custom registry (#8904)
Co-authored-by: wrappixelTeam <wrappixelteam.2016@gmail.com>
2025-12-05 15:34:33 +04:00
dependabot[bot]
334db11234 chore(deps): bump next from 16.0.0 to 16.0.7 (#8954)
Bumps [next](https://github.com/vercel/next.js) from 16.0.0 to 16.0.7.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.0.0...v16.0.7)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.0.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-04 16:24:46 +04:00
Luis Llanes
8a5027a0cd feat: add @shadcraft to directory.json and registries.json (#8913) 2025-12-01 00:44:28 +04:00
github-actions[bot]
803206305d chore(release): version packages (#8665)
* chore(release): version packages

* chore(release): version packages

* chore: lock

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-11-27 21:16:23 +04:00
shadcn
d0fb73ac0e fix: do not install baseStyle when adding registry:theme (#8900) 2025-11-27 21:13:56 +04:00
shadcn
62218c1c0c feat: update color value detection for cssVars (#8901) 2025-11-27 21:12:31 +04:00
Aditya Mathur
dd1563d57d fix: update author links in documentation for Drawer and Sonner components (#8881) 2025-11-27 13:38:26 +04:00
Amartya Singh
0538384860 Add @nexus-elements registry URL to registries.json and directory.json (#8797)
* Add @nexus-elements registry URL to registries.json

* feat: updated directory.json with nexus-elements

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-25 11:08:47 +04:00
shadcn
d43b437abc fix 2025-11-25 10:59:48 +04:00
Burhanuddin S. Tinwala
8fbfacd243 docs: fix typo 'mcpServers' to 'servers' in mcp server setup documentation for vs code (#8864) 2025-11-24 12:51:06 +04:00
Neha Prasad
778cee31ee feat: add @ui-layouts registry to directory (#8878) 2025-11-24 12:49:24 +04:00
Wolfr
73d8b8a817 docs - Move free kits to the top (#8639)
* docs - Move free kits to the top

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-24 12:48:21 +04:00
Phuc Bui
55ab069aca feat: add @phucbm in trusted registries (#8830)
* feat: add @phucbm in trusted registries

* Add @phucbm to registry directory

* fix: change svg quotes from double to single
2025-11-19 16:58:43 +04:00
Ajay Patel
c39925a9be Added shadcn/studio UI Kit to figma docs (#8852)
docs: Added shadcn/studio UI Kit to Figma paid section
2025-11-19 16:48:09 +04:00
shadcn
51179ccd64 fix: directory 2025-11-19 12:26:52 +04:00
Hin
dcfa05e392 feat: add shadcn-map to directory (#8832) 2025-11-19 12:20:20 +04:00
Aryan
541f55df04 feat: add @gaia registry entry to directory and registries.json (#8836)
Co-authored-by: shadcn <m@shadcn.com>
2025-11-19 12:19:53 +04:00
Edu Calvo
69010e0230 feat: add new registry entry for @smoothui with logo and description (#8837) 2025-11-19 12:18:35 +04:00
Rushil
a8025c866e feat: add moleculeui to registries and directories (#8838)
Co-authored-by: shadcn <m@shadcn.com>
2025-11-19 12:18:14 +04:00
Kaiyu Hsu
6e34ec7280 feat: add @uicapsule to registry (#8848)
Co-authored-by: shadcn <m@shadcn.com>
2025-11-19 12:16:42 +04:00
Arif Hossain
10ccb244a1 feat: add @commercn in registry directory (#8842) 2025-11-19 12:15:53 +04:00
Akshay Joshi
16fdb07ccc feat: added @crenspire/glass-ui in trusted registries and directory (#8831)
* feat: added @glass-ui in trusted registries and directory

* fix: svg logo not displayed properly
2025-11-19 12:14:41 +04:00
Moumen Soliman
49da1fae79 Add @uitripled registry (#8834)
* Add @uitripled registry

* Update registries.json

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-19 12:13:09 +04:00
Sepehr Soheili
a2244d42f7 docs: fix typo in documentation (#8793) 2025-11-17 20:06:32 +04:00
Brendan Dash
c2075e2a8b fix: typo (#8800) 2025-11-17 20:06:04 +04:00
Anthony Shew
dd2d8d7ead fix: dependencies for Bun in monorepo-next (#8791) 2025-11-13 09:42:31 +04:00
Akash Moradiya
b6a93b7ec6 feat: add @shadcnui-blocks to registry directory (#8763) 2025-11-12 09:39:21 +04:00
shadcn
4899d3f0da fix: minor directory updates 2025-11-10 15:35:21 +04:00
Pawan Kumar
3d04cb099a Add new registry entry for @taki (#8758)
* add taki ui in registry

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-10 15:27:56 +04:00
François Best
cde343916c ref: refactor nuqs usage in directory search (#8756)
* chore: fix typo

* ref: query is non-nullable (as it has a default value)

* ref: leverage nuqs' clearOnDefault behaviour
2025-11-10 15:27:06 +04:00
LN
c877df07b8 feat: add @square-ui in registries (#8761)
* feat: add new registry entry for @square-ui

* feat: add new registry entry for @square-ui with logo and description
2025-11-10 15:17:01 +04:00
DimaDevelopment
65e5c1c3cf Add search input for Directory list (#8673)
* feat(directory): Added directory search input

* feat(directory): Added nuqs for a search state management. Refactor searchFn - includes the description in the search criteria

* fear(directory): Added default query value. Added useQueryState limitUrlUpdates 250ms

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-10 12:24:15 +04:00
Anvarys
8a7f05f670 fix: correct typos in docs/components/chart (#8750) 2025-11-10 11:34:53 +04:00
Miracle Onyenma
db004ce4c0 Add new registry entry for @aevr (#8726)
* Add new registry entry for @aevr

Would like to add Aevr UI, a small collection of focused, production‑ready components and primitives for React/Next.js projects—built on shadcn/ui and complementary libraries—to the shadcn registry. It emphasizes practical, purpose‑specific building blocks with consistent design tokens, accessibility, and copy‑paste examples tailored for real product use.

Website: https://ui.aevr.space 

Repo: https://github.com/aevrhq/ui

* Add new component library entry for @aevr in registry directory

* Update logo SVG format in directory.json

* fix: logo color

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-10 11:30:16 +04:00
shadcn
e23698a897 feat: add tracking for directories 2025-11-10 11:27:10 +04:00
preet
5813ef20a3 Add new registry entry for @hextaui (#8715)
* Add new registry entry for @hextaui

* feat: add new registry entry for @HextaUI

* Update directory.json

Co-authored-by: shadcn <m@shadcn.com>

* fix: update directory

* fix: logo

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-11-10 11:20:23 +04:00
Arif Hossain
515024b69e feat: add @commercn in trusted registries (#8751) 2025-11-10 11:12:35 +04:00
Shaban
f7284c5cc3 feat(directory): update the @efferd logo (#8733) 2025-11-10 10:43:54 +04:00
Harshul
c02d00aafc fix(shadcn): Restore two-finger navigation on macOS by adjusting overscroll behavior (#8714)
* fix(shadcn): Tow finger navigation on macOS

* Add smooth scrolling to html element
2025-11-06 10:04:15 +04:00
Bassim Shahidy
df497ad236 feat: add assistant-ui to directory.json (#8720)
* feat: add assistant-ui to directory.json

* update description

* desc
2025-11-06 10:01:33 +04:00
shadcn
1e468e33ac feat: add more registries 2025-11-05 10:15:32 +04:00
dependabot[bot]
ff91c31a71 chore(deps): bump @radix-ui/react-toggle from 1.1.9 to 1.1.10 (#8702)
Bumps [@radix-ui/react-toggle](https://github.com/radix-ui/primitives) from 1.1.9 to 1.1.10.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-toggle"
  dependency-version: 1.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-05 09:51:45 +04:00
dependabot[bot]
25d6a18f6f chore(deps): bump @radix-ui/react-tooltip from 1.2.7 to 1.2.8 (#8701)
Bumps [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives) from 1.2.7 to 1.2.8.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-05 09:51:11 +04:00
dependabot[bot]
c0309510b6 chore(deps): bump @radix-ui/react-accordion from 1.2.11 to 1.2.12 (#8700)
Bumps [@radix-ui/react-accordion](https://github.com/radix-ui/primitives) from 1.2.11 to 1.2.12.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-accordion"
  dependency-version: 1.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-05 09:50:52 +04:00
dependabot[bot]
a3a1574668 chore(deps): bump @commitlint/cli from 17.8.1 to 20.1.0 (#8698)
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 17.8.1 to 20.1.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.1.0/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-05 09:50:29 +04:00
shadcn
65d581ea5a chore: remove console 2025-11-04 15:19:45 +04:00
shadcn
fdf80a1d49 fix: gh link 2025-11-04 09:21:43 +04:00
Ajay Patel
86c494c452 feat: add new entry for @shadcn-studio in directory.json (#8692)
* feat: add new entry for @shadcn-studio in directory.json

* [fix]: for @shadcn-studio in directory.json, Updated logo SVG to use CSS variables

Updated logo SVG to use CSS variables for colors.Refactor logo SVG to use CSS variables
2025-11-03 12:48:36 +04:00
Paul Burke
eb158686b9 feat: adds lens-blocks to the registry index (#8687) 2025-11-03 11:45:57 +04:00
shadcn
134cd46edb feat: add hsl and color indicators to theme (#8691) 2025-11-03 11:25:06 +04:00
shadcn
47b0efb20c docs: update tanstack start 2025-11-03 10:46:35 +04:00
shadcn
bd4d09d33e fix 2025-11-03 10:30:18 +04:00
Dylan
14d6265580 docs: fix typos in Empty (#8675) 2025-11-03 08:57:19 +04:00
Ali Imam
68805d29a1 Update directory.json (#8682) 2025-11-03 08:56:03 +04:00
ocavue
c100d5841a feat: add prosekit to directory (#8677) 2025-11-02 15:00:40 +04:00
Ali Imam
7a71da5218 Update directory.json (#8671)
* Update directory.json

* Update registries.json
2025-11-02 14:51:58 +04:00
Mudunuri bhaskara karthikeya varma
e18902039a adding @eldoraui to directory.json (#8670) 2025-10-31 21:54:20 +04:00
Emmanuel Odii
559af6c245 feat(registry): add paykit to directory (#8654)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-31 12:24:15 +04:00
Alex Paduraru
8971be484f feat: add @creative-tim to registries.json, directory.json (#8651)
* feat: add @creative-tim to registries.json, directory.json

* fix: logo color

---------

Co-authored-by: Alexandru Paduraru <axelut@yahoo.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-31 12:19:31 +04:00
Tommy D. Rossi
ad6a3c6367 Fix utils import transform when workspace alias does not start with @ (#7557)
* Fix nested src folder for utils import

* remove .only

* Update packages/shadcn/src/utils/transformers/transform-import.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* check for empty utils

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-31 12:07:02 +04:00
dependabot[bot]
befa56b5be chore(deps): bump @faker-js/faker from 8.4.1 to 10.1.0 (#8520)
Bumps [@faker-js/faker](https://github.com/faker-js/faker) from 8.4.1 to 10.1.0.
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v8.4.1...v10.1.0)

---
updated-dependencies:
- dependency-name: "@faker-js/faker"
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-31 11:15:26 +04:00
dependabot[bot]
5d1770e36d chore(deps): bump @radix-ui/react-navigation-menu from 1.2.13 to 1.2.14 (#8519)
Bumps [@radix-ui/react-navigation-menu](https://github.com/radix-ui/primitives) from 1.2.13 to 1.2.14.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-navigation-menu"
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-31 11:14:27 +04:00
dependabot[bot]
653521725a chore(deps): bump jotai from 2.13.1 to 2.15.0 (#8521)
Bumps [jotai](https://github.com/pmndrs/jotai) from 2.13.1 to 2.15.0.
- [Release notes](https://github.com/pmndrs/jotai/releases)
- [Commits](https://github.com/pmndrs/jotai/compare/v2.13.1...v2.15.0)

---
updated-dependencies:
- dependency-name: jotai
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-31 11:14:12 +04:00
shadcn
7c0618bf43 feat: add new registries (#8661) 2025-10-31 10:39:29 +04:00
shadcn
854641cea1 fix 2025-10-30 12:51:15 +04:00
shadcn
3a72007f61 fix 2025-10-30 12:04:55 +04:00
shadcn
6b53b238fb fix: spacing 2025-10-30 11:59:50 +04:00
shadcn
b398fea304 feat: add dir action (#8647)
* feat: add mcp config

* feat

* fix
2025-10-30 11:53:24 +04:00
shadcn
f22174a77f feat: add mcp config (#8641) 2025-10-29 22:56:24 +04:00
shadcn
c9a39f1007 docs: update README 2025-10-29 21:38:18 +04:00
shadcn
a8ad21f81f fix: svgs 2025-10-29 21:37:15 +04:00
shadcn
504503c638 feat: add new registries 2025-10-29 21:20:04 +04:00
Pasquale Vitiello
f8df5c95cb docs: update coss logo (#8631) 2025-10-29 20:57:42 +04:00
shadcn
2bfc1c82ba chore: deprecate www (#8629)
* chore: deprecate www

* chore: updates

* fix
2025-10-29 20:50:55 +04:00
shadcn
84bd724d97 feat: refactor registry (#8598)
* feat: refactor registry

* fix: remove components

* refactor: getActiveStyle

* fix: prettier in build-registry

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

* fix

* Update apps/v4/scripts/build-registry.mts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix

* Update apps/v4/scripts/build-registry.mts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update apps/v4/components/block-viewer.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-29 15:07:56 +04:00
shadcn
39fdf94550 feat: add svgl 2025-10-29 14:58:25 +04:00
shadcn
08479cc3db feat: add wigggle-ui 2025-10-29 13:55:50 +04:00
shadcn
02d5ce85ec feat: upgrade to Next.js 16 (#8615)
* feat: upgrade to Next.js 16

* chore: deps

* fix

* fix

* fix

* fix: workaround zod 4 for now

* fix

* fix: copy button

* fix: update apps/v4/hooks/use-is-mac.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix

* fix: remove

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-29 13:37:41 +04:00
shadcn
c0329c86b9 feat: add retroui and reui 2025-10-29 09:53:25 +04:00
shadcn
3b1491f908 fix: typo 2025-10-29 09:30:49 +04:00
shadcn
ca4c1c43ec feat: update registry directory 2025-10-29 00:15:20 +04:00
shadcn
1e840eb53c feat: update directory 2025-10-29 00:05:03 +04:00
shadcn
96ac92e63f fix 2025-10-29 00:00:20 +04:00
shadcn
e11546e692 fix: examples page 2025-10-28 23:56:15 +04:00
shadcn
0b4d62f95c chore: rebuild registry 2025-10-28 22:59:12 +04:00
shadcn
dae80dad65 fix: password field 2025-10-28 22:58:17 +04:00
shadcn
abc09809e8 feat: add blocks to directory 2025-10-28 22:56:29 +04:00
shadcn
8a40fe0ead feat: update registries 2025-10-28 22:45:06 +04:00
shadcn
b3ab304a00 fix: minor styles updates 2025-10-28 21:34:31 +04:00
Elliot Sutton
bb45fd83c3 fix(v4): correction of the Animate UI logo (#8608)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-28 21:03:19 +04:00
Chisom Uma
84678ee1c0 Add new registry entry for @chisom-ui (#8601) 2025-10-28 21:01:08 +04:00
shadcn
33ffb0419c Merge branch 'main' of github.com:shadcn-ui/ui
# Conflicts:
#	apps/v4/registry/directory.json
2025-10-28 20:52:28 +04:00
shadcn
a2f6c031e2 feat: add new registries 2025-10-28 20:49:49 +04:00
shadcn
ac098d8cf0 feat: add new registries (#8600) 2025-10-28 18:28:06 +04:00
shadcn
8160610410 Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-28 11:26:17 +04:00
shadcn
c7901e3a41 chore: update validate-registries script 2025-10-28 11:26:04 +04:00
shadcn
d73ac361b3 feat: add registry directory (#8574)
* feat: add registry directory

* fix: lint

* feat: add more registries

* feat: add nuqs to directory

* feat: add shadcndesign

* feat: add more registries

* feat: add new registries

* chore: remove hooks
2025-10-28 11:25:25 +04:00
François Best
ebad2901ce feat: add nuqs to registries.json (#8579) 2025-10-27 11:16:18 +04:00
shadcn
4f617d59b8 Fix description formatting in registry_directory.yml 2025-10-26 16:22:48 +04:00
shadcn
ed0e103bd6 Refactor checklist validations in registry_directory.yml
Removed individual required validations from checklist items and added a single required validation for the entire checklist.
2025-10-26 16:22:17 +04:00
shadcn
9cab0c9b18 chore: add registry directory issue template 2025-10-26 16:18:46 +04:00
DimaDevelopment
d80e084814 feat: add @wandry-ui to registries.json (#8566) 2025-10-26 15:28:42 +04:00
shadcn
efcf9728c2 feat: add docs for cell size 2025-10-26 14:53:50 +04:00
Lakshya Thakur
8835bacc8b fix: use calendar-05 blocks instead of calendar-02 for range (#8029) 2025-10-26 14:47:06 +04:00
Ian Thorslund
f2556d2386 fix(components): Fix left radius of days when weeks are shown in range calendar (#8570)
* fix(components): Fix left radius of days when weeks are shown in range calendar

* feat: update block to show range

* chore: rebuild registry

* docs: add changelog

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-26 14:44:24 +04:00
albertasaftei
75a0000075 Add Pixelact UI registry to registries.json (#8301) 2025-10-25 09:40:38 +04:00
Dillion Verma
ac306c60f5 feat: add magic ui pro registry (#8302)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-24 11:19:25 +04:00
shadcn
5e2ef1f8bd docs: updates 2025-10-24 10:56:23 +04:00
shadcn
7d9b8aefff Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-24 10:44:39 +04:00
shadcn
58208e3802 fix: minor updates 2025-10-24 10:44:21 +04:00
Shaban
a16a77446a feat: rename @efferd-ui -> @efferd (#8557) 2025-10-24 10:37:47 +04:00
github-actions[bot]
39032bb390 chore(release): version packages (#8551)
* chore(release): version packages

* 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-10-23 22:12:05 +04:00
shadcn
d7e0dc3ec8 feat(shadcn): middleware to proxy (#8555)
* feat: implement getFrameworkVersion

* feat(shadcn): add transformNext transformer

* feat(shadcn): rename

* chore: update

* chore: changeset

* fix

* fix: small refactor
2025-10-23 22:00:55 +04:00
shadcn
6bddba986d feat(shadcn): add next 16 to init (#8550)
* feat: add next 16 init

* chore: changeset

* fix: tests

* fix
2025-10-23 17:20:45 +04:00
shadcn
b70059b25b docs: add react-19 docs back 2025-10-23 17:14:09 +04:00
shadcn
37bc2eec1f feat: update code block for component preview (#8549) 2025-10-23 16:24:44 +04:00
mw10013
bb048fb532 feat: add @oui to registries.json (#8541) 2025-10-22 21:35:55 +04:00
Alejandro Wurts
9c373dbd27 feat: add @simple-ai to trusted registries (#8539) 2025-10-22 21:35:03 +04:00
shadcn
d75b092c61 docs: remove legacy docs 2025-10-22 07:02:23 +04:00
Gihan Rangana
be49662bf5 shadix-ui registry added (#8505) 2025-10-21 19:16:23 +04:00
FadilRumasoreng
b2b2e3fc98 docs (security) : add clickable link to security reporting section (#8525)
* docs (security) : add clickable link to security reporting section

* fix(docs): use markdown link format for security section
2025-10-21 17:41:45 +04:00
Neeraj Dalal
188b746074 refactor(field): no duplicate errors (#8514)
* chore: tweaks > field.tsx

apps/v4/registry/new-york-v4/ui/field.tsx

* chore: tweaks > field.tsx

apps/v4/registry/new-york-v4/ui/field.tsx

* chore: tweaks > field.json field.json

apps/www/public/r/styles/new-york-v4/field.json
apps/v4/public/r/styles/new-york-v4/field.json
2025-10-21 16:02:52 +04:00
Joshua Chung
6f093a0f3f Added ha-components registry to registries.json (#8531) 2025-10-21 15:40:03 +04:00
Matt Wierzbicki
f18f1eaff7 Added shadcndesign.com registry to registries.json (#8516)
* Added shadcndesign.com registry to registries.json

* Update registries.json with open source shadcndesign registry
2025-10-21 14:33:40 +04:00
shadcn
9ac1b5c0a5 feat: add native select (#8528)
* feat: add native select

* fix: width
2025-10-21 11:48:25 +04:00
shadcn
f63b70b413 feat: implement search via fumadocs (#8523)
* feat: implement search

* fix: update message when searching
2025-10-21 10:26:29 +04:00
shadcn
54e725d986 feat(toggle-group): add spacing props 2025-10-20 21:28:40 +04:00
shadcn
62dbad36bb fix: theme-customizer 2025-10-20 20:42:55 +04:00
shadcn
a707424fa2 fix: front page blocks 2025-10-20 20:39:01 +04:00
shadcn
e2bfa6bd85 feat(badge): make rounded-full (#8518) 2025-10-20 20:24:45 +04:00
preet
6292464d90 docs(calendar): add timezone fix for date selection offset issue (#8515)
Added a new section to the Calendar documentation explaining how to fix the issue where selecting a date highlights the previous day. The section includes an example showing how to detect the user’s local timezone and pass it as a timeZone prop to the Calendar component to resolve the date offset problem.
2025-10-20 13:17:58 +00:00
shadcn
6617167d6f Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-20 16:05:21 +04:00
shadcn
ca28857d40 fix: themes 2025-10-20 16:05:14 +04:00
Rakesh
343bc941b1 Fix/theme code mismatch (#8368)
* fix/replaced stale theme colors

* fix:missing chart colors added
2025-10-20 15:56:57 +04:00
Pasquale Vitiello
c9311f26fa feat: add @coss to registries.json (#8490) 2025-10-17 15:01:04 +04:00
Ivan Vasilov
4e0871f426 Add supabase registry to trusted registries. (#8161) 2025-10-17 15:00:51 +04:00
shadcn
cb769b7059 fix: navigation menu demo on mobile (#8488) 2025-10-16 14:44:27 +04:00
Dylan Tientcheu
93037dca94 feat: add @algolia in trusted registries (#8485) 2025-10-16 13:05:51 +04:00
github-actions[bot]
ed9d5939e6 chore(release): version packages (#8479)
* chore(release): version packages

* fix

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-10-15 22:55:13 +04:00
shadcn
b52ec12f1e fix(shadcn): universal items handling (#8478)
* fix(shadcn): universal items handling

* chore: add changeset
2025-10-15 22:27:12 +04:00
Igor S. Zizinio
2ab9bff4bb Fix formatting of Badge variant prop in documentation (#8477) 2025-10-15 22:09:06 +04:00
Shaban Haider
2f6b51fa0a feat: add @efferd-ui in trusted registries (#8474) 2025-10-15 22:03:39 +04:00
shadcn
8a4764ed91 chore: registry build 2025-10-15 12:05:03 +04:00
Chisom Uma
e934d4645b feat(registry): Add Austin UI Components to the registry index (#8456)
* feat(registry): Add Austin UI Components to the registry index

* Fix JSON syntax for @austin-ui registry URL

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-10-15 12:02:40 +04:00
shadcn
08b8e499d8 chore: update registries.json (#8470) 2025-10-15 11:52:43 +04:00
shadcn
69402b3579 ci: update deprecated to ignore www/package.json 2025-10-15 11:39:53 +04:00
shadcn
679c852254 Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-15 11:37:26 +04:00
github-actions[bot]
d478412e44 chore(release): version packages (#8453)
* chore(release): version packages

* 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-10-15 11:37:01 +04:00
shadcn
d5c8a25150 Merge branch 'main' of github.com:shadcn-ui/ui 2025-10-15 11:30:53 +04:00
Benjamin
26433a651c Added react-market registry (#8346)
Co-authored-by: shadcn <m@shadcn.com>
2025-10-15 11:30:43 +04:00
shadcn
c3da716e94 debug: ci 2025-10-15 11:30:24 +04:00
dependabot[bot]
b2572d0287 chore(deps): bump tj-actions/changed-files in /.github/workflows (#8466)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 44 to 46.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v44...v46)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: '46'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 11:30:03 +04:00
shadcn
b83f042416 fix: actions 2025-10-15 11:26:28 +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
6124 changed files with 208123 additions and 21626 deletions

View File

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

View File

@@ -2,8 +2,12 @@
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(npm run typecheck:*)"
"Bash(npm run typecheck:*)",
"Bash(ls:*)",
"Bash(cat:*)",
"WebSearch",
"WebFetch(domain:github.com)"
],
"deny": []
}
}
}

View File

@@ -0,0 +1,63 @@
name: Add registry to directory
description: Add your registry to the directory
title: "[Registry Directory]: "
labels: ["registry", "directory"]
assignees: []
body:
- type: input
id: name
attributes:
label: Name
description: The name of your registry. This is also the namespace.
placeholder: e.g., "@acme"
validations:
required: true
- type: input
id: url
attributes:
label: URL
description: The URL to your registry index. Use {name} placeholder.
placeholder: https://ui.acme.com/r/{name}.json
validations:
required: true
- type: input
id: homepage
attributes:
label: Homepage
description: The URL to your registry homepage. This is where users can browse your registry.
placeholder: https://ui.acme.com
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Briefly describe what is your registry and what type of components or code it distributes.
placeholder:
validations:
required: true
- type: textarea
id: logo
attributes:
label: Logo
description: Add your SVG logo here.
placeholder:
validations:
required: true
- type: checkboxes
id: requirements
attributes:
label: Checklist
description: Verify that your registry meets the following requirements.
options:
- label: The registry must be open source and publicly accessible.
- label: The registry must be a valid JSON file that conforms to the [registry schema](https://ui.shadcn.com/docs/registry/registry-json) specification.
- label: The `files` array, if present on your registry items, must NOT include a `content` property.
- label: I've attached a square SVG logo to this issue
validations:
required: true

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

@@ -0,0 +1,78 @@
name: Deprecated
on:
pull_request_target:
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:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
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/') &&
file !== 'apps/www/package.json'
);
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

@@ -7,6 +7,11 @@ on:
types: [labeled]
branches:
- main
permissions:
id-token: write
contents: read
jobs:
prerelease:
if: |
@@ -18,7 +23,7 @@ jobs:
steps:
- name: Checkout Repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -27,23 +32,22 @@ jobs:
with:
version: 9.0.6
- name: Use Node.js 18
uses: actions/setup-node@v3
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Install NPM Dependencies
run: pnpm install
- name: Modify package.json version
run: node .github/version-script-beta.js
- name: Authenticate to NPM
run: echo "//registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN" >> packages/shadcn/.npmrc
env:
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
- name: Publish Beta to NPM
run: pnpm pub:beta

View File

@@ -23,11 +23,11 @@ jobs:
with:
version: 9.0.6
- name: Use Node.js 18
- name: Use Node.js 20
uses: actions/setup-node@v3
with:
version: 9.0.6
node-version: 18
node-version: 20
cache: "pnpm"
- name: Install NPM Dependencies

View File

@@ -19,7 +19,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
- uses: pnpm/action-setup@v4
name: Install pnpm

2
.gitignore vendored
View File

@@ -39,3 +39,5 @@ tsconfig.tsbuildinfo
.idea
.fleet
.vscode
.notes

View File

@@ -3,5 +3,5 @@ node_modules
.next
build
.contentlayer
apps/www/pages/api/registry.json
**/fixtures
deprecated

10
.vscode/settings.json vendored
View File

@@ -8,5 +8,13 @@
"<node_internals>/**",
"**/node_modules/**",
"**/fixtures/**"
]
],
"files.exclude": {
"deprecated": true
},
"search.exclude": {
"apps/v4/registry/radix-*": true,
"apps/v4/public/r/*": true,
"packages/shadcn/test/fixtures/*": true
}
}

View File

@@ -20,28 +20,25 @@ This repository is structured as follows:
```
apps
└── www
└── v4
├── app
├── components
├── content
└── registry
── default
│ ├── example
│ └── ui
└── new-york
── new-york-v4
├── example
└── ui
packages
└── cli
└── shadcn
```
| Path | Description |
| --------------------- | ---------------------------------------- |
| `apps/www/app` | The Next.js application for the website. |
| `apps/www/components` | The React components for the website. |
| `apps/www/content` | The content for the website. |
| `apps/www/registry` | The registry for the components. |
| `packages/cli` | The `shadcn-ui` package. |
| Path | Description |
| -------------------- | ---------------------------------------- |
| `apps/v4/app` | The Next.js application for the website. |
| `apps/v4/components` | The React components for the website. |
| `apps/v4/content` | The content for the website. |
| `apps/v4/registry` | The registry for the components. |
| `packages/shadcn` | The `shadcn` package. |
## Development
@@ -82,32 +79,26 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
1. To run the `ui.shadcn.com` website:
```bash
pnpm --filter=www dev
pnpm --filter=v4 dev
```
2. To run the `shadcn-ui` package:
2. To run the `shadcn` package:
```bash
pnpm --filter=shadcn-ui dev
pnpm --filter=shadcn dev
```
## Running the CLI Locally
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:
1. Start by running the dev server:
```bash
pnpm v4:dev
pnpm dev
```
2. Run the development script for the CLI:
```bash
pnpm shadcn:dev
```
3. In another terminal tab, test the CLI by running:
2. In another terminal tab, test the CLI by running:
```bash
pnpm shadcn
@@ -119,36 +110,27 @@ To run the CLI locally, you can follow the workflow:
pnpm shadcn <init | add | ...> -c ~/Desktop/my-app
```
4. To run the tests for the CLI:
```bash
pnpm --filter=shadcn test
```
This workflow ensures that you are running the most recent version of the registry and testing the CLI properly in your local environment.
## Documentation
The documentation for this project is located in the `www` workspace. You can run the documentation locally by running the following command:
The documentation for this project is located in the `v4` workspace. You can run the documentation locally by running the following command:
```bash
pnpm --filter=www dev
pnpm --filter=v4 dev
```
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/www/content/docs` directory.
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/v4/content/docs` directory.
## Components
We use a registry system for developing components. You can find the source code for the components under `apps/www/registry`. The components are organized by styles.
We use a registry system for developing components. You can find the source code for the components under `apps/v4/registry`. The components are organized by styles.
```bash
apps
└── www
└── v4
└── registry
── default
│ ├── example
│ └── ui
└── new-york
── new-york-v4
├── example
└── ui
```
@@ -157,7 +139,7 @@ When adding or modifying components, please ensure that:
1. You make the changes for every style.
2. You update the documentation.
3. You run `pnpm build:registry` to update the registry.
3. You run `pnpm registry:build` to update the registry.
## Commit Convention
@@ -196,9 +178,9 @@ If you have a request for a new component, please open a discussion on GitHub. W
## CLI
The `shadcn-ui` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
The `shadcn` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
Any changes to the CLI should be made in the `packages/cli` directory. If you can, it would be great if you could add tests for your changes.
Any changes to the CLI should be made in the `packages/shadcn` directory. If you can, it would be great if you could add tests for your changes.
## Testing

View File

@@ -1,8 +1,8 @@
# shadcn/ui
Accessible and customizable components that you can copy and paste into your apps. Free. Open Source. **Use this to build your own component library**.
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. **Use this to build your own component library**.
![hero](apps/www/public/og.jpg)
![hero](apps/v4/public/opengraph-image.png)
## Documentation

View File

@@ -6,4 +6,4 @@ We will investigate all legitimate reports and do our best to quickly fix the pr
Our preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software.
To do this, please visit the security tab of the repository and click the "Report a vulnerability" button.
To do this, please visit the security tab of the repository and click the [Report a vulnerability](https://github.com/shadcn-ui/ui/security/advisories/new) button.

View File

@@ -0,0 +1,138 @@
"use client"
import * as React from "react"
import { IconMinus, IconPlus } from "@tabler/icons-react"
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 {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import { Switch } from "@/registry/new-york-v4/ui/switch"
export function AppearanceSettings() {
const [gpuCount, setGpuCount] = React.useState(8)
const handleGpuAdjustment = React.useCallback((adjustment: number) => {
setGpuCount((prevCount) =>
Math.max(1, Math.min(99, prevCount + adjustment))
)
}, [])
const handleGpuInputChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value, 10)
if (!isNaN(value) && value >= 1 && value <= 99) {
setGpuCount(value)
}
},
[]
)
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>
<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"
value={gpuCount}
onChange={handleGpuInputChange}
size={3}
className="h-8 !w-14 font-mono"
maxLength={3}
/>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label="Decrement"
onClick={() => handleGpuAdjustment(-1)}
disabled={gpuCount <= 1}
>
<IconMinus />
</Button>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label="Increment"
onClick={() => handleGpuAdjustment(1)}
disabled={gpuCount >= 99}
>
<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,
ListFilterIcon,
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>
<ListFilterIcon />
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 className="my-4">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">
<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

@@ -1,9 +1,10 @@
import { Metadata } from "next"
import { type Metadata } from "next"
import Image from "next/image"
import Link from "next/link"
import { PlusSignIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { Announcement } from "@/components/announcement"
import { CardsDemo } from "@/components/cards"
import { ExamplesNav } from "@/components/examples-nav"
import {
PageActions,
@@ -15,6 +16,8 @@ import { PageNav } from "@/components/page-nav"
import { ThemeSelector } from "@/components/theme-selector"
import { Button } from "@/registry/new-york-v4/ui/button"
import { RootComponents } from "./components"
const title = "The Foundation for your Design System"
const description =
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code."
@@ -54,10 +57,13 @@ export default function IndexPage() {
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
<PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions>
<Button asChild size="sm">
<Link href="/docs/installation">Get Started</Link>
<Button asChild size="sm" className="h-[31px] rounded-lg">
<Link href="/create">
<HugeiconsIcon icon={PlusSignIcon} />
New Project
</Link>
</Button>
<Button asChild size="sm" variant="ghost">
<Button asChild size="sm" variant="ghost" className="rounded-lg">
<Link href="/docs/components">View Components</Link>
</Button>
</PageActions>
@@ -87,7 +93,7 @@ export default function IndexPage() {
/>
</section>
<section className="theme-container hidden md:block">
<CardsDemo />
<RootComponents />
</section>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { getAllBlockIds } from "@/lib/blocks"
import { registryCategories } from "@/lib/categories"
import { BlockDisplay } from "@/components/block-display"
import { registryCategories } from "@/registry/registry-categories"
import { getActiveStyle } from "@/registry/_legacy-styles"
export const revalidate = false
export const dynamic = "force-static"
@@ -17,13 +18,16 @@ export default async function BlocksPage({
}: {
params: Promise<{ categories?: string[] }>
}) {
const { categories = [] } = await params
const [{ categories = [] }, activeStyle] = await Promise.all([
params,
getActiveStyle(),
])
const blocks = await getAllBlockIds(["registry:block"], categories)
return (
<div className="flex flex-col gap-12 md:gap-24">
{blocks.map((name) => (
<BlockDisplay name={name} key={name} />
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
))}
</div>
)

View File

@@ -1,4 +1,4 @@
import { Metadata } from "next"
import { type Metadata } from "next"
import Link from "next/link"
import { Announcement } from "@/components/announcement"

View File

@@ -1,6 +1,7 @@
import Link from "next/link"
import { BlockDisplay } from "@/components/block-display"
import { getActiveStyle } from "@/registry/_legacy-styles"
import { Button } from "@/registry/new-york-v4/ui/button"
export const dynamic = "force-static"
@@ -15,10 +16,12 @@ const FEATURED_BLOCKS = [
]
export default async function BlocksPage() {
const activeStyle = await getActiveStyle()
return (
<div className="flex flex-col gap-12 md:gap-24">
{FEATURED_BLOCKS.map((name) => (
<BlockDisplay name={name} key={name} />
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
))}
<div className="container-wrapper">
<div className="container flex justify-center py-6">

View File

@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
import { cn } from "@/lib/utils"
import { ChartDisplay } from "@/components/chart-display"
import { getActiveStyle } from "@/registry/_legacy-styles"
import { charts } from "@/app/(app)/charts/charts"
export const revalidate = false
@@ -41,6 +42,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
const chartType = type as ChartType
const chartList = charts[chartType]
const activeStyle = await getActiveStyle()
return (
<div className="grid flex-1 gap-12 lg:gap-24">
@@ -54,6 +56,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
<ChartDisplay
key={chart.id}
name={chart.id}
styleName={activeStyle.name}
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
>
<chart.component />

View File

@@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { ChartAreaAxes } from "@/registry/new-york-v4/charts/chart-area-axes"
import { ChartAreaDefault } from "@/registry/new-york-v4/charts/chart-area-default"

View File

@@ -1,4 +1,4 @@
import { Metadata } from "next"
import { type Metadata } from "next"
import Link from "next/link"
import { Announcement } from "@/components/announcement"

View File

@@ -1,4 +1,4 @@
import { Metadata } from "next"
import { type Metadata } from "next"
import Link from "next/link"
import { Announcement } from "@/components/announcement"

View File

@@ -6,7 +6,9 @@ import {
IconArrowRight,
IconArrowUpRight,
} from "@tabler/icons-react"
import { findNeighbour } from "fumadocs-core/server"
import fm from "front-matter"
import { findNeighbour } from "fumadocs-core/page-tree"
import z from "zod"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
@@ -25,7 +27,7 @@ export function generateStaticParams() {
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>
params: Promise<{ slug: string[] }>
}) {
const params = await props.params
const page = source.getPage(params.slug)
@@ -73,7 +75,7 @@ export async function generateMetadata(props: {
}
export default async function Page(props: {
params: Promise<{ slug?: string[] }>
params: Promise<{ slug: string[] }>
}) {
const params = await props.params
const page = source.getPage(params.slug)
@@ -82,18 +84,24 @@ export default async function Page(props: {
}
const doc = page.data
// @ts-expect-error - revisit fumadocs types.
const MDX = doc.body
const neighbours = await findNeighbour(source.pageTree, page.url)
const neighbours = findNeighbour(source.pageTree, page.url)
// @ts-expect-error - revisit fumadocs types.
const links = doc.links
const raw = await page.data.getText("raw")
const { attributes } = fm(raw)
const { links } = z
.object({
links: z
.object({
doc: z.string().optional(),
api: z.string().optional(),
})
.optional(),
})
.parse(attributes)
return (
<div
data-slot="docs"
className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full"
>
<div className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full">
<div className="flex min-w-0 flex-1 flex-col">
<div className="h-(--top-spacing) shrink-0" />
<div className="mx-auto flex w-full max-w-2xl min-w-0 flex-1 flex-col gap-8 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
@@ -104,11 +112,7 @@ export default async function Page(props: {
{doc.title}
</h1>
<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)}
/>
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
{neighbours.previous && (
<Button
variant="secondary"
@@ -144,19 +148,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>
@@ -195,10 +199,8 @@ export default async function Page(props: {
</div>
<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 ? (
<div className="no-scrollbar overflow-y-auto px-8">
{/* @ts-expect-error - revisit fumadocs types. */}
<DocsTableOfContents toc={doc.toc} />
<div className="h-12" />
</div>

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

@@ -1,9 +1,10 @@
import { Metadata } from "next"
import { type Metadata } from "next"
import Image from "next/image"
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

@@ -13,10 +13,10 @@ import {
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/registry/new-york-v4/ui/chart"
import {
Select,
@@ -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

@@ -35,8 +35,6 @@ import {
IconTrendingUp,
} from "@tabler/icons-react"
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
@@ -44,10 +42,12 @@ import {
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
type ColumnDef,
type ColumnFiltersState,
type Row,
type SortingState,
type VisibilityState,
} from "@tanstack/react-table"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { toast } from "sonner"
@@ -57,10 +57,10 @@ import { useIsMobile } from "@/registry/new-york-v4/hooks/use-mobile"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/registry/new-york-v4/ui/chart"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import {

View File

@@ -1,4 +1,4 @@
import { Metadata } from "next"
import { type Metadata } from "next"
import Link from "next/link"
import { Announcement } from "@/components/announcement"
@@ -16,8 +16,9 @@ import { Button } from "@/registry/new-york-v4/ui/button"
export const dynamic = "force-static"
export const revalidate = false
const title = "Examples"
const description = "Check out some examples app built using the components."
const title = "The Foundation for your Design System"
const description =
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code."
export const metadata: Metadata = {
title,
@@ -52,24 +53,20 @@ export default function ExamplesLayout({
<>
<PageHeader>
<Announcement />
<PageHeaderHeading>Build your Component Library</PageHeaderHeading>
<PageHeaderDescription>
A set of beautifully-designed, accessible components and a code
distribution platform. Works with your favorite frameworks. Open
Source. Open Code.
</PageHeaderDescription>
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
<PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions>
<Button asChild size="sm">
<Link href="/docs">Get Started</Link>
<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>
<PageNav id="examples">
<PageNav id="examples" className="hidden md:flex">
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
<ThemeSelector className="mr-4 hidden md:block" />
<ThemeSelector className="mr-4 hidden md:flex" />
</PageNav>
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
<div className="theme-container container flex flex-1 scroll-mt-20 flex-col">

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { SliderProps } from "@radix-ui/react-slider"
import { type SliderProps } from "@radix-ui/react-slider"
import {
HoverCard,

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { PopoverProps } from "@radix-ui/react-popover"
import { type PopoverProps } from "@radix-ui/react-popover"
import { Check, ChevronsUpDown } from "lucide-react"
import { cn } from "@/lib/utils"
@@ -27,7 +27,7 @@ import {
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Model, ModelType } from "../data/models"
import { type Model, type ModelType } from "../data/models"
interface ModelSelectorProps extends PopoverProps {
types: readonly ModelType[]

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { PopoverProps } from "@radix-ui/react-popover"
import { type PopoverProps } from "@radix-ui/react-popover"
import { Check, ChevronsUpDown } from "lucide-react"
import { cn } from "@/lib/utils"
@@ -21,7 +21,7 @@ import {
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Preset } from "../data/presets"
import { type Preset } from "../data/presets"
interface PresetSelectorProps extends PopoverProps {
presets: Preset[]

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { SliderProps } from "@radix-ui/react-slider"
import { type SliderProps } from "@radix-ui/react-slider"
import {
HoverCard,

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { SliderProps } from "@radix-ui/react-slider"
import { type SliderProps } from "@radix-ui/react-slider"
import {
HoverCard,

View File

@@ -1,4 +1,4 @@
import { Metadata } from "next"
import { type Metadata } from "next"
import Image from "next/image"
import { RotateCcw } from "lucide-react"

View File

@@ -1,12 +1,12 @@
"use client"
import { ColumnDef } from "@tanstack/react-table"
import { type ColumnDef } from "@tanstack/react-table"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { labels, priorities, statuses } from "../data/data"
import { Task } from "../data/schema"
import { type Task } from "../data/schema"
import { DataTableColumnHeader } from "./data-table-column-header"
import { DataTableRowActions } from "./data-table-row-actions"

View File

@@ -1,4 +1,4 @@
import { Column } from "@tanstack/react-table"
import { type Column } from "@tanstack/react-table"
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react"
import { cn } from "@/lib/utils"

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import { Column } from "@tanstack/react-table"
import { type Column } from "@tanstack/react-table"
import { Check, PlusCircle } from "lucide-react"
import { cn } from "@/lib/utils"

View File

@@ -1,4 +1,4 @@
import { Table } from "@tanstack/react-table"
import { type Table } from "@tanstack/react-table"
import {
ChevronLeft,
ChevronRight,

View File

@@ -1,6 +1,6 @@
"use client"
import { Row } from "@tanstack/react-table"
import { type Row } from "@tanstack/react-table"
import { MoreHorizontal } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"

View File

@@ -1,6 +1,6 @@
"use client"
import { Table } from "@tanstack/react-table"
import { type Table } from "@tanstack/react-table"
import { X } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"

View File

@@ -1,7 +1,7 @@
"use client"
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
import { Table } from "@tanstack/react-table"
import { type Table } from "@tanstack/react-table"
import { Settings2 } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"

View File

@@ -2,8 +2,6 @@
import * as React from "react"
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
@@ -11,9 +9,11 @@ import {
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
type ColumnDef,
type ColumnFiltersState,
type SortingState,
type VisibilityState,
} from "@tanstack/react-table"
import {

View File

@@ -1,6 +1,6 @@
import { promises as fs } from "fs"
import path from "path"
import { Metadata } from "next"
import { type Metadata } from "next"
import Image from "next/image"
import { z } from "zod"

View File

@@ -3,7 +3,10 @@ import { SiteHeader } from "@/components/site-header"
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="bg-background relative z-10 flex min-h-svh flex-col">
<div
data-slot="layout"
className="bg-background relative z-10 flex min-h-svh flex-col"
>
<SiteHeader />
<main className="flex flex-1 flex-col">{children}</main>
<SiteFooter />

View File

@@ -1,23 +1,30 @@
import { notFound } from "next/navigation"
import { NextResponse, type NextRequest } from "next/server"
import { processMdxForLLMs } from "@/lib/llm"
import { source } from "@/lib/source"
import { getActiveStyle } from "@/registry/_legacy-styles"
export const revalidate = false
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const slug = (await params).slug
const [{ slug }, activeStyle] = await Promise.all([params, getActiveStyle()])
const page = source.getPage(slug)
if (!page) {
notFound()
}
// @ts-expect-error - revisit fumadocs types.
return new NextResponse(page.data.content, {
const processedContent = processMdxForLLMs(
await page.data.getText("raw"),
activeStyle.name
)
return new NextResponse(processedContent, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},

View File

@@ -1,4 +1,4 @@
import { Metadata } from "next"
import { type Metadata } from "next"
import Link from "next/link"
import { Announcement } from "@/components/announcement"

View File

@@ -0,0 +1,95 @@
"use client"
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function MenuAccentPicker({
isMobile,
anchorRef,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentAccent = MENU_ACCENTS.find(
(accent) => accent.value === params.menuAccent
)
return (
<Picker>
<div className="group/picker relative">
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Menu Accent</div>
<div className="text-foreground text-sm font-medium">
{currentAccent?.label}
</div>
</div>
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
className="text-foreground"
>
<path
d="M19 12.1294L12.9388 18.207C11.1557 19.9949 10.2641 20.8889 9.16993 20.9877C8.98904 21.0041 8.80705 21.0041 8.62616 20.9877C7.53195 20.8889 6.64039 19.9949 4.85726 18.207L2.83687 16.1811C1.72104 15.0622 1.72104 13.2482 2.83687 12.1294M19 12.1294L10.9184 4.02587M19 12.1294H2.83687M10.9184 4.02587L2.83687 12.1294M10.9184 4.02587L8.89805 2"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
data-accent={currentAccent?.value}
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
></path>
<path
d="M22 20C22 21.1046 21.1046 22 20 22C18.8954 22 18 21.1046 18 20C18 18.8954 20 17 20 17C20 17 22 18.8954 22 20Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
data-accent={currentAccent?.value}
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
></path>
</svg>
</div>
</PickerTrigger>
<LockButton
param="menuAccent"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentAccent?.value}
onValueChange={(value) => {
setParams({ menuAccent: value as MenuAccentValue })
}}
>
<PickerGroup>
{MENU_ACCENTS.map((accent) => (
<PickerRadioItem key={accent.value} value={accent.value}>
{accent.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,125 @@
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import { useMounted } from "@/hooks/use-mounted"
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerItem,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function BaseColorPicker({
isMobile,
anchorRef,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const { resolvedTheme, setTheme } = useTheme()
const mounted = useMounted()
const [params, setParams] = useDesignSystemSearchParams()
const currentBaseColor = React.useMemo(
() => BASE_COLORS.find((baseColor) => baseColor.name === params.baseColor),
[params.baseColor]
)
return (
<Picker>
<div className="group/picker relative">
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Base Color</div>
<div className="text-foreground text-sm font-medium">
{currentBaseColor?.title}
</div>
</div>
{mounted && resolvedTheme && (
<div
style={
{
"--color":
currentBaseColor?.cssVars?.[
resolvedTheme as "light" | "dark"
]?.["muted-foreground"],
} as React.CSSProperties
}
className="absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color)"
/>
)}
</PickerTrigger>
<LockButton
param="baseColor"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentBaseColor?.name}
onValueChange={(value) => {
if (value === "dark") {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
return
}
setParams({ baseColor: value as BaseColorName })
}}
>
<PickerGroup>
{BASE_COLORS.map((baseColor) => (
<PickerRadioItem key={baseColor.name} value={baseColor.name}>
<div className="flex items-center gap-2">
{mounted && resolvedTheme && (
<div
style={
{
"--color":
baseColor.cssVars?.[
resolvedTheme as "light" | "dark"
]?.["muted-foreground"],
} as React.CSSProperties
}
className="size-4 rounded-full bg-(--color)"
/>
)}
{baseColor.title}
</div>
</PickerRadioItem>
))}
</PickerGroup>
<PickerSeparator />
<PickerGroup>
<PickerItem
onClick={() => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}}
>
<div className="flex flex-col justify-start pointer-coarse:gap-1">
<div>
Switch to {resolvedTheme === "dark" ? "Light" : "Dark"} Mode
</div>
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
Base colors are easier to see in dark mode.
</div>
</div>
</PickerItem>
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,88 @@
"use client"
import * as React from "react"
import { BASES } from "@/registry/config"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function BasePicker({
isMobile,
anchorRef,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentBase = React.useMemo(
() => BASES.find((base) => base.name === params.base),
[params.base]
)
const handleValueChange = React.useCallback(
(value: string) => {
const newBase = BASES.find((base) => base.name === value)
if (!newBase) {
return
}
setParams({ base: newBase.name })
},
[setParams]
)
return (
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Component Library</div>
<div className="text-foreground text-sm font-medium">
{currentBase?.title}
</div>
</div>
{currentBase?.meta?.logo && (
<div
className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 size-4 -translate-y-1/2 *:[svg]:size-4"
dangerouslySetInnerHTML={{
__html: currentBase.meta.logo,
}}
/>
)}
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentBase?.name}
onValueChange={handleValueChange}
>
<PickerGroup>
{BASES.map((base) => (
<PickerRadioItem key={base.name} value={base.name}>
{base.meta?.logo && (
<div
className="text-foreground *:[svg]:text-foreground! size-4 shrink-0 [&_svg]:size-4"
dangerouslySetInnerHTML={{
__html: base.meta.logo,
}}
/>
)}
{base.title}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,197 @@
"use client"
import * as React from "react"
import Script from "next/script"
import { DiceFaces05Icon, Undo02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import {
BASE_COLORS,
DEFAULT_CONFIG,
getThemesForBaseColor,
iconLibraries,
MENU_ACCENTS,
MENU_COLORS,
RADII,
STYLES,
} from "@/registry/config"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
import { useLocks } from "@/app/(create)/hooks/use-locks"
import { FONTS } from "@/app/(create)/lib/fonts"
import {
applyBias,
RANDOMIZE_BIASES,
type RandomizeContext,
} from "@/app/(create)/lib/randomize-biases"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
function randomItem<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)]
}
export function CustomizerControls({ className }: { className?: string }) {
const { locks } = useLocks()
const [params, setParams] = useDesignSystemSearchParams()
const handleReset = React.useCallback(() => {
setParams({
base: params.base, // Keep the current base value
style: DEFAULT_CONFIG.style,
baseColor: DEFAULT_CONFIG.baseColor,
theme: DEFAULT_CONFIG.theme,
iconLibrary: DEFAULT_CONFIG.iconLibrary,
font: DEFAULT_CONFIG.font,
menuAccent: DEFAULT_CONFIG.menuAccent,
menuColor: DEFAULT_CONFIG.menuColor,
radius: DEFAULT_CONFIG.radius,
template: DEFAULT_CONFIG.template,
item: "preview",
})
}, [setParams, params.base])
const handleRandomize = React.useCallback(() => {
// Use current value if locked, otherwise randomize.
const baseColor = locks.has("baseColor")
? params.baseColor
: randomItem(BASE_COLORS).name
const selectedStyle = locks.has("style")
? params.style
: randomItem(STYLES).name
// Build context for bias application.
const context: RandomizeContext = {
style: selectedStyle,
baseColor,
}
const availableThemes = getThemesForBaseColor(baseColor)
const availableFonts = applyBias(FONTS, context, RANDOMIZE_BIASES.fonts)
const availableRadii = applyBias(RADII, context, RANDOMIZE_BIASES.radius)
const selectedTheme = locks.has("theme")
? params.theme
: randomItem(availableThemes).name
const selectedFont = locks.has("font")
? params.font
: randomItem(availableFonts).value
const selectedRadius = locks.has("radius")
? params.radius
: randomItem(availableRadii).name
const selectedIconLibrary = locks.has("iconLibrary")
? params.iconLibrary
: randomItem(Object.values(iconLibraries)).name
const selectedMenuAccent = locks.has("menuAccent")
? params.menuAccent
: randomItem(MENU_ACCENTS).value
const selectedMenuColor = locks.has("menuColor")
? params.menuColor
: randomItem(MENU_COLORS).value
// Update context with selected values for potential future biases.
context.theme = selectedTheme
context.font = selectedFont
context.radius = selectedRadius
setParams({
style: selectedStyle,
baseColor,
theme: selectedTheme,
iconLibrary: selectedIconLibrary,
font: selectedFont,
menuAccent: selectedMenuAccent,
menuColor: selectedMenuColor,
radius: selectedRadius,
})
}, [setParams, locks, params])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === "r" || e.key === "R") && !e.metaKey && !e.ctrlKey) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
e.preventDefault()
handleRandomize()
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [handleRandomize])
return (
<div className={cn("items-center gap-0", className)}>
<Button
variant="ghost"
size="sm"
onClick={handleRandomize}
className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Shuffle</div>
<div className="text-foreground text-sm font-medium">Try Random</div>
</div>
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
<Kbd className="bg-foreground/10 text-foreground hidden md:flex">R</Kbd>
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleReset}
className="border-foreground/10 bg-muted/50 hidden h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:flex md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Reset</div>
<div className="text-foreground text-sm font-medium">Start Over</div>
</div>
<HugeiconsIcon icon={Undo02Icon} className="-translate-x-0.5" />
</Button>
</div>
)
}
export function RandomizeScript() {
return (
<Script
id="randomize-listener"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
(function() {
// Forward R key
document.addEventListener('keydown', function(e) {
if ((e.key === 'r' || e.key === 'R') && !e.metaKey && !e.ctrlKey) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return;
}
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: '${RANDOMIZE_FORWARD_TYPE}',
key: e.key
}, '*');
}
}
});
})();
`,
}}
/>
)
}

View File

@@ -0,0 +1,83 @@
"use client"
import * as React from "react"
import { Settings05Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useIsMobile } from "@/hooks/use-mobile"
import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config"
import { FieldGroup } from "@/registry/new-york-v4/ui/field"
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
import { BasePicker } from "@/app/(create)/components/base-picker"
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
import { FontPicker } from "@/app/(create)/components/font-picker"
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
import { PresetPicker } from "@/app/(create)/components/preset-picker"
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
import { StylePicker } from "@/app/(create)/components/style-picker"
import { ThemePicker } from "@/app/(create)/components/theme-picker"
import { FONTS } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function Customizer() {
const [params] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const anchorRef = React.useRef<HTMLDivElement | null>(null)
const availableThemes = React.useMemo(
() => getThemesForBaseColor(params.baseColor),
[params.baseColor]
)
return (
<div
className="no-scrollbar -mx-2.5 flex flex-col overflow-y-auto p-1 md:mx-0 md:h-[calc(100svh-var(--header-height)-2rem)] md:w-48 md:gap-0 md:py-0"
ref={anchorRef}
>
<div className="hidden items-center gap-2 px-[calc(--spacing(2.5))] pb-1 md:flex md:flex-col md:items-start">
<HugeiconsIcon
icon={Settings05Icon}
className="size-4"
strokeWidth={2}
/>
<div className="relative flex flex-col gap-1 rounded-lg text-[13px]/snug">
<div className="flex items-center gap-1 font-medium text-balance">
Build your own shadcn/ui
</div>
<div className="hidden md:flex">
When you&apos;re done, click Create Project to start a new project.
</div>
</div>
</div>
<div className="no-scrollbar h-14 overflow-x-auto overflow-y-hidden p-px md:h-full md:overflow-x-hidden md:overflow-y-auto">
<FieldGroup className="flex h-full flex-1 flex-row gap-2 md:flex-col md:gap-0">
<PresetPicker
presets={PRESETS}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<BasePicker isMobile={isMobile} anchorRef={anchorRef} />
<StylePicker
styles={STYLES}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<ThemePicker
themes={availableThemes}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
<CustomizerControls className="mt-auto hidden w-full flex-col md:flex" />
</FieldGroup>
</div>
</div>
)
}

View File

@@ -0,0 +1,172 @@
"use client"
import * as React from "react"
import {
buildRegistryTheme,
DEFAULT_CONFIG,
type DesignSystemConfig,
} from "@/registry/config"
import { useDesignSystemParam } from "@/app/(create)/hooks/use-design-system"
import { FONTS } from "@/app/(create)/lib/fonts"
export function DesignSystemProvider({
children,
}: {
children: React.ReactNode
}) {
const style = useDesignSystemParam("style")
const theme = useDesignSystemParam("theme")
const font = useDesignSystemParam("font")
const baseColor = useDesignSystemParam("baseColor")
const menuAccent = useDesignSystemParam("menuAccent")
const menuColor = useDesignSystemParam("menuColor")
const radius = useDesignSystemParam("radius")
const [isReady, setIsReady] = React.useState(false)
// Use useLayoutEffect for synchronous style updates to prevent flash.
React.useLayoutEffect(() => {
if (!style || !theme || !font || !baseColor) {
return
}
const body = document.body
// Update style class in place (remove old, add new).
body.classList.forEach((className) => {
if (className.startsWith("style-")) {
body.classList.remove(className)
}
})
body.classList.add(`style-${style}`)
// Update base color class in place.
body.classList.forEach((className) => {
if (className.startsWith("base-color-")) {
body.classList.remove(className)
}
})
body.classList.add(`base-color-${baseColor}`)
// Update font.
const selectedFont = FONTS.find((f) => f.value === font)
if (selectedFont) {
const fontFamily = selectedFont.font.style.fontFamily
document.documentElement.style.setProperty("--font-sans", fontFamily)
}
setIsReady(true)
}, [style, theme, font, baseColor])
const registryTheme = React.useMemo(() => {
if (!baseColor || !theme || !menuAccent || !radius) {
return null
}
const config: DesignSystemConfig = {
...DEFAULT_CONFIG,
baseColor,
theme,
menuAccent,
radius,
}
return buildRegistryTheme(config)
}, [baseColor, theme, menuAccent, radius])
// Use useLayoutEffect for synchronous CSS var updates.
React.useLayoutEffect(() => {
if (!registryTheme || !registryTheme.cssVars) {
return
}
const styleId = "design-system-theme-vars"
let styleElement = document.getElementById(
styleId
) as HTMLStyleElement | null
if (!styleElement) {
styleElement = document.createElement("style")
styleElement.id = styleId
document.head.appendChild(styleElement)
}
const {
light: lightVars,
dark: darkVars,
theme: themeVars,
} = registryTheme.cssVars
let cssText = ":root {\n"
// Add theme vars (shared across light/dark).
if (themeVars) {
Object.entries(themeVars).forEach(([key, value]) => {
if (value) {
cssText += ` --${key}: ${value};\n`
}
})
}
// Add light mode vars.
if (lightVars) {
Object.entries(lightVars).forEach(([key, value]) => {
if (value) {
cssText += ` --${key}: ${value};\n`
}
})
}
cssText += "}\n\n"
cssText += ".dark {\n"
if (darkVars) {
Object.entries(darkVars).forEach(([key, value]) => {
if (value) {
cssText += ` --${key}: ${value};\n`
}
})
}
cssText += "}\n"
styleElement.textContent = cssText
}, [registryTheme])
// Handle menu color inversion by adding/removing dark class to elements with cn-menu-target.
React.useEffect(() => {
if (!menuColor) {
return
}
const updateMenuElements = () => {
const menuElements = document.querySelectorAll(".cn-menu-target")
menuElements.forEach((element) => {
if (menuColor === "inverted") {
element.classList.add("dark")
} else {
element.classList.remove("dark")
}
})
}
// Update existing menu elements.
updateMenuElements()
// Watch for new menu elements being added to the DOM.
const observer = new MutationObserver(() => {
updateMenuElements()
})
observer.observe(document.body, {
childList: true,
subtree: true,
})
return () => {
observer.disconnect()
}
}, [menuColor])
if (!isReady) {
return null
}
return <>{children}</>
}

View File

@@ -0,0 +1,102 @@
"use client"
import * as React from "react"
import {
Item,
ItemContent,
ItemDescription,
ItemTitle,
} from "@/registry/bases/radix/ui/item"
import { type FontValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { type Font } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function FontPicker({
fonts,
isMobile,
anchorRef,
}: {
fonts: readonly Font[]
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentFont = React.useMemo(
() => fonts.find((font) => font.value === params.font),
[fonts, params.font]
)
return (
<Picker>
<div className="group/picker relative">
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Font</div>
<div className="text-foreground text-sm font-medium">
{currentFont?.name}
</div>
</div>
<div
className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base"
style={{ fontFamily: currentFont?.font.style.fontFamily }}
>
Aa
</div>
</PickerTrigger>
<LockButton
param="font"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-80 md:w-72"
>
<PickerRadioGroup
value={currentFont?.value}
onValueChange={(value) => {
setParams({ font: value as FontValue })
}}
>
<PickerGroup>
{fonts.map((font, index) => (
<React.Fragment key={font.value}>
<PickerRadioItem value={font.value}>
<Item size="xs">
<ItemContent className="gap-1">
<ItemTitle className="text-muted-foreground text-xs font-medium">
{font.name}
</ItemTitle>
<ItemDescription
style={{ fontFamily: font.font.style.fontFamily }}
>
Designers love packing quirky glyphs into test phrases.
</ItemDescription>
</ItemContent>
</Item>
</PickerRadioItem>
{index < fonts.length - 1 && (
<PickerSeparator className="opacity-50" />
)}
</React.Fragment>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,289 @@
"use client"
import * as React from "react"
import { lazy, memo, Suspense } from "react"
import { Item, ItemContent, ItemTitle } from "@/registry/bases/radix/ui/item"
import {
iconLibraries,
type IconLibrary,
type IconLibraryName,
} from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const IconLucide = lazy(() =>
import("@/registry/icons/icon-lucide").then((mod) => ({
default: mod.IconLucide,
}))
)
const IconTabler = lazy(() =>
import("@/registry/icons/icon-tabler").then((mod) => ({
default: mod.IconTabler,
}))
)
const IconHugeicons = lazy(() =>
import("@/registry/icons/icon-hugeicons").then((mod) => ({
default: mod.IconHugeicons,
}))
)
const PREVIEW_ICONS = {
lucide: [
"CopyIcon",
"CircleAlertIcon",
"TrashIcon",
"ShareIcon",
"ShoppingBagIcon",
"MoreHorizontalIcon",
"Loader2Icon",
"PlusIcon",
"MinusIcon",
"ArrowLeftIcon",
"ArrowRightIcon",
"CheckIcon",
"ChevronDownIcon",
"ChevronRightIcon",
],
tabler: [
"IconCopy",
"IconExclamationCircle",
"IconTrash",
"IconShare",
"IconShoppingBag",
"IconDots",
"IconLoader",
"IconPlus",
"IconMinus",
"IconArrowLeft",
"IconArrowRight",
"IconCheck",
"IconChevronDown",
"IconChevronRight",
],
hugeicons: [
"Copy01Icon",
"AlertCircleIcon",
"Delete02Icon",
"Share03Icon",
"ShoppingBag01Icon",
"MoreHorizontalCircle01Icon",
"Loading03Icon",
"PlusSignIcon",
"MinusSignIcon",
"ArrowLeft02Icon",
"ArrowRight02Icon",
"Tick02Icon",
"ArrowDown01Icon",
"ArrowRight01Icon",
],
}
const logos = {
lucide: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
d="M14 12a4 4 0 0 0-8 0 8 8 0 1 0 16 0 11.97 11.97 0 0 0-4-8.944"
/>
<path
stroke="currentColor"
d="M10 12a4 4 0 0 0 8 0 8 8 0 1 0-16 0 11.97 11.97 0 0 0 4.063 9"
/>
</svg>
),
tabler: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="none"
viewBox="0 0 32 32"
>
<path
fill="currentColor"
d="M31.288 7.107A8.83 8.83 0 0 0 24.893.712a55.9 55.9 0 0 0-17.786 0A8.83 8.83 0 0 0 .712 7.107a55.9 55.9 0 0 0 0 17.786 8.83 8.83 0 0 0 6.395 6.395c5.895.95 11.89.95 17.786 0a8.83 8.83 0 0 0 6.395-6.395c.95-5.895.95-11.89 0-17.786"
/>
<path
fill="#fff"
d="m17.884 9.076 1.5-2.488 6.97 6.977-2.492 1.494zm-7.96 3.127 7.814-.909 3.91 3.66-.974 7.287-9.582 2.159a3.06 3.06 0 0 1-2.17-.329l5.244-4.897c.91.407 2.003.142 2.587-.626.584-.77.488-1.818-.226-2.484s-1.84-.755-2.664-.21c-.823.543-1.107 1.562-.67 2.412l-5.245 4.89a2.53 2.53 0 0 1-.339-2.017z"
/>
</svg>
),
hugeicons: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M2 9.5H22" stroke="currentColor"></path>
<path
d="M20.5 9.5H3.5L4.23353 15.3682C4.59849 18.2879 4.78097 19.7477 5.77343 20.6239C6.76589 21.5 8.23708 21.5 11.1795 21.5H12.8205C15.7629 21.5 17.2341 21.5 18.2266 20.6239C19.219 19.7477 19.4015 18.2879 19.7665 15.3682L20.5 9.5Z"
stroke="currentColor"
></path>
<path
d="M5 9C5 5.41015 8.13401 2.5 12 2.5C15.866 2.5 19 5.41015 19 9"
stroke="currentColor"
></path>
</svg>
),
}
export function IconLibraryPicker({
isMobile,
anchorRef,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentIconLibrary = React.useMemo(
() => iconLibraries[params.iconLibrary as keyof typeof iconLibraries],
[params.iconLibrary]
)
return (
<Picker>
<div className="group/picker relative">
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Icon Library</div>
<div className="text-foreground text-sm font-medium">
{currentIconLibrary?.title}
</div>
</div>
<div className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
{logos[currentIconLibrary?.name as keyof typeof logos]}
</div>
</PickerTrigger>
<LockButton
param="iconLibrary"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentIconLibrary?.name}
onValueChange={(value) => {
setParams({ iconLibrary: value as IconLibraryName })
}}
>
<PickerGroup>
{Object.values(iconLibraries).map((iconLibrary, index) => (
<React.Fragment key={iconLibrary.name}>
<IconLibraryPickerItem
iconLibrary={iconLibrary}
value={iconLibrary.name}
/>
{index < Object.values(iconLibraries).length - 1 && (
<PickerSeparator className="opacity-50" />
)}
</React.Fragment>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}
function IconLibraryPickerItem({
iconLibrary,
value,
}: {
iconLibrary: IconLibrary
value: string
}) {
return (
<PickerRadioItem
value={value}
className="pr-2 *:data-[slot=dropdown-menu-radio-item-indicator]:hidden"
>
<Item size="xs">
<ItemContent className="gap-1">
<ItemTitle className="text-muted-foreground text-xs font-medium">
{iconLibrary.title}
</ItemTitle>
<IconLibraryPreview iconLibrary={iconLibrary.name} />
</ItemContent>
</Item>
</PickerRadioItem>
)
}
const IconLibraryPreview = memo(function IconLibraryPreview({
iconLibrary,
}: {
iconLibrary: IconLibraryName
}) {
const previewIcons = PREVIEW_ICONS[iconLibrary]
if (!previewIcons) {
return null
}
const IconRenderer =
iconLibrary === "lucide"
? IconLucide
: iconLibrary === "tabler"
? IconTabler
: IconHugeicons
return (
<Suspense
fallback={
<div className="-mx-1 grid w-full grid-cols-7 gap-2">
{previewIcons.map((iconName) => (
<div
key={iconName}
className="bg-muted size-6 animate-pulse rounded"
/>
))}
</div>
}
>
<div className="-mx-1 grid w-full grid-cols-7 gap-2">
{previewIcons.map((iconName) => (
<div
key={iconName}
className="flex size-6 items-center justify-center *:[svg]:size-5"
>
<IconRenderer name={iconName} />
</div>
))}
</div>
</Suspense>
)
})

View File

@@ -0,0 +1,48 @@
"use client"
import { lazy, Suspense } from "react"
import { SquareIcon } from "lucide-react"
import type { IconLibraryName } from "shadcn/icons"
import { useDesignSystemParam } from "@/app/(create)/hooks/use-design-system"
const IconLucide = lazy(() =>
import("@/registry/icons/icon-lucide").then((mod) => ({
default: mod.IconLucide,
}))
)
const IconTabler = lazy(() =>
import("@/registry/icons/icon-tabler").then((mod) => ({
default: mod.IconTabler,
}))
)
const IconHugeicons = lazy(() =>
import("@/registry/icons/icon-hugeicons").then((mod) => ({
default: mod.IconHugeicons,
}))
)
export function IconPlaceholder({
...props
}: {
[K in IconLibraryName]: string
} & React.ComponentProps<"svg">) {
const iconLibrary = useDesignSystemParam("iconLibrary")
const iconName = props[iconLibrary]
if (!iconName) {
return null
}
return (
<Suspense fallback={<SquareIcon {...props} />}>
{iconLibrary === "lucide" && <IconLucide name={iconName} {...props} />}
{iconLibrary === "tabler" && <IconTabler name={iconName} {...props} />}
{iconLibrary === "hugeicons" && (
<IconHugeicons name={iconName} {...props} />
)}
</Suspense>
)
}

View File

@@ -0,0 +1,108 @@
"use client"
import * as React from "react"
import Link from "next/link"
import { ChevronRightIcon } from "lucide-react"
import { type RegistryItem } from "shadcn/schema"
import { cn } from "@/lib/utils"
import { type Base } from "@/registry/bases"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/registry/new-york-v4/ui/collapsible"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(create)/lib/utils"
const cachedGroupedItems = React.cache(
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
return groupItemsByType(items)
}
)
export function ItemExplorer({
base,
items,
}: {
base: Base["name"]
items: Pick<RegistryItem, "name" | "title" | "type">[]
}) {
const [params, setParams] = useDesignSystemSearchParams()
const groupedItems = React.useMemo(() => cachedGroupedItems(items), [items])
const currentItem = React.useMemo(
() => items.find((item) => item.name === params.item) ?? null,
[items, params.item]
)
return (
<Sidebar
className="sticky z-30 hidden h-[calc(100svh-var(--header-height)-2rem)] overscroll-none bg-transparent xl:flex"
collapsible="none"
>
<SidebarContent className="no-scrollbar -mx-1 overflow-x-hidden">
{groupedItems.map((group) => (
<Collapsible
key={group.type}
defaultOpen
className="group/collapsible"
>
<SidebarGroup className="px-1 py-0">
<CollapsibleTrigger className="flex w-full items-center gap-1 py-1.5 text-[0.8rem] font-medium [&[data-state=open]>svg]:rotate-90">
<ChevronRightIcon className="text-muted-foreground size-3.5 transition-transform" />
<span>{group.title}</span>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu className="border-border/50 relative ml-1.5 border-l pl-2">
{group.items.map((item, index) => (
<SidebarMenuItem key={item.name} className="relative">
<div
className={cn(
"border-border/50 absolute top-1/2 -left-2 h-px w-2 border-t",
index === group.items.length - 1 && "bg-sidebar"
)}
/>
{index === group.items.length - 1 && (
<div className="bg-sidebar absolute top-1/2 -bottom-1 -left-2.5 w-1" />
)}
<SidebarMenuButton
onClick={() => setParams({ item: item.name })}
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[26px] w-fit cursor-pointer overflow-visible border border-transparent text-[0.8rem] font-normal after:absolute after:inset-x-0 after:-inset-y-1 after:-z-0 after:rounded-md"
data-active={item.name === currentItem?.name}
isActive={item.name === currentItem?.name}
>
{item.title}
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
</SidebarMenuButton>
<Link
href={`/preview/${base}/${item.name}`}
prefetch
className="sr-only"
tabIndex={-1}
>
{item.title}
</Link>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
))}
</SidebarContent>
</Sidebar>
)
}

View File

@@ -0,0 +1,193 @@
"use client"
import * as React from "react"
import Script from "next/script"
import { Search01Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { type RegistryItem } from "shadcn/schema"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Combobox,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxTrigger,
ComboboxValue,
} from "@/registry/new-york-v4/ui/combobox"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(create)/lib/utils"
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
const cachedGroupedItems = React.cache(
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
return groupItemsByType(items)
}
)
export function ItemPicker({
items,
}: {
items: Pick<RegistryItem, "name" | "title" | "type">[]
}) {
const [open, setOpen] = React.useState(false)
const [params, setParams] = useDesignSystemSearchParams()
const groupedItems = React.useMemo(() => cachedGroupedItems(items), [items])
const currentItem = React.useMemo(
() => items.find((item) => item.name === params.item) ?? null,
[items, params.item]
)
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === "k" || e.key === "p") && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
const handleSelect = React.useCallback(
(item: Pick<RegistryItem, "name" | "title" | "type">) => {
setParams({ item: item.name })
setOpen(false)
},
[setParams]
)
const comboboxValue = React.useMemo(() => {
return currentItem ?? null
}, [currentItem])
return (
<Combobox
autoHighlight
items={groupedItems}
value={comboboxValue}
onValueChange={(value) => {
if (value) {
handleSelect(value)
}
}}
open={open}
onOpenChange={setOpen}
itemToStringValue={(item) => {
if (!item) {
return ""
}
// Handle both groups and items.
if ("items" in item) {
return item.title ?? ""
}
return item.title ?? item.name ?? ""
}}
>
<ComboboxTrigger
render={
<Button
variant="outline"
aria-label="Select item"
size="sm"
className="data-popup-open:bg-muted dark:data-popup-open:bg-muted/50 bg-muted/50 sm:bg-background md:dark:bg-background border-foreground/10 dark:bg-muted/50 h-[calc(--spacing(13.5))] flex-1 touch-manipulation justify-between gap-2 rounded-xl pr-4! pl-2.5 text-left shadow-none select-none *:data-[slot=combobox-trigger-icon]:hidden sm:h-8 sm:max-w-56 sm:rounded-lg sm:pr-2! xl:max-w-md"
/>
}
>
<ComboboxValue>
{(value) => (
<>
<div className="flex flex-col justify-start text-left sm:hidden">
<div className="text-muted-foreground text-xs font-normal">
Preview
</div>
<div className="text-foreground text-sm font-medium">
{value?.title || "Not Found"}
</div>
</div>
<div className="text-foreground hidden flex-1 text-sm sm:flex">
{value?.title || "Not Found"}
</div>
</>
)}
</ComboboxValue>
<HugeiconsIcon icon={Search01Icon} />
</ComboboxTrigger>
<ComboboxContent
className="ring-foreground/10 min-w-[calc(var(--available-width)---spacing(4))] translate-x-2 animate-none rounded-xl border-0 ring-1 data-open:animate-none sm:min-w-[calc(var(--anchor-width)+--spacing(7))] sm:translate-x-0"
side="bottom"
align="center"
>
<ComboboxInput
showTrigger={false}
placeholder="Search"
className="bg-muted h-8 rounded-lg shadow-none has-focus-visible:border-inherit! has-focus-visible:ring-0! pointer-coarse:hidden"
/>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList className="no-scrollbar scroll-my-1 pb-1">
{(group) => (
<ComboboxGroup key={group.type} items={group.items}>
<ComboboxLabel>{group.title}</ComboboxLabel>
<ComboboxCollection>
{(item) => (
<ComboboxItem
key={item.name}
value={item}
className="group/combobox-item rounded-lg pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base"
>
{item.title}
<span className="text-muted-foreground ml-auto text-xs opacity-0 group-data-[selected=true]/combobox-item:opacity-100">
{group.title}
</span>
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
<div
data-open={open}
className="fixed inset-0 z-50 hidden bg-transparent data-[open=true]:block"
onClick={() => setOpen(false)}
/>
</Combobox>
)
}
export function ItemPickerScript() {
return (
<Script
id="design-system-listener"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
(function() {
// Forward Cmd/Ctrl + K and Cmd/Ctrl + P
document.addEventListener('keydown', function(e) {
if ((e.key === 'k' || e.key === 'p') && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: '${CMD_K_FORWARD_TYPE}',
key: e.key
}, '*');
}
}
});
})();
`,
}}
/>
)
}

View File

@@ -0,0 +1,50 @@
"use client"
import {
SquareLock01Icon,
SquareUnlock01Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { useLocks, type LockableParam } from "@/app/(create)/hooks/use-locks"
export function LockButton({
param,
className,
}: {
param: LockableParam
className?: string
}) {
const { isLocked, toggleLock } = useLocks()
const locked = isLocked(param)
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => toggleLock(param)}
data-locked={locked}
className={cn(
"flex size-4 cursor-pointer items-center justify-center rounded opacity-0 transition-opacity group-focus-within/picker:opacity-100 group-hover/picker:opacity-100 focus:opacity-100 data-[locked=true]:opacity-100 pointer-coarse:hidden",
className
)}
aria-label={locked ? "Unlock" : "Lock"}
>
<HugeiconsIcon
icon={locked ? SquareLock01Icon : SquareUnlock01Icon}
strokeWidth={2}
className="text-foreground size-5"
/>
</button>
</TooltipTrigger>
<TooltipContent>{locked ? "Unlock" : "Lock"}</TooltipContent>
</Tooltip>
)
}

View File

@@ -0,0 +1,163 @@
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import { useMounted } from "@/hooks/use-mounted"
import { type MenuColorValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const MENU_OPTIONS = [
{
value: "default" as const,
label: "Default",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
role="img"
stroke="currentColor"
className="text-foreground"
>
<path
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M8.5 11.5L14.5001 11.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.5 15H13.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 8H15.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
{
value: "inverted" as const,
label: "Inverted",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
role="img"
className="fill-foreground text-foreground"
>
<path
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M8.5 11.5L14.5001 11.5"
stroke="var(--background)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.5 15H13.5"
stroke="var(--background)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 8H15.5"
stroke="var(--background)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
] as const
export function MenuColorPicker({
isMobile,
anchorRef,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const { resolvedTheme } = useTheme()
const mounted = useMounted()
const [params, setParams] = useDesignSystemSearchParams()
const currentMenu = MENU_OPTIONS.find(
(menu) => menu.value === params.menuColor
)
return (
<Picker>
<div className="group/picker relative">
<PickerTrigger disabled={mounted && resolvedTheme === "dark"}>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Menu Color</div>
<div className="text-foreground text-sm font-medium">
{currentMenu?.label}
</div>
</div>
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
{currentMenu?.icon}
</div>
</PickerTrigger>
<LockButton
param="menuColor"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentMenu?.value}
onValueChange={(value) => {
setParams({ menuColor: value as MenuColorValue })
}}
>
<PickerGroup>
{MENU_OPTIONS.map((menu) => (
<PickerRadioItem key={menu.value} value={menu.value}>
{menu.icon}
{menu.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,284 @@
"use client"
import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { cn } from "@/registry/bases/base/lib/utils"
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
function Picker({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function PickerPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
}
function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
return (
<MenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
className={cn(
"hover:bg-muted data-popup-open:bg-muted border-foreground/10 bg-muted/50 relative w-[160px] shrink-0 touch-manipulation rounded-xl border p-2 select-none disabled:opacity-50 md:w-full md:rounded-lg md:border-transparent md:bg-transparent",
className
)}
{...props}
/>
)
}
function PickerContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
anchor,
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "anchor"
>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
anchor={anchor}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground cn-menu-target ring-foreground/10 no-scrollbar z-50 max-h-(--available-height) w-[calc(var(--available-width)-(--spacing(3.5)))] min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border-0 p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden md:w-52",
className
)}
{...props}
/>
</MenuPrimitive.Positioner>
<div className="absolute inset-0 z-40 bg-transparent" />
</MenuPrimitive.Portal>
)
}
function PickerGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
}
function PickerLabel({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"text-muted-foreground px-2 py-1.5 text-xs font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function PickerItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function PickerSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
}
function PickerSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<IconPlaceholder
lucide="ChevronRightIcon"
tabler="IconChevronRight"
hugeicons="ArrowRight01Icon"
className="ml-auto"
/>
</MenuPrimitive.SubmenuTrigger>
)
}
function PickerSubContent({
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof PickerContent>) {
return (
<PickerContent
data-slot="dropdown-menu-sub-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground w-auto min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100",
className
)}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
)
}
function PickerCheckboxItem({
className,
children,
checked,
...props
}: MenuPrimitive.CheckboxItem.Props) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
<MenuPrimitive.CheckboxItemIndicator>
<IconPlaceholder
lucide="CheckIcon"
tabler="IconCheck"
hugeicons="Tick02Icon"
/>
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
)
}
function PickerRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return (
<MenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function PickerRadioItem({
className,
children,
...props
}: MenuPrimitive.RadioItem.Props) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<IconPlaceholder
lucide="CheckIcon"
tabler="IconCheck"
hugeicons="Tick02Icon"
className="size-4 pointer-coarse:size-5"
/>
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
)
}
function PickerSeparator({
className,
...props
}: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function PickerShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Picker,
PickerPortal,
PickerTrigger,
PickerContent,
PickerGroup,
PickerLabel,
PickerItem,
PickerCheckboxItem,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerShortcut,
PickerSub,
PickerSubTrigger,
PickerSubContent,
}

View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import { STYLES, type Preset } from "@/registry/config"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function PresetPicker({
presets,
isMobile,
anchorRef,
}: {
presets: readonly Preset[]
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentPreset = React.useMemo(() => {
return presets.find(
(preset) =>
preset.base === params.base &&
preset.style === params.style &&
preset.baseColor === params.baseColor &&
preset.theme === params.theme &&
preset.iconLibrary === params.iconLibrary &&
preset.font === params.font &&
preset.menuAccent === params.menuAccent &&
preset.menuColor === params.menuColor &&
preset.radius === params.radius
)
}, [
presets,
params.base,
params.style,
params.baseColor,
params.theme,
params.iconLibrary,
params.font,
params.menuAccent,
params.menuColor,
params.radius,
])
// Filter presets for current base only
const currentBasePresets = React.useMemo(() => {
return presets.filter((preset) => preset.base === params.base)
}, [presets, params.base])
const handlePresetChange = (value: string) => {
const preset = presets.find((p) => p.title === value)
if (!preset) {
return
}
// Update all params including base.
setParams({
base: preset.base,
style: preset.style,
baseColor: preset.baseColor,
theme: preset.theme,
iconLibrary: preset.iconLibrary,
font: preset.font,
menuAccent: preset.menuAccent,
menuColor: preset.menuColor,
radius: preset.radius,
custom: false,
})
}
return (
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Preset</div>
<div className="text-foreground line-clamp-1 text-sm font-medium">
{currentPreset?.description ?? "Custom"}
</div>
</div>
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="md:w-72"
>
<PickerRadioGroup
value={currentPreset?.title ?? ""}
onValueChange={handlePresetChange}
>
<PickerGroup>
{currentBasePresets.map((preset) => {
const style = STYLES.find((s) => s.name === preset.style)
return (
<PickerRadioItem key={preset.title} value={preset.title}>
<div className="flex items-center gap-2">
{style?.icon && (
<div className="flex size-4 shrink-0 items-center justify-center">
{React.cloneElement(style.icon, {
className: "size-4",
})}
</div>
)}
{preset.description}
</div>
</PickerRadioItem>
)
})}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,40 @@
"use client"
import { Monitor, Smartphone, Tablet } from "lucide-react"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/registry/new-york-v4/ui/toggle-group"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function PreviewControls() {
const [params, setParams] = useDesignSystemSearchParams({
history: "replace",
})
return (
<div className="flex h-8 items-center gap-1.5 rounded-md border p-1">
<ToggleGroup
type="single"
value={params.size.toString()}
onValueChange={(newValue) => {
if (newValue) {
setParams({ size: parseInt(newValue) })
}
}}
className="gap-1 *:data-[slot=toggle-group-item]:!size-6 *:data-[slot=toggle-group-item]:!rounded-sm"
>
<ToggleGroupItem value="100" title="Desktop">
<Monitor />
</ToggleGroupItem>
<ToggleGroupItem value="60" title="Tablet">
<Tablet />
</ToggleGroupItem>
<ToggleGroupItem value="30" title="Mobile">
<Smartphone />
</ToggleGroupItem>
</ToggleGroup>
</div>
)
}

View File

@@ -0,0 +1,16 @@
"use client"
export function PreviewStyle() {
return (
<style jsx global>{`
html {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
`}</style>
)
}

View File

@@ -0,0 +1,125 @@
"use client"
import * as React from "react"
import { type ImperativePanelHandle } from "react-resizable-panels"
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/customizer-controls"
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker"
import { useDesignSystemSync } from "@/app/(create)/hooks/use-design-system"
const MESSAGE_TYPE = "design-system-params"
export function Preview() {
const params = useDesignSystemSync()
const iframeRef = React.useRef<HTMLIFrameElement>(null)
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
const [initialParams] = React.useState(params)
const [iframeKey, setIframeKey] = React.useState(0)
// Sync resizable panel with URL param changes.
React.useEffect(() => {
if (resizablePanelRef.current && params.size) {
resizablePanelRef.current.resize(params.size)
}
}, [params.size])
React.useEffect(() => {
const iframe = iframeRef.current
if (!iframe) {
return
}
const sendParams = () => {
iframe.contentWindow?.postMessage(
{
type: MESSAGE_TYPE,
params,
},
"*"
)
}
if (iframe.contentWindow) {
sendParams()
}
iframe.addEventListener("load", sendParams)
return () => {
iframe.removeEventListener("load", sendParams)
}
}, [params])
const handleMessage = (event: MessageEvent) => {
if (event.data.type === CMD_K_FORWARD_TYPE) {
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
const key = event.data.key || "k"
const syntheticEvent = new KeyboardEvent("keydown", {
key,
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
document.dispatchEvent(syntheticEvent)
}
if (event.data.type === RANDOMIZE_FORWARD_TYPE) {
const key = event.data.key || "r"
const syntheticEvent = new KeyboardEvent("keydown", {
key,
bubbles: true,
cancelable: true,
})
document.dispatchEvent(syntheticEvent)
}
if (event.data.type === DARK_MODE_FORWARD_TYPE) {
const key = event.data.key || "d"
const syntheticEvent = new KeyboardEvent("keydown", {
key,
bubbles: true,
cancelable: true,
})
document.dispatchEvent(syntheticEvent)
}
}
React.useEffect(() => {
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
}, [])
if (!params.item || !params.base) {
return null
}
const iframeSrc = `/preview/${params.base}/${params.item}?theme=${initialParams.theme ?? "neutral"}&iconLibrary=${initialParams.iconLibrary ?? "lucide"}&style=${initialParams.style ?? "vega"}&font=${initialParams.font ?? "inter"}&baseColor=${initialParams.baseColor ?? "neutral"}`
return (
<div className="relative -mx-1 flex flex-1 flex-col justify-center sm:mx-0">
<div className="ring-foreground/15 3xl:max-h-[1200px] 3xl:max-w-[1800px] relative -z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden rounded-2xl ring-1">
<div className="bg-muted dark:bg-muted/30 absolute inset-0 rounded-2xl" />
<iframe
key={`${params.item}-${iframeKey}`}
ref={iframeRef}
src={iframeSrc}
className="z-10 size-full flex-1"
title="Preview"
/>
<Badge
className="absolute right-2 bottom-2 isolate z-10"
variant="secondary"
>
Preview
</Badge>
</div>
</div>
)
}

View File

@@ -0,0 +1,101 @@
"use client"
import { RADII, type RadiusValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function RadiusPicker({
isMobile,
anchorRef,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentRadius = RADII.find((radius) => radius.name === params.radius)
const defaultRadius = RADII.find((radius) => radius.name === "default")
const otherRadii = RADII.filter((radius) => radius.name !== "default")
return (
<Picker>
<div className="group/picker relative">
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Radius</div>
<div className="text-foreground text-sm font-medium">
{currentRadius?.label}
</div>
</div>
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="text-foreground"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 20v-5C4 8.925 8.925 4 15 4h5"
/>
</svg>
</div>
</PickerTrigger>
<LockButton
param="radius"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentRadius?.name}
onValueChange={(value) => {
setParams({ radius: value as RadiusValue })
}}
>
<PickerGroup>
{defaultRadius && (
<PickerRadioItem
key={defaultRadius.name}
value={defaultRadius.name}
>
<div className="flex flex-col justify-start pointer-coarse:gap-1">
<div>{defaultRadius.label}</div>
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
Use radius from style
</div>
</div>
</PickerRadioItem>
)}
</PickerGroup>
<PickerSeparator />
<PickerGroup>
{otherRadii.map((radius) => (
<PickerRadioItem key={radius.name} value={radius.name}>
{radius.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,73 @@
"use client"
import * as React from "react"
import { Share03Icon, Tick02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function ShareButton() {
const [params] = useDesignSystemSearchParams()
const [hasCopied, setHasCopied] = React.useState(false)
const shareUrl = React.useMemo(() => {
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
return `${origin}/create?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&item=${params.item}`
}, [
params.base,
params.style,
params.baseColor,
params.theme,
params.iconLibrary,
params.font,
params.menuAccent,
params.menuColor,
params.radius,
params.item,
])
React.useEffect(() => {
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
const handleCopy = React.useCallback(() => {
copyToClipboardWithMeta(shareUrl, {
name: "copy_create_share_url",
properties: {
url: shareUrl,
},
})
setHasCopied(true)
}, [shareUrl])
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
className="rounded-lg shadow-none"
onClick={handleCopy}
>
{hasCopied ? (
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
) : (
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
)}
Share
</Button>
</TooltipTrigger>
<TooltipContent>Copy Link</TooltipContent>
</Tooltip>
)
}

View File

@@ -0,0 +1,96 @@
"use client"
import * as React from "react"
import { type Style, type StyleName } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function StylePicker({
styles,
isMobile,
anchorRef,
}: {
styles: readonly Style[]
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentStyle = styles.find((style) => style.name === params.style)
return (
<Picker>
<div className="group/picker relative">
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Style</div>
<div className="text-foreground text-sm font-medium">
{currentStyle?.title}
</div>
</div>
{currentStyle?.icon && (
<div className="absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center">
{React.cloneElement(currentStyle.icon, {
className: "size-4",
})}
</div>
)}
</PickerTrigger>
<LockButton
param="style"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="md:w-64"
>
<PickerRadioGroup
value={currentStyle?.name}
onValueChange={(value) => {
setParams({ style: value as StyleName })
}}
>
<PickerGroup>
{styles.map((style, index) => (
<React.Fragment key={style.name}>
<PickerRadioItem value={style.name}>
<div className="flex items-start gap-2">
{style.icon && (
<div className="flex size-4 translate-y-0.5 items-center justify-center">
{React.cloneElement(style.icon, {
className: "size-4",
})}
</div>
)}
<div className="flex flex-col justify-start pointer-coarse:gap-1">
<div>{style.title}</div>
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
{style.description}
</div>
</div>
</div>
</PickerRadioItem>
{index < styles.length - 1 && (
<PickerSeparator className="opacity-50" />
)}
</React.Fragment>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,94 @@
"use client"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const TEMPLATES = [
{
value: "next",
title: "Next.js",
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Next.js</title><path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z"/></svg>',
},
{
value: "start",
title: "TanStack Start",
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TanStack</title><path d="M11.078.042c.316-.042.65-.014.97-.014 1.181 0 2.341.184 3.472.532a12.3 12.3 0 0 1 3.973 2.086 11.9 11.9 0 0 1 3.432 4.33c1.446 3.15 1.436 6.97-.046 10.107-.958 2.029-2.495 3.727-4.356 4.965-1.518 1.01-3.293 1.629-5.1 1.848-2.298.279-4.784-.129-6.85-1.188-3.88-1.99-6.518-5.994-6.57-10.382-.01-.846.003-1.697.17-2.534.273-1.365.748-2.683 1.463-3.88a12 12 0 0 1 2.966-3.36A12.3 12.3 0 0 1 9.357.3a12 12 0 0 1 1.255-.2l.133-.016zM7.064 19.99c-.535.057-1.098.154-1.557.454.103.025.222 0 .33 0 .258 0 .52-.01.778.002.647.028 1.32.131 1.945.303.8.22 1.505.65 2.275.942.813.307 1.622.402 2.484.402.435 0 .866-.001 1.287-.12-.22-.117-.534-.095-.778-.144a11 11 0 0 1-1.556-.416 12 12 0 0 1-1.093-.467l-.23-.108a15 15 0 0 0-1.012-.44c-.905-.343-1.908-.512-2.873-.408m.808-2.274c-1.059 0-2.13.187-3.083.667q-.346.177-.659.41c-.063.046-.175.106-.199.188s.061.151.11.204c.238-.127.464-.261.718-.357 1.64-.624 3.63-.493 5.268.078.817.285 1.569.712 2.365 1.046.89.374 1.798.616 2.753.74 1.127.147 2.412.028 3.442-.48.362-.179.865-.451 1.018-.847-.189.017-.36.098-.539.154a9 9 0 0 1-.868.222c-.994.2-2.052.24-3.053.06-.943-.17-1.82-.513-2.693-.873l-.111-.046-.223-.092-.112-.046a26 26 0 0 0-1.35-.527c-.89-.31-1.842-.5-2.784-.5M9.728 1.452c-1.27.28-2.407.826-3.502 1.514-.637.4-1.245.81-1.796 1.323-.82.765-1.447 1.695-1.993 2.666-.563 1-.924 2.166-1.098 3.297-.172 1.11-.2 2.277-.004 3.388.245 1.388.712 2.691 1.448 3.897.248-.116.424-.38.629-.557.414-.359.85-.691 1.317-.978a3.5 3.5 0 0 1 .539-.264c.07-.029.187-.055.22-.132.053-.124-.045-.34-.062-.468a7 7 0 0 1-.068-1.109 9.7 9.7 0 0 1 .61-3.177c.29-.76.73-1.45 1.254-2.069.177-.21.365-.405.56-.6.115-.114.258-.212.33-.359-.376 0-.751.108-1.108.218-.769.237-1.518.588-2.155 1.084-.291.226-.504.522-.779.76-.084.073-.235.17-.352.116-.176-.083-.149-.43-.169-.59-.078-.612.154-1.387.45-1.918.473-.852 1.348-1.58 2.376-1.555.444.011.833.166 1.257.266-.107-.153-.252-.264-.389-.39a5.4 5.4 0 0 0-1.107-.8c-.163-.085-.338-.136-.509-.2-.086-.03-.195-.074-.227-.17-.06-.177.26-.342.377-.417.453-.289 1.01-.527 1.556-.54.854-.021 1.688.452 2.04 1.258.123.284.16.583.184.885l.004.057.006.085.002.029.005.057.004.056c.268-.218.457-.54.718-.774.612-.547 1.45-.79 2.245-.544a2.97 2.97 0 0 1 1.71 1.378c.097.173.365.595.171.767-.152.134-.344.03-.504-.026a3 3 0 0 0-.372-.094l-.068-.014-.069-.013a3.9 3.9 0 0 0-1.377-.002c-.282.05-.557.15-.838.192v.06c.768.006 1.51.444 1.89 1.109.157.275.235.59.295.9.075.38.022.796-.082 1.168-.035.125-.098.336-.247.365-.106.02-.195-.085-.256-.155a4.6 4.6 0 0 0-.492-.522 20 20 0 0 0-1.467-1.14c-.267-.19-.56-.44-.868-.556.087.208.171.402.2.63.088.667-.192 1.296-.612 1.798a2.6 2.6 0 0 1-.426.427c-.067.05-.151.114-.24.1-.277-.044-.31-.463-.353-.677-.144-.726-.086-1.447.114-2.158-.178.09-.307.287-.418.45a5.3 5.3 0 0 0-.612 1.138c-.61 1.617-.604 3.51.186 5.066.088.174.221.15.395.15h.157a3 3 0 0 1 .472.018c.08.01.193 0 .257.06.077.072.036.194.018.282-.05.246-.066.469-.066.72.328-.051.419-.576.535-.84.131-.298.265-.597.387-.9.06-.148.14-.314.119-.479-.024-.185-.157-.381-.25-.54-.177-.298-.378-.606-.508-.929-.104-.258-.007-.58.286-.672.161-.05.334.049.439.166.22.244.363.609.523.896l1.249 2.248q.159.286.32.57c.043.074.086.188.173.219.077.028.182-.012.26-.027.198-.04.398-.083.598-.12.24-.043.605-.035.778-.222-.253-.08-.545-.075-.808-.057-.158.01-.333.067-.479-.025-.216-.137-.36-.455-.492-.667-.326-.525-.633-1.057-.945-1.59l-.05-.084-.1-.17q-.075-.126-.149-.255c-.037-.066-.092-.153-.039-.227.056-.076.179-.08.29-.081h.021q.066.001.117-.004a10 10 0 0 1 1.347-.107c-.035-.122-.135-.26-.103-.39.071-.292.49-.383.686-.174.131.14.207.334.292.504.113.223.24.44.361.66.211.383.441.757.658 1.138l.055.094.028.047c.093.156.187.314.238.489-.753-.035-1.318-.909-1.646-1.499-.027.095.016.179.05.27q.103.282.262.54c.152.244.326.495.556.673.408.315.945.317 1.436.283.315-.022.708-.165 1.018-.068s.434.438.25.7c-.138.196-.321.27-.55.3.162.346.373.667.527 1.02.064.146.13.37.283.448.102.051.248.003.358 0-.11-.292-.317-.54-.419-.839.31.015.61.176.898.28.567.202 1.128.424 1.687.648l.258.104c.23.092.462.183.689.283.083.037.198.123.29.07.074-.043.123-.146.169-.215a10.3 10.3 0 0 0 1.393-3.208c.75-2.989.106-6.287-1.695-8.783-.692-.96-1.562-1.789-2.522-2.476-2.401-1.718-5.551-2.407-8.44-1.768m4.908 14.904c-.636.166-1.292.317-1.945.401.086.293.296.577.45.84.059.101.122.237.24.281.132.05.292-.03.417-.072-.058-.158-.155-.3-.235-.45-.033-.06-.084-.133-.056-.206.05-.137.263-.13.381-.153.31-.063.617-.142.928-.204.114-.023.274-.085.389-.047.086.03.138.1.187.174l.022.033q.043.07.097.122c.125.113.313.13.472.162-.097-.219-.259-.41-.362-.63-.06-.127-.11-.315-.242-.388-.182-.102-.557.089-.743.137m-4.01-1.457c-.03.38-.147.689-.33 1.019.21.026.423.036.629.087.154.038.296.11.449.153-.082-.224-.233-.423-.35-.63-.12-.208-.226-.462-.398-.63"/></svg>',
},
{
value: "vite",
title: "Vite",
logo: '<svg xmlns="http://www.w3.org/2000/svg" width="410" height="404" fill="none" viewBox="0 0 410 404"><path fill="var(--foreground)" d="m399.641 59.525-183.998 329.02c-3.799 6.793-13.559 6.833-17.415.073L10.582 59.556C6.38 52.19 12.68 43.266 21.028 44.76l184.195 32.923c1.175.21 2.378.208 3.553-.006l180.343-32.87c8.32-1.517 14.649 7.337 10.522 14.719"/><path fill="var(--background)" d="M292.965 1.574 156.801 28.255a5 5 0 0 0-4.03 4.611l-8.376 141.464c-.197 3.332 2.863 5.918 6.115 5.168l37.91-8.749c3.547-.818 6.752 2.306 6.023 5.873l-11.263 55.153c-.758 3.712 2.727 6.886 6.352 5.785l23.415-7.114c3.63-1.102 7.118 2.081 6.35 5.796l-17.899 86.633c-1.12 5.419 6.088 8.374 9.094 3.728l2.008-3.103 110.954-221.428c1.858-3.707-1.346-7.935-5.418-7.15l-39.022 7.532c-3.667.707-6.787-2.708-5.752-6.296l25.469-88.291c1.036-3.594-2.095-7.012-5.766-6.293"/></svg>',
},
] as const
export function TemplatePicker({
isMobile,
anchorRef,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentTemplate = TEMPLATES.find(
(template) => template.value === params.template
)
return (
<Picker>
<PickerTrigger className="hidden md:flex">
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Template</div>
<div className="text-foreground text-sm font-medium">
{currentTemplate?.title}
</div>
</div>
{currentTemplate?.logo && (
<div
className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 size-4 -translate-y-1/2 *:[svg]:size-4"
dangerouslySetInnerHTML={{
__html: currentTemplate.logo,
}}
/>
)}
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={params.template}
onValueChange={(value) => {
setParams({
template: value as "next" | "start" | "vite",
})
}}
>
<PickerGroup>
{TEMPLATES.map((template) => (
<PickerRadioItem key={template.value} value={template.value}>
{template.logo && (
<div
className="text-foreground *:[svg]:text-foreground! size-4 shrink-0 [&_svg]:size-4"
dangerouslySetInnerHTML={{
__html: template.logo,
}}
/>
)}
{template.title}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,166 @@
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import { useMounted } from "@/hooks/use-mounted"
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function ThemePicker({
themes,
isMobile,
anchorRef,
}: {
themes: readonly Theme[]
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const { resolvedTheme } = useTheme()
const mounted = useMounted()
const [params, setParams] = useDesignSystemSearchParams()
const currentTheme = React.useMemo(
() => themes.find((theme) => theme.name === params.theme),
[themes, params.theme]
)
const currentThemeIsBaseColor = React.useMemo(
() => BASE_COLORS.find((baseColor) => baseColor.name === params.theme),
[params.theme]
)
React.useEffect(() => {
if (!currentTheme && themes.length > 0) {
setParams({ theme: themes[0].name })
}
}, [currentTheme, themes, setParams])
return (
<Picker>
<div className="group/picker relative">
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Theme</div>
<div className="text-foreground text-sm font-medium">
{currentTheme?.title}
</div>
</div>
{mounted && resolvedTheme && (
<div
style={
{
"--color":
currentTheme?.cssVars?.[
resolvedTheme as "light" | "dark"
]?.[
currentThemeIsBaseColor ? "muted-foreground" : "primary"
],
} as React.CSSProperties
}
className="absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color)"
/>
)}
</PickerTrigger>
<LockButton
param="theme"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-96"
>
<PickerRadioGroup
value={currentTheme?.name}
onValueChange={(value) => {
setParams({ theme: value as ThemeName })
}}
>
<PickerGroup>
{themes
.filter((theme) =>
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
)
.map((theme) => {
const isBaseColor = BASE_COLORS.find(
(baseColor) => baseColor.name === theme.name
)
return (
<PickerRadioItem key={theme.name} value={theme.name}>
<div className="flex items-start gap-2">
{mounted && resolvedTheme && (
<div
style={
{
"--color":
theme.cssVars?.[
resolvedTheme as "light" | "dark"
]?.[
isBaseColor ? "muted-foreground" : "primary"
],
} as React.CSSProperties
}
className="size-4 translate-y-1 rounded-full bg-(--color)"
/>
)}
<div className="flex flex-col justify-start pointer-coarse:gap-1">
<div>{theme.title}</div>
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
Match base color
</div>
</div>
</div>
</PickerRadioItem>
)
})}
</PickerGroup>
<PickerSeparator />
<PickerGroup>
{themes
.filter(
(theme) =>
!BASE_COLORS.find(
(baseColor) => baseColor.name === theme.name
)
)
.map((theme) => {
return (
<PickerRadioItem key={theme.name} value={theme.name}>
<div className="flex items-center gap-2">
{mounted && resolvedTheme && (
<div
style={
{
"--color":
theme.cssVars?.[
resolvedTheme as "light" | "dark"
]?.["primary"],
} as React.CSSProperties
}
className="size-4 rounded-full bg-(--color)"
/>
)}
{theme.title}
</div>
</PickerRadioItem>
)
})}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
)
}

View File

@@ -0,0 +1,273 @@
"use client"
import * as React from "react"
import {
ComputerTerminal01Icon,
Copy01Icon,
Tick02Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { toast } from "sonner"
import { useConfig } from "@/hooks/use-config"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/registry/new-york-v4/ui/dialog"
import {
Field,
FieldGroup,
FieldLabel,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/new-york-v4/ui/tabs"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const TEMPLATES = [
{
value: "next",
title: "Next.js",
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Next.js</title><path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" fill="currentColor"/></svg>',
},
{
value: "start",
title: "TanStack Start",
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TanStack</title><path d="M11.078.042c.316-.042.65-.014.97-.014 1.181 0 2.341.184 3.472.532a12.3 12.3 0 0 1 3.973 2.086 11.9 11.9 0 0 1 3.432 4.33c1.446 3.15 1.436 6.97-.046 10.107-.958 2.029-2.495 3.727-4.356 4.965-1.518 1.01-3.293 1.629-5.1 1.848-2.298.279-4.784-.129-6.85-1.188-3.88-1.99-6.518-5.994-6.57-10.382-.01-.846.003-1.697.17-2.534.273-1.365.748-2.683 1.463-3.88a12 12 0 0 1 2.966-3.36A12.3 12.3 0 0 1 9.357.3a12 12 0 0 1 1.255-.2l.133-.016zM7.064 19.99c-.535.057-1.098.154-1.557.454.103.025.222 0 .33 0 .258 0 .52-.01.778.002.647.028 1.32.131 1.945.303.8.22 1.505.65 2.275.942.813.307 1.622.402 2.484.402.435 0 .866-.001 1.287-.12-.22-.117-.534-.095-.778-.144a11 11 0 0 1-1.556-.416 12 12 0 0 1-1.093-.467l-.23-.108a15 15 0 0 0-1.012-.44c-.905-.343-1.908-.512-2.873-.408m.808-2.274c-1.059 0-2.13.187-3.083.667q-.346.177-.659.41c-.063.046-.175.106-.199.188s.061.151.11.204c.238-.127.464-.261.718-.357 1.64-.624 3.63-.493 5.268.078.817.285 1.569.712 2.365 1.046.89.374 1.798.616 2.753.74 1.127.147 2.412.028 3.442-.48.362-.179.865-.451 1.018-.847-.189.017-.36.098-.539.154a9 9 0 0 1-.868.222c-.994.2-2.052.24-3.053.06-.943-.17-1.82-.513-2.693-.873l-.111-.046-.223-.092-.112-.046a26 26 0 0 0-1.35-.527c-.89-.31-1.842-.5-2.784-.5M9.728 1.452c-1.27.28-2.407.826-3.502 1.514-.637.4-1.245.81-1.796 1.323-.82.765-1.447 1.695-1.993 2.666-.563 1-.924 2.166-1.098 3.297-.172 1.11-.2 2.277-.004 3.388.245 1.388.712 2.691 1.448 3.897.248-.116.424-.38.629-.557.414-.359.85-.691 1.317-.978a3.5 3.5 0 0 1 .539-.264c.07-.029.187-.055.22-.132.053-.124-.045-.34-.062-.468a7 7 0 0 1-.068-1.109 9.7 9.7 0 0 1 .61-3.177c.29-.76.73-1.45 1.254-2.069.177-.21.365-.405.56-.6.115-.114.258-.212.33-.359-.376 0-.751.108-1.108.218-.769.237-1.518.588-2.155 1.084-.291.226-.504.522-.779.76-.084.073-.235.17-.352.116-.176-.083-.149-.43-.169-.59-.078-.612.154-1.387.45-1.918.473-.852 1.348-1.58 2.376-1.555.444.011.833.166 1.257.266-.107-.153-.252-.264-.389-.39a5.4 5.4 0 0 0-1.107-.8c-.163-.085-.338-.136-.509-.2-.086-.03-.195-.074-.227-.17-.06-.177.26-.342.377-.417.453-.289 1.01-.527 1.556-.54.854-.021 1.688.452 2.04 1.258.123.284.16.583.184.885l.004.057.006.085.002.029.005.057.004.056c.268-.218.457-.54.718-.774.612-.547 1.45-.79 2.245-.544a2.97 2.97 0 0 1 1.71 1.378c.097.173.365.595.171.767-.152.134-.344.03-.504-.026a3 3 0 0 0-.372-.094l-.068-.014-.069-.013a3.9 3.9 0 0 0-1.377-.002c-.282.05-.557.15-.838.192v.06c.768.006 1.51.444 1.89 1.109.157.275.235.59.295.9.075.38.022.796-.082 1.168-.035.125-.098.336-.247.365-.106.02-.195-.085-.256-.155a4.6 4.6 0 0 0-.492-.522 20 20 0 0 0-1.467-1.14c-.267-.19-.56-.44-.868-.556.087.208.171.402.2.63.088.667-.192 1.296-.612 1.798a2.6 2.6 0 0 1-.426.427c-.067.05-.151.114-.24.1-.277-.044-.31-.463-.353-.677-.144-.726-.086-1.447.114-2.158-.178.09-.307.287-.418.45a5.3 5.3 0 0 0-.612 1.138c-.61 1.617-.604 3.51.186 5.066.088.174.221.15.395.15h.157a3 3 0 0 1 .472.018c.08.01.193 0 .257.06.077.072.036.194.018.282-.05.246-.066.469-.066.72.328-.051.419-.576.535-.84.131-.298.265-.597.387-.9.06-.148.14-.314.119-.479-.024-.185-.157-.381-.25-.54-.177-.298-.378-.606-.508-.929-.104-.258-.007-.58.286-.672.161-.05.334.049.439.166.22.244.363.609.523.896l1.249 2.248q.159.286.32.57c.043.074.086.188.173.219.077.028.182-.012.26-.027.198-.04.398-.083.598-.12.24-.043.605-.035.778-.222-.253-.08-.545-.075-.808-.057-.158.01-.333.067-.479-.025-.216-.137-.36-.455-.492-.667-.326-.525-.633-1.057-.945-1.59l-.05-.084-.1-.17q-.075-.126-.149-.255c-.037-.066-.092-.153-.039-.227.056-.076.179-.08.29-.081h.021q.066.001.117-.004a10 10 0 0 1 1.347-.107c-.035-.122-.135-.26-.103-.39.071-.292.49-.383.686-.174.131.14.207.334.292.504.113.223.24.44.361.66.211.383.441.757.658 1.138l.055.094.028.047c.093.156.187.314.238.489-.753-.035-1.318-.909-1.646-1.499-.027.095.016.179.05.27q.103.282.262.54c.152.244.326.495.556.673.408.315.945.317 1.436.283.315-.022.708-.165 1.018-.068s.434.438.25.7c-.138.196-.321.27-.55.3.162.346.373.667.527 1.02.064.146.13.37.283.448.102.051.248.003.358 0-.11-.292-.317-.54-.419-.839.31.015.61.176.898.28.567.202 1.128.424 1.687.648l.258.104c.23.092.462.183.689.283.083.037.198.123.29.07.074-.043.123-.146.169-.215a10.3 10.3 0 0 0 1.393-3.208c.75-2.989.106-6.287-1.695-8.783-.692-.96-1.562-1.789-2.522-2.476-2.401-1.718-5.551-2.407-8.44-1.768m4.908 14.904c-.636.166-1.292.317-1.945.401.086.293.296.577.45.84.059.101.122.237.24.281.132.05.292-.03.417-.072-.058-.158-.155-.3-.235-.45-.033-.06-.084-.133-.056-.206.05-.137.263-.13.381-.153.31-.063.617-.142.928-.204.114-.023.274-.085.389-.047.086.03.138.1.187.174l.022.033q.043.07.097.122c.125.113.313.13.472.162-.097-.219-.259-.41-.362-.63-.06-.127-.11-.315-.242-.388-.182-.102-.557.089-.743.137m-4.01-1.457c-.03.38-.147.689-.33 1.019.21.026.423.036.629.087.154.038.296.11.449.153-.082-.224-.233-.423-.35-.63-.12-.208-.226-.462-.398-.63" fill="currentColor"/></svg>',
},
{
value: "vite",
title: "Vite",
logo: '<svg xmlns="http://www.w3.org/2000/svg" width="410" height="404" fill="none" viewBox="0 0 410 404"><path fill="var(--foreground)" d="m399.641 59.525-183.998 329.02c-3.799 6.793-13.559 6.833-17.415.073L10.582 59.556C6.38 52.19 12.68 43.266 21.028 44.76l184.195 32.923c1.175.21 2.378.208 3.553-.006l180.343-32.87c8.32-1.517 14.649 7.337 10.522 14.719"/><path fill="var(--background)" d="M292.965 1.574 156.801 28.255a5 5 0 0 0-4.03 4.611l-8.376 141.464c-.197 3.332 2.863 5.918 6.115 5.168l37.91-8.749c3.547-.818 6.752 2.306 6.023 5.873l-11.263 55.153c-.758 3.712 2.727 6.886 6.352 5.785l23.415-7.114c3.63-1.102 7.118 2.081 6.35 5.796l-17.899 86.633c-1.12 5.419 6.088 8.374 9.094 3.728l2.008-3.103 110.954-221.428c1.858-3.707-1.346-7.935-5.418-7.15l-39.022 7.532c-3.667.707-6.787-2.708-5.752-6.296l25.469-88.291c1.036-3.594-2.095-7.012-5.766-6.293"/></svg>',
},
] as const
export function ToolbarControls() {
const [open, setOpen] = React.useState(false)
const [params, setParams] = useDesignSystemSearchParams()
const [config, setConfig] = useConfig()
const [hasCopied, setHasCopied] = React.useState(false)
const packageManager = config.packageManager || "pnpm"
const commands = React.useMemo(() => {
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
const url = `${origin}/init?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&template=${params.template}`
const templateFlag = params.template ? ` --template ${params.template}` : ""
return {
pnpm: `pnpm dlx shadcn@latest create --preset "${url}"${templateFlag}`,
npm: `npx shadcn@latest create --preset "${url}"${templateFlag}`,
yarn: `yarn dlx shadcn@latest create --preset "${url}"${templateFlag}`,
bun: `bunx --bun shadcn@latest create --preset "${url}"${templateFlag}`,
}
}, [
params.base,
params.style,
params.baseColor,
params.theme,
params.iconLibrary,
params.font,
params.menuAccent,
params.menuColor,
params.radius,
params.template,
])
const command = commands[packageManager]
React.useEffect(() => {
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
const handleCopy = React.useCallback(() => {
const properties: Record<string, string> = {
command,
}
if (params.template) {
properties.template = params.template
}
copyToClipboardWithMeta(command, {
name: "copy_npm_command",
properties,
})
setOpen(false)
setHasCopied(true)
toast("Command copied to clipboard.", {
description:
"Paste and run the command in your terminal to create a new shadcn/ui project.",
position: "bottom-center",
classNames: {
content: "rounded-xl",
toast: "rounded-xl!",
description: "text-sm/leading-normal!",
},
})
}, [command, params.template, setOpen])
const handleCopyFromTabs = React.useCallback(() => {
const properties: Record<string, string> = {
command,
}
if (params.template) {
properties.template = params.template
}
copyToClipboardWithMeta(command, {
name: "copy_npm_command",
properties,
})
setHasCopied(true)
}, [command, params.template])
const selectedTemplate = TEMPLATES.find(
(template) => template.value === params.template
)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size="sm" className="hidden h-[31px] rounded-lg pl-2 md:flex">
<HugeiconsIcon
icon={ComputerTerminal01Icon}
className="hidden xl:flex"
/>
Create Project
</Button>
</DialogTrigger>
<DialogContent className="dialog-ring min-w-0 overflow-hidden rounded-xl sm:max-w-md">
<DialogHeader>
<DialogTitle>Create Project</DialogTitle>
<DialogDescription className="text-balance">
Select a template and run this command to create a{" "}
{selectedTemplate?.title} + shadcn/ui project.
</DialogDescription>
</DialogHeader>
<FieldGroup>
<Field>
<FieldLabel htmlFor="template" className="sr-only">
Template
</FieldLabel>
<RadioGroup
id="template"
value={params.template}
onValueChange={(value) => {
setParams({
template: value as "next" | "start" | "vite",
})
}}
className="grid grid-cols-3 gap-2"
>
{TEMPLATES.map((template) => (
<FieldLabel
key={template.value}
htmlFor={template.value}
className="rounded-lg!"
>
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!">
<RadioGroupItem
value={template.value}
id={template.value}
className="sr-only"
/>
{template.logo && (
<div
className="text-foreground *:[svg]:text-foreground! size-6 [&_svg]:size-6"
dangerouslySetInnerHTML={{
__html: template.logo,
}}
/>
)}
<FieldTitle>{template.title}</FieldTitle>
</Field>
</FieldLabel>
))}
</RadioGroup>
</Field>
</FieldGroup>
<Tabs
value={packageManager}
onValueChange={(value) => {
setConfig({
...config,
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
})
}}
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
>
<div className="flex items-center gap-2 p-2">
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
<TabsTrigger value="npm">npm</TabsTrigger>
<TabsTrigger value="yarn">yarn</TabsTrigger>
<TabsTrigger value="bun">bun</TabsTrigger>
</TabsList>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto size-7 rounded-lg"
onClick={handleCopyFromTabs}
>
{hasCopied ? (
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
) : (
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
)}
<span className="sr-only">Copy command</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied!" : "Copy command"}
</TooltipContent>
</Tooltip>
</div>
{Object.entries(commands).map(([key, cmd]) => {
return (
<TabsContent key={key} value={key}>
<div className="bg-surface border-border/50 text-surface-foreground relative overflow-hidden border-t px-3 py-3">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">
{cmd}
</code>
</div>
</div>
</TabsContent>
)
})}
</Tabs>
<DialogFooter className="bg-muted/50 -mx-6 mt-2 -mb-6 flex flex-col gap-2 border-t p-6 sm:flex-col">
<Button
size="sm"
onClick={handleCopy}
className="h-9 w-full rounded-lg"
>
Copy Command
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,56 @@
"use client"
import { cn } from "@/lib/utils"
import { useIsMobile } from "@/hooks/use-mobile"
import { useMounted } from "@/hooks/use-mounted"
import { Icons } from "@/components/icons"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Skeleton } from "@/registry/new-york-v4/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function V0Button({ className }: { className?: string }) {
const [params, setParams] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const isMounted = useMounted()
const url = `${process.env.NEXT_PUBLIC_APP_URL}/create/v0?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&item=${params.item}`
console.log(url)
if (!isMounted) {
return <Skeleton className="h-8 w-24 rounded-lg" />
}
return (
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant={isMobile ? "default" : "outline"}
className={cn(
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px]",
className
)}
asChild
>
<a
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`}
target="_blank"
>
Open in <Icons.v0 className="size-5" />
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Open current design in v0</p>
</TooltipContent>
</Tooltip>
</>
)
}

View File

@@ -0,0 +1,69 @@
"use client"
import * as React from "react"
import { Icons } from "@/components/icons"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/registry/new-york-v4/ui/dialog"
const STORAGE_KEY = "shadcn-create-welcome-dialog"
export function WelcomeDialog() {
const [isOpen, setIsOpen] = React.useState(false)
React.useEffect(() => {
const dismissed = localStorage.getItem(STORAGE_KEY)
if (!dismissed) {
setIsOpen(true)
}
}, [])
const handleOpenChange = (open: boolean) => {
setIsOpen(open)
if (!open) {
localStorage.setItem(STORAGE_KEY, "true")
}
}
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent
showCloseButton={false}
className="dialog-ring max-w-[23rem] min-w-0 gap-0 overflow-hidden rounded-xl p-0 sm:max-w-sm dark:bg-neutral-900"
>
<div className="flex aspect-[2/1.2] w-full items-center justify-center rounded-t-xl bg-neutral-950 text-center text-neutral-100 sm:aspect-[2/1]">
<div className="font-mono text-2xl font-bold">
<Icons.logo className="size-12" />
</div>
</div>
<DialogHeader className="gap-1 p-4">
<DialogTitle className="text-left text-base">
Build your own shadcn/ui
</DialogTitle>
<DialogDescription className="text-foreground text-left leading-relaxed">
Customize everything from the ground up. Pick your component
library, font, color scheme, and more.
</DialogDescription>
<DialogDescription className="text-foreground mt-2 text-left leading-relaxed font-medium">
Available for Next.js, Vite, TanStack Start, and v0.
</DialogDescription>
</DialogHeader>
<DialogFooter className="p-4 pt-0">
<DialogClose asChild>
<Button className="w-full rounded-lg shadow-none">
Get Started
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,9 @@
import { LocksProvider } from "@/app/(create)/hooks/use-locks"
export default function CreateLayout({
children,
}: {
children: React.ReactNode
}) {
return <LocksProvider>{children}</LocksProvider>
}

View File

@@ -0,0 +1,142 @@
import { type Metadata } from "next"
import Link from "next/link"
import { ArrowLeftIcon } from "lucide-react"
import type { SearchParams } from "nuqs/server"
import { siteConfig } from "@/lib/config"
import { absoluteUrl } from "@/lib/utils"
import { ModeSwitcher } from "@/components/mode-switcher"
import { SiteConfig } from "@/components/site-config"
import { BASES } from "@/registry/config"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar"
import { Customizer } from "@/app/(create)/components/customizer"
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
import { ItemExplorer } from "@/app/(create)/components/item-explorer"
import { ItemPicker } from "@/app/(create)/components/item-picker"
import { Preview } from "@/app/(create)/components/preview"
import { ShareButton } from "@/app/(create)/components/share-button"
import { ToolbarControls } from "@/app/(create)/components/toolbar-controls"
import { V0Button } from "@/app/(create)/components/v0-button"
import { WelcomeDialog } from "@/app/(create)/components/welcome-dialog"
import { getItemsForBase } from "@/app/(create)/lib/api"
import { loadDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export const revalidate = false
export const dynamic = "force-static"
export const metadata: Metadata = {
title: "New Project",
description:
"Customize everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.",
openGraph: {
title: "New Project",
description:
"Customize everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.",
type: "website",
url: absoluteUrl("/create"),
images: [
{
url: siteConfig.ogImage,
width: 1200,
height: 630,
alt: siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
title: "New Project",
description:
"Customize everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.",
images: [siteConfig.ogImage],
creator: "@shadcn",
},
}
export default async function CreatePage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}) {
const params = await loadDesignSystemSearchParams(searchParams)
const base = BASES.find((b) => b.name === params.base) ?? BASES[0]
const items = await getItemsForBase(base.name)
const filteredItems = items
.filter((item) => item !== null)
.map((item) => ({
name: item.name,
title: item.title,
type: item.type,
}))
return (
<div
data-slot="layout"
className="section-soft relative z-10 flex min-h-svh flex-col"
>
<header className="sticky top-0 z-50 w-full">
<div className="container-wrapper 3xl:fixed:px-0 px-6">
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
<div className="flex items-center xl:w-1/3">
<Button
asChild
variant="outline"
size="sm"
className="rounded-lg shadow-none"
>
<Link href="/">
<ArrowLeftIcon />
Back
</Link>
</Button>
<Separator
orientation="vertical"
className="mx-2 hidden sm:mx-4 lg:flex"
/>
<div className="text-muted-foreground hidden text-sm font-medium lg:flex">
New Project
</div>
</div>
<div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center gap-2 px-4.5 pb-4 sm:static sm:justify-end sm:p-0 lg:ml-0 xl:justify-center">
<ItemPicker items={filteredItems} />
<CustomizerControls className="sm:hidden" />
<Separator
orientation="vertical"
className="mr-2 hidden sm:flex xl:hidden"
/>
</div>
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end xl:ml-auto xl:w-1/3">
<SiteConfig className="3xl:flex hidden" />
<Separator orientation="vertical" className="3xl:flex hidden" />
<ModeSwitcher />
<Separator
orientation="vertical"
className="mr-0 -ml-2 sm:ml-0"
/>
<ShareButton />
<V0Button />
<ToolbarControls />
</div>
</div>
</div>
</header>
<main className="flex flex-1 flex-col pb-16 sm:pb-0">
<SidebarProvider className="flex h-auto min-h-min flex-1 flex-col items-start overflow-hidden px-0">
<div
data-slot="designer"
className="3xl:fixed:container flex w-full flex-1 flex-col gap-2 p-6 pt-1 pb-4 [--sidebar-width:--spacing(40)] sm:gap-2 sm:pt-2 md:flex-row md:pb-6 2xl:gap-6"
>
<ItemExplorer base={base.name} items={filteredItems} />
<Preview />
<Customizer />
</div>
</SidebarProvider>
<WelcomeDialog />
</main>
</div>
)
}

View File

@@ -0,0 +1,391 @@
import { NextResponse, type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import dedent from "dedent"
import {
registryItemFileSchema,
registryItemSchema,
type configSchema,
type RegistryItem,
} from "shadcn/schema"
import { transformIcons, transformMenu, transformRender } from "shadcn/utils"
import { Project, ScriptKind } from "ts-morph"
import { z } from "zod"
import {
buildRegistryBase,
designSystemConfigSchema,
fonts,
type DesignSystemConfig,
} from "@/registry/config"
const { Index } = await import("@/registry/bases/__index__")
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const parseResult = designSystemConfigSchema.safeParse({
base: searchParams.get("base"),
style: searchParams.get("style"),
iconLibrary: searchParams.get("iconLibrary"),
baseColor: searchParams.get("baseColor"),
theme: searchParams.get("theme"),
font: searchParams.get("font"),
item: searchParams.get("item"),
menuAccent: searchParams.get("menuAccent"),
menuColor: searchParams.get("menuColor"),
radius: searchParams.get("radius"),
})
if (!parseResult.success) {
return NextResponse.json(
{ error: parseResult.error.issues[0].message },
{ status: 400 }
)
}
const designSystemConfig = parseResult.data
const registryBase = buildRegistryBase(designSystemConfig)
const validateResult = registryItemSchema.safeParse(registryBase)
if (!validateResult.success) {
return NextResponse.json(
{
error: "Invalid registry base item",
details: validateResult.error.format(),
},
{ status: 500 }
)
}
track("create_open_in_v0", designSystemConfig)
const payload = await buildV0Payload(designSystemConfig)
return NextResponse.json(payload)
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "An unknown error occurred",
},
{ status: 500 }
)
}
}
async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
const files: z.infer<typeof registryItemFileSchema>[] = []
// Build globals.css file.
files.push(buildGlobalsCss(designSystemConfig))
// Build layout.tsx file.
files.push(buildLayoutFile(designSystemConfig))
// Build component files.
const componentFiles = await buildComponentFiles(designSystemConfig)
files.push(...componentFiles)
return registryItemSchema.parse({
name: designSystemConfig.item ?? "Item",
type: "registry:item",
files,
})
}
function buildGlobalsCss(designSystemConfig: DesignSystemConfig) {
const registryBase = buildRegistryBase(designSystemConfig)
const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
const darkVars = Object.entries(registryBase.cssVars?.dark ?? {})
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
const content = dedent`@import "tailwindcss";
@import "tw-animate-css";
/* @import "shadcn/tailwind.css"; */
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
:root {
${lightVars}
}
.dark {
${darkVars}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
`
return registryItemFileSchema.parse({
path: "app/globals.css",
type: "registry:file",
target: "app/globals.css",
content,
})
}
function buildLayoutFile(designSystemConfig: DesignSystemConfig) {
const font = fonts.find(
(font) => font.name === `font-${designSystemConfig.font}`
)
if (!font) {
throw new Error(`Font "${designSystemConfig.font}" not found`)
}
const content = dedent`
import type { Metadata } from "next";
import { ${font.font.import} } from "next/font/google";
import "./globals.css";
const fontSans = ${font.font.import}({subsets:['latin'],variable:'--font-sans'});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={fontSans.variable}>
<body
className="antialiased"
>
{children}
</body>
</html>
);
}
`
return registryItemFileSchema.parse({
path: "app/layout.tsx",
type: "registry:page",
target: "app/layout.tsx",
content,
})
}
async function buildComponentFiles(designSystemConfig: DesignSystemConfig) {
const files = []
const allItemsForBase = Object.values(Index[designSystemConfig.base])
.filter(
(item: RegistryItem) =>
item.type === "registry:ui" || item.name === "example"
)
.map((item) => item.name)
const registryItemFiles = await Promise.all(
allItemsForBase.map(async (name) => {
const file = await getRegistryItemFile(name, designSystemConfig)
return file
})
)
files.push(...registryItemFiles)
const pageFile = {
path: "app/page.tsx",
type: "registry:page",
target: "app/page.tsx",
content: dedent`
import { Button } from "@/components/ui/button";
export default function Page() {
return <Button>Click me</Button>
}
`,
}
// Build the actual item component.
if (designSystemConfig.item) {
const itemComponentFile = await getRegistryItemFile(
designSystemConfig.item,
designSystemConfig
)
if (itemComponentFile) {
// Find the export default function from the component file.
const exportDefault = itemComponentFile.content.match(
/export default function (\w+)/
)
if (exportDefault) {
const functionName = exportDefault[1]
// Replace the export default function with a named export.
itemComponentFile.content = itemComponentFile.content.replace(
/export default function (\w+)/,
`export function ${functionName}`
)
// Import and render the item on the page.
pageFile.content = dedent`import { ${functionName} } from "@/components/${designSystemConfig.item}";
export default function Page() {
return <${functionName} />
}`
}
files.push({
...itemComponentFile,
target: `components/${designSystemConfig.item}.tsx`,
type: "registry:component",
})
}
}
files.push(pageFile)
return z.array(registryItemFileSchema).parse(files)
}
async function getRegistryItemFile(
name: string,
designSystemConfig: DesignSystemConfig
) {
const response = await fetch(
`${process.env.NEXT_PUBLIC_APP_URL}/r/styles/${designSystemConfig.base}-${designSystemConfig.style}/${name}.json`
)
if (!response.ok) {
throw new Error(`Failed to fetch registry item: ${response.statusText}`)
}
const json = await response.json()
const item = registryItemSchema.parse(json)
// Build a v0 config i.e components.json
const config = {
$schema: "https://ui.shadcn.com/schema.json",
style: `${designSystemConfig.base}-${designSystemConfig.style}`,
rsc: true,
tsx: true,
tailwind: {
config: "",
css: "app/globals.css",
baseColor: designSystemConfig.baseColor,
cssVariables: true,
prefix: "",
},
iconLibrary: designSystemConfig.iconLibrary,
aliases: {
components: "@/components",
utils: "@/lib/utils",
ui: "@/components/ui",
lib: "@/lib",
hooks: "@/hooks",
},
menuAccent: designSystemConfig.menuAccent,
menuColor: designSystemConfig.menuColor,
resolvedPaths: {
cwd: "/",
tailwindConfig: "./tailwind.config.js",
tailwindCss: "./globals.css",
utils: "./lib/utils",
components: "./components",
lib: "./lib",
hooks: "./hooks",
ui: "./components/ui",
},
} satisfies z.infer<typeof configSchema>
const file = item.files?.[0]
if (!file?.content) {
return null
}
const content = await transformFileContent(file.content, config)
return {
...file,
target:
name === "example"
? "components/example.tsx"
: `components/ui/${name}.tsx`,
type: name === "example" ? "registry:component" : "registry:ui",
content,
}
}
const transformers = [transformIcons, transformMenu, transformRender]
async function transformFileContent(
content: string,
config: z.infer<typeof configSchema>
) {
const project = new Project({
compilerOptions: {},
})
const sourceFile = project.createSourceFile("component.tsx", content, {
scriptKind: ScriptKind.TSX,
})
for (const transformer of transformers) {
await transformer({
filename: "component.tsx",
raw: content,
sourceFile,
config,
})
}
return sourceFile.getText()
}

View File

@@ -0,0 +1,41 @@
"use client"
import * as React from "react"
import {
sendToIframe,
sendToParent,
useParentMessageListener,
} from "@/app/(create)/hooks/use-iframe-sync"
const MESSAGE_TYPE = "canva-zoom"
export type ZoomCommand =
| { type: "ZOOM_IN" }
| { type: "ZOOM_OUT" }
| { type: "ZOOM_SET"; value: number }
| { type: "ZOOM_FIT" }
| { type: "RESET" }
export function sendCanvaZoomCommand(
iframe: HTMLIFrameElement | null,
command: ZoomCommand
) {
sendToIframe(iframe, MESSAGE_TYPE, { command })
}
export function sendCanvaZoomUpdate(zoom: number) {
sendToParent(MESSAGE_TYPE, { zoom })
}
export function useCanvaZoomSync() {
const [zoom, setZoom] = React.useState(1)
useParentMessageListener<{ zoom: number }>(MESSAGE_TYPE, (data) => {
if (typeof data.zoom === "number") {
setZoom(data.zoom)
}
})
return zoom
}

View File

@@ -0,0 +1,44 @@
"use client"
import {
createIframeSyncStore,
useIframeSyncAll,
useIframeSyncValue,
} from "@/app/(create)/hooks/use-iframe-sync"
import {
loadDesignSystemSearchParams,
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
const MESSAGE_TYPE = "design-system-params"
const getInitialValues = (): DesignSystemSearchParams => {
const initialValues = loadDesignSystemSearchParams(
typeof window === "undefined" ? "" : location.search
)
// Override defaults for iframe use case
initialValues.item = "cover-example"
return initialValues
}
const designSystemStore = createIframeSyncStore(
MESSAGE_TYPE,
getInitialValues()
)
export function useDesignSystemSync() {
const [urlParams] = useDesignSystemSearchParams()
const keys = Object.keys(urlParams) as (keyof DesignSystemSearchParams)[]
return useIframeSyncAll(designSystemStore, keys, urlParams)
}
export function useDesignSystemParam<K extends keyof DesignSystemSearchParams>(
key: K
) {
const [urlParams] = useDesignSystemSearchParams()
return useIframeSyncValue(designSystemStore, key, urlParams[key])
}

View File

@@ -0,0 +1,204 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client"
import * as React from "react"
export const isInIframe = () => {
if (typeof window === "undefined") {
return false
}
return window.self !== window.top
}
export function createIframeSyncStore<T extends Record<string, any>>(
messageType: string,
defaultValues: T
) {
const store = new Map<keyof T, any>()
const listeners = new Map<keyof T, Set<() => void>>()
if (typeof window !== "undefined") {
Object.entries(defaultValues).forEach(([key, value]) => {
store.set(key as keyof T, value)
})
}
if (typeof window !== "undefined" && isInIframe()) {
window.addEventListener("message", (event: MessageEvent) => {
if (event.data.type === messageType && event.data.params) {
Object.keys(event.data.params).forEach((key) => {
const newValue = event.data.params[key]
const oldValue = store.get(key as keyof T)
if (newValue !== oldValue) {
store.set(key as keyof T, newValue)
const keyListeners = listeners.get(key as keyof T)
if (keyListeners) {
keyListeners.forEach((listener) => listener())
}
}
})
}
})
}
return {
store,
listeners,
subscribe(key: keyof T, callback: () => void) {
if (!isInIframe()) return () => {}
if (!listeners.has(key)) {
listeners.set(key, new Set())
}
const keyListeners = listeners.get(key)!
keyListeners.add(callback)
return () => {
keyListeners.delete(callback)
if (keyListeners.size === 0) {
listeners.delete(key)
}
}
},
subscribeAll(keys: (keyof T)[], callback: () => void) {
if (!isInIframe()) return () => {}
keys.forEach((key) => {
if (!listeners.has(key)) {
listeners.set(key, new Set())
}
listeners.get(key)!.add(callback)
})
return () => {
keys.forEach((key) => {
const keyListeners = listeners.get(key)
if (keyListeners) {
keyListeners.delete(callback)
if (keyListeners.size === 0) {
listeners.delete(key)
}
}
})
}
},
get(key: keyof T) {
return store.get(key) as T[keyof T]
},
getAll() {
const result = {} as T
store.forEach((value, key) => {
result[key as keyof T] = value
})
return result
},
}
}
export function useIframeSyncValue<T>(
store: ReturnType<typeof createIframeSyncStore<any>>,
key: string,
urlValue: T
) {
const subscribe = React.useCallback(
(callback: () => void) => {
return store.subscribe(key, callback)
},
[store, key]
)
const getSnapshot = React.useCallback(() => {
if (!isInIframe()) {
return urlValue
}
return store.get(key) as T
}, [store, key, urlValue])
const getServerSnapshot = React.useCallback(() => {
return urlValue
}, [urlValue])
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
}
export function useIframeSyncAll<T extends Record<string, any>>(
store: ReturnType<typeof createIframeSyncStore<T>>,
keys: (keyof T)[],
urlValues: T
) {
const subscribe = React.useCallback(
(callback: () => void) => {
return store.subscribeAll(keys, callback)
},
[store, keys]
)
const getSnapshot = React.useCallback(() => {
if (!isInIframe()) {
return urlValues
}
return store.getAll()
}, [store, urlValues])
const getServerSnapshot = React.useCallback(() => {
return urlValues
}, [urlValues])
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
}
export function useParentMessageListener<T>(
messageType: string,
onMessage: (data: T) => void
) {
React.useEffect(() => {
if (isInIframe()) {
return
}
const handleMessage = (event: MessageEvent) => {
if (event.data.type === messageType) {
onMessage(event.data)
}
}
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
}, [messageType, onMessage])
}
export function sendToIframe(
iframe: HTMLIFrameElement | null,
messageType: string,
data: any
) {
if (!iframe || !iframe.contentWindow) {
return
}
iframe.contentWindow.postMessage(
{
type: messageType,
...data,
},
"*"
)
}
export function sendToParent(messageType: string, data: any) {
if (!isInIframe()) {
return
}
window.parent.postMessage(
{
type: messageType,
...data,
},
"*"
)
}

View File

@@ -0,0 +1,57 @@
"use client"
import * as React from "react"
export type LockableParam =
| "style"
| "baseColor"
| "theme"
| "iconLibrary"
| "font"
| "menuAccent"
| "menuColor"
| "radius"
type LocksContextValue = {
locks: Set<LockableParam>
isLocked: (param: LockableParam) => boolean
toggleLock: (param: LockableParam) => void
}
const LocksContext = React.createContext<LocksContextValue | null>(null)
export function LocksProvider({ children }: { children: React.ReactNode }) {
const [locks, setLocks] = React.useState<Set<LockableParam>>(new Set())
const isLocked = React.useCallback(
(param: LockableParam) => locks.has(param),
[locks]
)
const toggleLock = React.useCallback((param: LockableParam) => {
setLocks((prev) => {
const next = new Set(prev)
if (next.has(param)) {
next.delete(param)
} else {
next.add(param)
}
return next
})
}, [])
const value = React.useMemo(
() => ({ locks, isLocked, toggleLock }),
[locks, isLocked, toggleLock]
)
return <LocksContext value={value}>{children}</LocksContext>
}
export function useLocks() {
const context = React.useContext(LocksContext)
if (!context) {
throw new Error("useLocks must be used within LocksProvider")
}
return context
}

View File

@@ -0,0 +1,56 @@
import { NextResponse, type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import { registryItemSchema } from "shadcn/schema"
import { buildRegistryBase, designSystemConfigSchema } from "@/registry/config"
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const result = designSystemConfigSchema.safeParse({
base: searchParams.get("base"),
style: searchParams.get("style"),
iconLibrary: searchParams.get("iconLibrary"),
baseColor: searchParams.get("baseColor"),
theme: searchParams.get("theme"),
font: searchParams.get("font"),
menuAccent: searchParams.get("menuAccent"),
menuColor: searchParams.get("menuColor"),
radius: searchParams.get("radius"),
template: searchParams.get("template"),
})
if (!result.success) {
return NextResponse.json(
{ error: result.error.issues[0].message },
{ status: 400 }
)
}
const registryBase = buildRegistryBase(result.data)
const parseResult = registryItemSchema.safeParse(registryBase)
if (!parseResult.success) {
return NextResponse.json(
{
error: "Invalid registry base item",
details: parseResult.error.format(),
},
{ status: 500 }
)
}
track("create_app", result.data)
return NextResponse.json(parseResult.data)
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "An unknown error occurred",
},
{ status: 500 }
)
}
}

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