Compare commits

...

186 Commits

Author SHA1 Message Date
shadcn
ad25490cf9 Merge pull request #9616 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-02-16 14:45:15 +04:00
github-actions[bot]
e94d3d80fa chore(release): version packages 2026-02-16 10:40:02 +00:00
shadcn
0e6b6d90bc Merge pull request #9493 from ANIBIT14/add-boldkit-registry
feat(registry): add @boldkit neubrutalism components
2026-02-16 14:39:04 +04:00
shadcn
0da9826821 Merge pull request #9624 from hongaar/clean-up-path-collisions
chore(v4): clean up path collisions
2026-02-16 11:50:44 +04:00
shadcn
2ef58bd75d Merge pull request #9634 from kapishdima/feat/soundcn
feat: added soundcn to directory
2026-02-15 20:30:34 +04:00
shadcn
cac794208e Merge branch 'main' into feat/soundcn 2026-02-15 20:30:11 +04:00
shadcn
a22aec8694 chore: rebuild 2026-02-15 20:29:58 +04:00
kapishdima
6a75b60b4f feat: added soundcn to directory 2026-02-15 17:00:30 +02:00
Aniruddha Agarwal
c494adbd87 Merge branch 'main' into add-boldkit-registry 2026-02-15 19:31:45 +05:30
anibit14
3aa0f13869 chore: run registry:build and update BoldKit description
- Updated component count: 43 components, 42 SVG shapes
- Added Vue and Nuxt framework support to description
- Regenerated registries.json as requested
2026-02-15 19:30:14 +05:30
shadcn
e1af950724 Merge pull request #9625 from shadcn-ui/shadcn/debug-react-hook-form
fix: checkbox demo for rhf and tanstack form
2026-02-13 16:11:03 +04:00
shadcn
e91388a010 fix 2026-02-13 16:07:57 +04:00
shadcn
8648ddb528 fix 2026-02-13 16:03:19 +04:00
shadcn
feff5b6a57 fix: checkbox example for forms 2026-02-13 15:59:10 +04:00
Joram van den Boezem
07f7147ff3 chore: clean up path collisions 2026-02-13 11:09:59 +01:00
shadcn
0e8a006adc Merge pull request #9615 from pavan-sh/docs/fix-available-typo
docs: fix typo in December 2023 changelog
2026-02-13 14:05:44 +04:00
Pavan Shinde
d2f91d6f1e Merge branch 'main' into docs/fix-available-typo 2026-02-12 21:22:56 +05:30
shadcn
e53bc92f41 Merge pull request #9599 from shadcn-ui/shadcn/fix-nested-aschild
fix: handling of nested aschild transforms
2026-02-12 11:36:49 +04:00
shadcn
597a8db2d9 Merge pull request #9592 from shadcn-ui/shadcn/fix-base-layer-handling
feat: update handling of base styles
2026-02-12 11:36:32 +04:00
shadcn
0b0f639cd0 Merge pull request #9609 from prithvi-rajan-222/add-slide-cn
chore: Adding slide-cn to the registry
2026-02-12 09:30:35 +04:00
shadcn
6b4ba6bca1 Merge pull request #9613 from petekp/codex/add-tool-ui-registry-directory
registry: add @tool-ui to registry directory
2026-02-12 09:30:08 +04:00
Pavan Shinde
3cdd67b5b4 docs: fix typo in December 2023 changelog 2026-02-12 02:14:52 +00:00
Pete Petrash
2b03bc7a53 registry: add @tool-ui to open registry directory 2026-02-11 11:19:08 -08:00
Aniruddha Agarwal
f6447b8936 Merge branch 'main' into add-boldkit-registry 2026-02-11 23:15:54 +05:30
Prithvi Rajan
4069c33671 Adding slide-cn to the registry 2026-02-10 23:16:46 -08:00
shadcn
f123057ae5 Merge pull request #9600 from sean0205/reui-package-update
ReUI Registry Update
2026-02-10 12:24:47 +04:00
sean0205
65c6c8146d reui registry update 2026-02-10 13:03:08 +05:00
shadcn
bbb59c9fe1 fix: handling of nested aschild transforms 2026-02-10 11:23:36 +04:00
shadcn
fb56f6571a Merge pull request #9598 from AnmolSaini16/AnmolSaini16/add-mapcn-registry
feature: add @mapcn registry
2026-02-10 10:56:09 +04:00
Anmoldeep Singh
082af1f82c feature: add @mapcn registry 2026-02-10 12:09:05 +05:30
shadcn
ae95fbd1be Merge pull request #9591 from Rhysjosmin/patch-1
Docs:Update Sonner API Reference link in Radix UI
2026-02-09 17:25:48 +04:00
shadcn
625bd97d8b Merge branch 'shadcn/fix-base-layer-handling' of github.com:shadcn-ui/ui into shadcn/fix-base-layer-handling 2026-02-09 17:15:31 +04:00
shadcn
603fce7cd3 Merge branch 'main' into shadcn/fix-base-layer-handling 2026-02-09 17:15:23 +04:00
shadcn
c759f460d5 feat: add docs 2026-02-09 17:13:49 +04:00
shadcn
e1c00667f7 Merge branch 'main' into shadcn/fix-base-layer-handling 2026-02-09 17:05:02 +04:00
shadcn
46631fc4d4 chore: rebuild 2026-02-09 17:04:26 +04:00
shadcn
f235a5d951 feat: update handling of base styles 2026-02-09 16:31:56 +04:00
RhysJosmin
b0b711f181 Update Sonner API Reference link
fixes the same issue solved here
https://github.com/shadcn-ui/ui/pull/9440
but for radix
2026-02-09 17:10:15 +05:30
shadcn
f1b7102583 Merge branch 'main' of github.com:shadcn-ui/ui 2026-02-09 13:19:46 +04:00
shadcn
f076420e68 fix 2026-02-09 13:19:40 +04:00
shadcn
4ce0a7eaa1 Merge pull request #9577 from rezaaa/feat/calendar_hijri_font
feat(calendar): add Vazirmatn font to Persian examples
2026-02-09 12:53:11 +04:00
Reza Mahmoudi
270b730c21 Merge branch 'main' into feat/calendar_hijri_font 2026-02-09 11:52:06 +03:30
shadcn
14a6cc5999 fix: position of command menu 2026-02-09 12:10:41 +04:00
shadcn
0067873f60 fix: block viewer 2026-02-09 12:08:10 +04:00
shadcn
fc16e1461f ci: update validate registries workflow 2026-02-09 11:41:34 +04:00
shadcn
8f01916bb2 Merge pull request #9586 from mxkaske/registries/openstatus
feat(registry): add @openstatus registry
2026-02-09 11:30:45 +04:00
Maximilian Kaske
ead138b4cd chore: add openstatus registry 2026-02-08 21:28:57 +01:00
Reza Mahmoudi
ab6c8caf2f Merge branch 'main' into feat/calendar_hijri_font 2026-02-08 12:05:21 +03:30
shadcn
ba9206bded chore: rebuild registry 2026-02-08 12:08:47 +04:00
Reza Mahmoudi
c5838cf955 Merge branch 'main' into feat/calendar_hijri_font 2026-02-08 00:29:42 +03:30
shadcn
0c41fc30e4 Merge pull request #9426 from inference-sh/add-inferencesh-registry
registry: add @inferencesh
2026-02-07 13:39:54 +04:00
Aniruddha Agarwal
8270cfa39e Merge branch 'main' into add-boldkit-registry 2026-02-07 14:04:03 +05:30
Reza Mahmoudi
06e356cab9 feat(calendar): add Vazirmatn font to Persian examples 2026-02-06 22:04:50 +03:30
shadcn
f24631dc48 Merge pull request #9573 from educlopez/add-thegridcn-registry
feat: add @thegridcn registry
2026-02-06 17:21:55 +04:00
Edu Calvo
ec936bcd06 feat: add @thegridcn registry
Add The Gridcn (thegridcn.com) - a Tron-inspired shadcn/ui theme system with Greek god color schemes, glow intensity levels, and sci-fi components.
2026-02-06 10:47:37 +01:00
shadcn
6c7975e400 feat: default to nova 2026-02-06 13:19:30 +04:00
shadcn
8acef7ab66 docs: add changelog 2026-02-06 13:10:12 +04:00
shadcn
4ddfd39b0d Merge pull request #9440 from rahman-D3V/fix-sonner-doc-link
fix: correct Sonner documentation link
2026-02-06 12:50:31 +04:00
shadcn
3ba37cc24c Merge pull request #9570 from shadcn-ui/shadcn/init-new-york
feat: add shadcn as a dependency for init
2026-02-06 12:49:35 +04:00
shadcn
da080118b0 feat: add shadcn as a dependency for init 2026-02-06 12:35:51 +04:00
shadcn
e8897ea80a Merge pull request #9255 from PhilemonChiro/fix/tanstack-form-performance
fix(docs): improve TanStack Form example performance
2026-02-06 12:27:54 +04:00
shadcn
9d26f582fa Merge pull request #9569 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-02-06 11:23:29 +04:00
github-actions[bot]
0a2ad2176c chore(release): version packages 2026-02-06 07:21:44 +00:00
shadcn
7c36439836 Merge pull request #9568 from shadcn-ui/shadcn/remove-block-message
feat: remove restricted block message
2026-02-06 11:20:44 +04:00
shadcn
a1e3afed06 chore: changeset 2026-02-06 11:14:54 +04:00
shadcn
be5b1bbae3 feat: remove restricted blocks 2026-02-06 11:14:30 +04:00
shadcn
52de23bf95 Merge pull request #9567 from shadcn-ui/shadcn/blocks-for-base
feat: create blocks for new styles
2026-02-06 11:06:19 +04:00
shadcn
1d16fe46cd Merge pull request #9555 from shadcn-ui/dependabot/npm_and_yarn/modelcontextprotocol/sdk-1.26.0
chore(deps): bump @modelcontextprotocol/sdk from 1.17.2 to 1.26.0
2026-02-06 11:01:53 +04:00
shadcn
cbecda13f9 fix 2026-02-06 10:55:44 +04:00
shadcn
24649ec103 fix 2026-02-06 10:51:42 +04:00
shadcn
b9f62a8399 feat: create blocks for new styles 2026-02-06 10:47:18 +04:00
shadcn
689d45e095 Merge branch 'main' of github.com:shadcn-ui/ui 2026-02-05 12:29:31 +04:00
shadcn
33f7b3f2bb fix: minor style updates 2026-02-05 12:29:21 +04:00
shadcn
2cce072393 Merge pull request #9558 from Dinil-Thilakarathne/feat/add-sona-ui-registry
feat: Add @sona-ui to registries and directory.
2026-02-05 12:25:14 +04:00
shadcn
d64bdec2f9 Merge branch 'main' into feat/add-sona-ui-registry 2026-02-05 12:25:02 +04:00
shadcn
5adacdecad Merge pull request #9550 from rgbkrk/add-nteract-to-registry
feat(registry): added new nteract registry
2026-02-05 12:23:11 +04:00
Dinil Thilakarathne
f2552d3f3b feat: Add @sona-ui to registries and directory. 2026-02-05 04:18:30 +05:30
dependabot[bot]
b435e01199 chore(deps): bump @modelcontextprotocol/sdk from 1.17.2 to 1.26.0
Bumps [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) from 1.17.2 to 1.26.0.
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/1.17.2...v1.26.0)

---
updated-dependencies:
- dependency-name: "@modelcontextprotocol/sdk"
  dependency-version: 1.26.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-04 20:27:49 +00:00
Kyle Kelley
cd576df6e4 feat(registry): added new nteract registry 2026-02-04 10:55:41 -08:00
shadcn
9fbd3b1a72 Merge pull request #9552 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-02-04 21:10:30 +04:00
github-actions[bot]
c6dd35a092 chore(release): version packages 2026-02-04 17:07:09 +00:00
shadcn
470c6f42b0 Merge pull request #9539 from shadcn-ui/shadcn/fix-canonical-classes
fix: canonical classes in bases
2026-02-04 21:05:56 +04:00
shadcn
e6956e45ac Merge branch 'main' into shadcn/fix-canonical-classes 2026-02-04 20:44:08 +04:00
shadcn
a2b9dedbb7 fix: resizable version in registry 2026-02-04 19:52:12 +04:00
shadcn
384129609f fix 2026-02-04 19:48:14 +04:00
shadcn
5be0811f01 fix 2026-02-04 19:23:20 +04:00
shadcn
1a10b4671a Merge branch 'main' into shadcn/fix-canonical-classes
# Conflicts:
#	apps/v4/public/r/styles/base-lyra/tooltip.json
#	apps/v4/public/r/styles/base-maia/tooltip.json
#	apps/v4/public/r/styles/base-mira/tooltip.json
#	apps/v4/public/r/styles/base-nova/tooltip.json
#	apps/v4/public/r/styles/base-vega/tooltip.json
#	apps/v4/public/r/styles/radix-lyra/tooltip.json
#	apps/v4/public/r/styles/radix-maia/tooltip.json
#	apps/v4/public/r/styles/radix-mira/tooltip.json
#	apps/v4/public/r/styles/radix-nova/tooltip.json
#	apps/v4/public/r/styles/radix-vega/tooltip.json
2026-02-04 18:25:27 +04:00
shadcn
e7d36b7e21 Merge pull request #9548 from shadcn-ui/shadcn/fix-code-blocks
feat: update tooltip provider handling
2026-02-04 18:14:54 +04:00
shadcn
290fac9115 Merge branch 'main' into shadcn/fix-code-blocks 2026-02-04 18:09:22 +04:00
shadcn
0633333db4 chore: rebuild registry 2026-02-04 18:06:46 +04:00
shadcn
630323ad47 feat: update TooltipProvider handling 2026-02-04 18:04:19 +04:00
shadcn
44a9b3bd12 fix 2026-02-04 15:18:05 +04:00
shadcn
2b879a5ec8 Merge branch 'shadcn/fix-canonical-classes' of github.com:shadcn-ui/ui into shadcn/fix-canonical-classes 2026-02-04 15:11:48 +04:00
shadcn
381f2ef165 fix 2026-02-04 15:11:30 +04:00
shadcn
825ebca3f0 Merge branch 'main' into shadcn/fix-canonical-classes 2026-02-04 13:15:17 +04:00
shadcn
e0063070a6 feat: add a test:apps script 2026-02-04 13:14:04 +04:00
shadcn
013ae51d10 Merge pull request #9541 from francescopesoli/fix/rtl-password-link-margin
fix(rtl): use logical margin for password link alignment
2026-02-04 11:02:36 +04:00
okaris
08e54510ed rebuild registry 2026-02-03 21:54:23 +01:00
okaris
a95606cee9 add missing logo and rebuild registry 2026-02-03 21:54:23 +01:00
okaris
c990476d99 registry: add @inferencesh 2026-02-03 21:54:23 +01:00
Francesco
44c8f02d06 fix(rtl): use logical margin for password link in RTL examples
Change ml-auto to ms-auto (margin-inline-start) so the Forgot your
  password link aligns correctly in both LTR and RTL layouts.

  Fixes #9515
2026-02-03 18:36:19 +01:00
shadcn
a012542015 fix: duplicate classes 2026-02-03 17:37:11 +04:00
shadcn
926df433a7 fix 2026-02-03 16:43:17 +04:00
shadcn
5c09e0d8fa chore: update canonical classes across styles 2026-02-03 16:41:44 +04:00
shadcn
dba86053f5 fix: canonical classes in base 2026-02-03 14:08:56 +04:00
shadcn
cd188b267d Merge branch 'main' into shadcn/fix-canonical-classes 2026-02-03 11:14:09 +04:00
shadcn
8a09fbaac9 deps: upgrade tailwind 2026-02-03 11:10:54 +04:00
shadcn
9676c8f4ee Merge pull request #9461 from jaem0629/fix/resizable-v4-upgrade
fix(resizable): upgrade to react-resizable-panels v4
2026-02-03 11:09:34 +04:00
Jaem
9b5aeab889 Merge branch 'main' into fix/resizable-v4-upgrade 2026-02-03 09:16:53 +09:00
shadcn
28ebf1b88a Merge pull request #9531 from WebDevSimplified/add-wds-registry
fix: Re-add WDS registry
2026-02-02 21:10:46 +04:00
shadcn
f922e82f53 fix: ring for focus visible 2026-02-02 21:09:10 +04:00
Web Dev Simplified
beec1e060e Add WDS registry 2026-02-02 07:15:58 -06:00
shadcn
26a24d3d5c Merge branch 'fix/resizable-v4-upgrade' of github.com:jaem0629/ui into fix/resizable-v4-upgrade 2026-02-02 16:33:55 +04:00
shadcn
c3c7f03f04 fix: update props, migrate components and add changelog 2026-02-02 16:33:35 +04:00
Jaem
4af29d6c20 Update pnpm-lock.yaml 2026-02-02 21:05:38 +09:00
shadcn
b28f77f893 Merge branch 'main' into fix/resizable-v4-upgrade 2026-02-02 15:57:45 +04:00
shadcn
b8c7ae8088 Merge pull request #9528 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-02-02 15:29:24 +04:00
github-actions[bot]
d21c74fb3a chore(release): version packages 2026-02-02 11:25:41 +00:00
shadcn
d6548b4ae8 Merge pull request #9507 from shadcn-ui/ny-radix-ui
feat: update new-york to radix-ui
2026-02-02 15:24:41 +04:00
shadcn
110a4ec10b docs: add changelog 2026-02-02 15:15:02 +04:00
shadcn
851562f4f2 Merge branch 'ny-radix-ui' of github.com:shadcn-ui/ui into ny-radix-ui 2026-02-02 14:32:21 +04:00
shadcn
b7b839ebc2 chore: changeset 2026-02-02 14:32:01 +04:00
shadcn
8d9be074a3 feat: update migrate radix command 2026-02-02 14:31:40 +04:00
shadcn
a0c077da9e Merge branch 'main' into ny-radix-ui 2026-02-02 13:15:42 +04:00
shadcn
540cd031c3 fix 2026-02-02 13:14:09 +04:00
Jaem
4d9720449f Merge branch 'main' into fix/resizable-v4-upgrade 2026-02-02 15:43:50 +09:00
shadcn
f1e10f3da8 Merge pull request #9495 from withden/patch-3 2026-02-02 08:40:02 +04:00
Denish Navadiya
e2225d4a93 Rename @paceui to @pacekit with updated details
Updated '@paceui' to '@pacekit' with new homepage, URL, and description.
2026-02-02 10:07:22 +05:30
Jaem
444f6889c8 Merge remote-tracking branch 'upstream/main' into fix/resizable-v4-upgrade 2026-02-02 01:16:10 +09:00
Copilot
03a7804c42 Update callout component to use rounded-xl (#9512)
* Initial plan

* Fix callout component to use rounded-lg in style-lyra.css

Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>

* Revert CSS change - callout.tsx already has rounded-lg

Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>

* Change callout rounded class from rounded-lg to rounded-xl

Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>
2026-02-01 15:44:23 +04:00
Chad Bell
acc847bed3 docs(select): add SelectGroup to usage examples (#9508) 2026-02-01 10:19:59 +04:00
shadcn
abfa2ddb74 Merge branch 'main' into ny-radix-ui 2026-01-31 21:19:59 +04:00
shadcn
5e92c160dd feat: update new-york to radix-ui 2026-01-31 21:18:49 +04:00
shadcn
d41e857ba3 fix: select group in field-demo (#9504) 2026-01-31 15:32:07 +04:00
dependabot[bot]
99651191cc chore(deps): bump next in /templates/monorepo-next/apps/web (#9499)
Bumps [next](https://github.com/vercel/next.js) from 16.0.10 to 16.1.5.
- [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.10...v16.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 15:25:45 +04:00
dependabot[bot]
712285f60e chore(deps): bump eslint from 8.57.1 to 9.26.0 (#9500)
Bumps [eslint](https://github.com/eslint/eslint) from 8.57.1 to 9.26.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/v9.26.0/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.57.1...v9.26.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.26.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 15:25:21 +04:00
github-actions[bot]
aed95086e0 chore(release): version packages (#9503)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-31 15:10:28 +04:00
shadcn
1990280d66 ci: update changesets 2026-01-31 15:04:54 +04:00
shadcn
2bf55c9133 feat: add geist fonts (#9502) 2026-01-31 14:52:43 +04:00
shadcn
3192a3db55 fix: registry script 2026-01-31 11:34:35 +04:00
shadcn
afa2a7adf2 fix 2026-01-30 22:14:48 +04:00
github-actions[bot]
728d8af275 chore(release): version packages (#9363)
* 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>
2026-01-30 21:13:27 +04:00
shadcn
38de7fddc2 feat: rtl (#9498)
* feat: rtl

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add sidebar

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* chore: changeset

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2026-01-30 21:08:39 +04:00
Aniruddha Agarwal
c719d24f3a Merge branch 'main' into add-boldkit-registry 2026-01-30 19:31:09 +05:30
shadcn
4479965555 fix: directory.json 2026-01-30 16:41:57 +04:00
Denish Navadiya
7ea124b25d Rename @paceui to @pacekit with updated details 2026-01-30 17:42:39 +05:30
anibit14
f746368369 chore: update component count to 45 2026-01-30 12:14:39 +05:30
anibit14
164b6ff6c1 feat(registry): add @boldkit neubrutalism components 2026-01-30 12:04:00 +05:30
Jaem
759003c781 Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 15:11:00 +09:00
Jaem
6d467d2e1d fix: allow vertical scroll pass-through on code blocks (#9454) 2026-01-27 09:57:47 +04:00
Harit
893cddd2dc add satoriui registry (#9432)
* add satoriui registry

* updated registries.json file

---------

Co-authored-by: Harit Patel <harit.ptl.business.com>
2026-01-27 09:50:14 +04:00
Nicolas Vargas
1781186def fix(docs): update navigation-menu docs package name and add styleName (#9455) 2026-01-27 09:48:54 +04:00
Wolfr
89b9a76368 fix - Update copy (#9453) 2026-01-27 09:47:58 +04:00
Jaem
6529256e98 Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 11:57:41 +09:00
Saullo Bretas Silva
0266253841 Fix JSON formatting in registries.json (#9464) 2026-01-27 00:49:18 +04:00
Jaem
4a39de5c56 Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 03:30:02 +09:00
Nirav joshi
e5fda2c139 Fixed: directory json issue for shadcnspace (#9460)
* feat(registry): add my custom registry

* Feat: Added Shadcnspace into  registries.json

* Updated directory.json

---------

Co-authored-by: ShadcnSpace <shadcnspace@gmail.com>
2026-01-26 22:28:06 +04:00
Jaem
d53f7489ce Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 03:19:30 +09:00
Jaem
dfe784b44a fix(resizable): upgrade to react-resizable-panels v4
- Update component API: PanelGroup → Group, PanelResizeHandle → Separator
- Update prop: direction → orientation
- Update size values: number → string with units (e.g., "50%")
- Update CSS selectors: data-[panel-group-direction] → aria-[orientation]
- Update controlled component: onLayout → onLayoutChange with Layout type

Closes #9118, #9136, #9200
2026-01-27 03:10:54 +09:00
Usman Sabuwala
40b9de46e9 Fix Base UI dropdown menu links (#9457)
Base UI does not have a `dropdown-menu` but rather just `menu`
This PR fixes the link that lead to Base UI docs
2026-01-26 22:01:09 +04:00
shadcn
6d97ab0b9b Revert "feat(registry): added new registry(@shadcn-space , @shadcn-dashboard)…" (#9458)
This reverts commit d06e84a007.
2026-01-26 21:03:04 +04:00
ShadcnSpace
d06e84a007 feat(registry): added new registry(@shadcn-space , @shadcn-dashboard) (#9102)
* feat(registry): add my custom registry

* Feat: Added Shadcnspace into  registries.json

---------

Co-authored-by: ShadcnSpace <shadcnspace@gmail.com>
Co-authored-by: Nirav joshi <31440272+Niravjoshi-Wrappixel@users.noreply.github.com>
2026-01-26 20:56:47 +04:00
Akash Moradiya
a29185c9cf fix(directory): basecn registry url typo (#9452) 2026-01-26 09:20:03 +04:00
Sitsiilia
84c801ac67 docs(figma): add shadcn/ui components kit by Sitsiilia Bergmann (#9416)
* docs(figma): add shadcn/ui components kit by Sitsiilia Bergmann

* docs: updates

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-26 09:06:35 +04:00
rahman-D3V
3dbe9e6a3e fix: correct Sonner documentation link 2026-01-24 18:32:59 +05:30
shadcn
267d45ac7a docs: update changelog 2026-01-23 20:59:49 +04:00
shadcn
caadc3d7e8 feat: update new-york-v4 components to match new styles (#9434) 2026-01-23 20:35:04 +04:00
shadcn
a4ee54836e feat: add inline-start and inline-end support for base-ui (#9430) 2026-01-23 15:26:35 +04:00
shadcn
7b5c919eae fix: format 2026-01-23 15:20:11 +04:00
Hayden Bleasel
f1cacdc051 Update AI Elements Registry URL (#9425)
* Update directory.json

* chore: rebuild registry

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-23 11:18:35 +04:00
dependabot[bot]
8cb8fb66b3 chore(deps): bump lodash from 4.17.21 to 4.17.23 (#9415)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-23 11:12:08 +04:00
Dallas Carraher
ef01cd4315 fix(components): use DialogOverlay in CommandMenu to fix button interactions (#9410)
Replace DialogPrimitive.Overlay with the standard DialogOverlay component
to properly manage the overlay lifecycle with correct animation state
classes. This fixes issue #9403 where buttons on documentation pages
were not working due to react-remove-scroll getting into a corrupted
global state.
2026-01-23 11:10:45 +04:00
shadcn
6cb2a1fd65 fix: sidebar 2026-01-22 23:55:06 +04:00
shadcn
ee88d296f4 feat: refactor component preview 2026-01-22 20:59:59 +04:00
shadcn
598f17812d feat: rss for changelog (#9420)
* feat: init

* fix

* fix
2026-01-22 17:43:45 +04:00
Aron Hafner
0ae734bdb2 docs: Update to correct url for vaul docs (#9406) 2026-01-22 09:58:42 +04:00
shadcn
18bd8f07cb fix: improve perf of v0 route (#9392) 2026-01-20 23:14:52 +04:00
shadcn
5fc9ced0fd fix 2026-01-20 21:53:52 +04:00
shadcn
b5dff005f6 fix 2026-01-20 21:53:00 +04:00
shadcn
c5c08bb773 Merge branch 'main' of github.com:shadcn-ui/ui 2026-01-20 21:33:57 +04:00
shadcn
5998e59839 fix 2026-01-20 21:33:40 +04:00
Ronny Badilla
4b7e38ab42 feat(registry): add @pastecn to the registry (#9390) 2026-01-20 20:53:55 +04:00
shadcn
e2ba2d241e fix: format 2026-01-20 20:51:57 +04:00
shadcn
13e2a6c598 fix: root components 2026-01-20 19:38:04 +04:00
shadcn
47c47eaed2 feat: add docs for base-ui components (#9304)
* feat: add base and radix docs

* feat: transform code for display

* fix

* fix

* fix

* fix

* fix

* chore: remove claude files

* fix

* fix

* fix

* chore: run format:write

* fix

* feat: add more examples

* fix

* feat: add aspect-ratio

* feat: add avatar

* feat: add badge

* feat: add breadcrumb

* fix

* feat: add button

* fix

* fix

* fix

* feat: add calendar and card

* feat: add carousel

* fix: chart

* feat: add checkbox

* feat: add collapsible

* feat: add combobox

* feat: add command

* feat: add context menu

* feat: add data-table dialog and drawer

* feat: dropdown-menu

* feat: add date-picker

* feat: add empty

* feat: add field and hover-card

* fix: input

* feat: add input

* feat: add input-group

* feat: add input-otp

* feat: add item

* feat: add kbd and label

* feat: add menubar

* feat: add native-select

* feat: add more components

* feat: more components

* feat: more components

* feat: add skeleton, slider and sonner

* feat: add spinner and switch

* feat: add more components

* fix: tabs

* fix: tabs

* feat: add docs for sidebar

* fix

* fix

* fi

* docs: update

* fix: create page

* fix

* fix

* chore: add changelog

* fix
2026-01-20 19:31:38 +04:00
Philemon Chiro
9317a93152 fix(docs): improve TanStack Form example performance
Change validation strategy from onChange to onBlur to prevent
validation on every keystroke, which causes input lag in textareas.
2026-01-02 13:41:26 +02:00
3508 changed files with 170292 additions and 22812 deletions

View File

@@ -1,5 +0,0 @@
---
"shadcn": patch
---
Fix: skip all transforms for universal registry items

View File

@@ -1,12 +1,12 @@
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js
import { exec } from "child_process"
import { execSync } from "child_process"
// This script is used by the `release.yml` workflow to update the version of the packages being released.
// The standard step is only to run `changeset version` but this does not update the package-lock.json file.
// So we also run `npm install`, which does this update.
// The standard step is only to run `changeset version` but this does not update the pnpm-lock.yaml file.
// So we also run `pnpm install`, which does this update.
// This is a workaround until this is handled automatically by `changeset version`.
// See https://github.com/changesets/changesets/issues/421.
exec("npx changeset version")
exec("npm install")
execSync("npx changeset version", { stdio: "inherit" })
execSync("pnpm install --lockfile-only", { stdio: "inherit" })

View File

@@ -4,11 +4,13 @@ on:
pull_request:
paths:
- "apps/v4/public/r/registries.json"
- "apps/v4/registry/directory.json"
push:
branches:
- main
paths:
- "apps/v4/public/r/registries.json"
- "apps/v4/registry/directory.json"
jobs:
validate:
@@ -47,8 +49,5 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm build --filter=shadcn
- name: Validate registries
run: pnpm --filter=v4 validate:registries

1
.gitignore vendored
View File

@@ -41,3 +41,4 @@ tsconfig.tsbuildinfo
.vscode
.notes
.playwright-mcp

View File

@@ -1,10 +1,8 @@
"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 { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import {
Field,
FieldContent,
@@ -15,13 +13,11 @@ import {
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"
} from "@/examples/radix/ui/field"
import { Input } from "@/examples/radix/ui/input"
import { RadioGroup, RadioGroupItem } from "@/examples/radix/ui/radio-group"
import { Switch } from "@/examples/radix/ui/switch"
import { IconMinus, IconPlus } from "@tabler/icons-react"
export function AppearanceSettings() {
const [gpuCount, setGpuCount] = React.useState(8)
@@ -97,7 +93,7 @@ export function AppearanceSettings() {
value={gpuCount}
onChange={handleGpuInputChange}
size={3}
className="h-8 !w-14 font-mono"
className="h-7 !w-14 font-mono"
maxLength={3}
/>
<Button

View File

@@ -1,20 +1,8 @@
"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 { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
@@ -27,7 +15,18 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
} from "@/examples/radix/ui/dropdown-menu"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
export function ButtonGroupDemo() {
const [label, setLabel] = React.useState("personal")
@@ -57,7 +56,7 @@ export function ButtonGroupDemo() {
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 [--radius:1rem]">
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />

View File

@@ -1,21 +1,20 @@
"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 { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
} from "@/examples/radix/ui/input-group"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
} from "@/examples/radix/ui/tooltip"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
export function ButtonGroupInputGroup() {
const [voiceEnabled, setVoiceEnabled] = React.useState(false)

View File

@@ -1,10 +1,9 @@
"use client"
import { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
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>

View File

@@ -1,14 +1,13 @@
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 { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/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"
} from "@/examples/radix/ui/popover"
import { Separator } from "@/examples/radix/ui/separator"
import { Textarea } from "@/examples/radix/ui/textarea"
import { BotIcon, ChevronDownIcon } from "lucide-react"
export function ButtonGroupPopover() {
return (
@@ -22,7 +21,7 @@ export function ButtonGroupPopover() {
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
<PopoverContent align="end" className="gap-0 rounded-xl p-0 text-sm">
<div className="px-4 py-3">
<div className="text-sm font-medium">Agent Tasks</div>
</div>

View File

@@ -1,11 +1,10 @@
import { PlusIcon } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
} from "@/examples/radix/ui/avatar"
import { Button } from "@/examples/radix/ui/button"
import {
Empty,
EmptyContent,
@@ -13,14 +12,15 @@ import {
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
} from "@/examples/radix/ui/empty"
import { PlusIcon } from "lucide-react"
export function EmptyAvatarGroup() {
return (
<Empty className="flex-none border">
<Empty className="flex-none border py-10">
<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">
<AvatarGroup className="grayscale">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
@@ -39,7 +39,7 @@ export function EmptyAvatarGroup() {
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
</AvatarGroup>
</EmptyMedia>
<EmptyTitle>No Team Members</EmptyTitle>
<EmptyDescription>

View File

@@ -1,5 +1,5 @@
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
import { Checkbox } from "@/examples/radix/ui/checkbox"
import { Field, FieldLabel } from "@/examples/radix/ui/field"
export function FieldCheckbox() {
return (

View File

@@ -1,5 +1,5 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { Button } from "@/examples/radix/ui/button"
import { Checkbox } from "@/examples/radix/ui/checkbox"
import {
Field,
FieldDescription,
@@ -8,20 +8,21 @@ import {
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
} from "@/examples/radix/ui/field"
import { Input } from "@/examples/radix/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
} from "@/examples/radix/ui/select"
import { Textarea } from "@/examples/radix/ui/textarea"
export function FieldDemo() {
return (
<div className="w-full max-w-md rounded-lg border p-6">
<div className="w-full max-w-md rounded-xl border p-6">
<form>
<FieldGroup>
<FieldSet>
@@ -69,18 +70,20 @@ export function FieldDemo() {
<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>
<SelectGroup>
<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>
</SelectGroup>
</SelectContent>
</Select>
</Field>
@@ -93,12 +96,14 @@ export function FieldDemo() {
<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>
<SelectGroup>
<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>
</SelectGroup>
</SelectContent>
</Select>
</Field>

View File

@@ -1,5 +1,5 @@
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { Card, CardContent } from "@/examples/radix/ui/card"
import { Checkbox } from "@/examples/radix/ui/checkbox"
import {
Field,
FieldDescription,
@@ -8,7 +8,7 @@ import {
FieldLegend,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
} from "@/examples/radix/ui/field"
const options = [
{
@@ -50,7 +50,7 @@ export function FieldHear() {
>
<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"
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}

View File

@@ -1,13 +1,8 @@
"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"
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
import { Slider } from "@/examples/radix/ui/slider"
export function FieldSlider() {
const [value, setValue] = useState([200, 800])

View File

@@ -1,4 +1,4 @@
import { FieldSeparator } from "@/registry/new-york-v4/ui/field"
import { FieldSeparator } from "@/examples/radix/ui/field"
import { AppearanceSettings } from "./appearance-settings"
import { ButtonGroupDemo } from "./button-group-demo"

View File

@@ -1,20 +1,19 @@
"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"
} from "@/examples/radix/ui/input-group"
import { Label } from "@/examples/radix/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
} from "@/examples/radix/ui/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
export function InputGroupButtonExample() {
const [isFavorite, setIsFavorite] = React.useState(false)

View File

@@ -1,12 +1,9 @@
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"
} from "@/examples/radix/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
@@ -14,13 +11,15 @@ import {
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
import { Separator } from "@/registry/new-york-v4/ui/separator"
} from "@/examples/radix/ui/input-group"
import { Separator } from "@/examples/radix/ui/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
} from "@/examples/radix/ui/tooltip"
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
export function InputGroupDemo() {
return (
@@ -67,11 +66,7 @@ export function InputGroupDemo() {
<DropdownMenuTrigger asChild>
<InputGroupButton variant="ghost">Auto</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:0.95rem]"
>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem>

View File

@@ -1,6 +1,4 @@
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Button } from "@/examples/radix/ui/button"
import {
Item,
ItemActions,
@@ -8,7 +6,8 @@ import {
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
} from "@/examples/radix/ui/item"
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
export function ItemDemo() {
return (

View File

@@ -1,24 +1,8 @@
"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 { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
import { Badge } from "@/examples/radix/ui/badge"
import {
Command,
CommandEmpty,
@@ -26,7 +10,7 @@ import {
CommandInput,
CommandItem,
CommandList,
} from "@/registry/new-york-v4/ui/command"
} from "@/examples/radix/ui/command"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -39,25 +23,36 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
} from "@/examples/radix/ui/dropdown-menu"
import { Field, FieldLabel } from "@/examples/radix/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
} from "@/examples/radix/ui/input-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
import { Switch } from "@/registry/new-york-v4/ui/switch"
} from "@/examples/radix/ui/popover"
import { Switch } from "@/examples/radix/ui/switch"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
} from "@/examples/radix/ui/tooltip"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
const SAMPLE_DATA = {
mentionable: [
@@ -190,17 +185,17 @@ export function NotionPromptForm() {
const hasMentions = mentions.length > 0
return (
<form className="[--radius:1.2rem]">
<form>
<Field>
<FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt
</FieldLabel>
<InputGroup>
<InputGroup className="rounded-xl">
<InputGroupTextarea
id="notion-prompt"
placeholder="Ask, search, or make anything..."
/>
<InputGroupAddon align="block-start">
<InputGroupAddon align="block-start" className="pt-3">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
@@ -214,7 +209,7 @@ export function NotionPromptForm() {
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="rounded-full transition-transform"
className="transition-transform"
>
<IconAt /> {!hasMentions && "Add context"}
</InputGroupButton>
@@ -222,7 +217,7 @@ export function NotionPromptForm() {
</TooltipTrigger>
<TooltipContent>Mention a person, page, or date</TooltipContent>
</Tooltip>
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput placeholder="Search pages..." />
<CommandList>
@@ -240,6 +235,7 @@ export function NotionPromptForm() {
setMentions((prev) => [...prev, currentValue])
setMentionPopoverOpen(false)
}}
className="rounded-lg"
>
<MentionableIcon item={item} />
{item.title}
@@ -306,12 +302,8 @@ export function NotionPromptForm() {
</TooltipTrigger>
<TooltipContent>Select AI model</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:1rem]"
>
<DropdownMenuGroup className="w-42">
<DropdownMenuContent side="top" align="start" className="w-48">
<DropdownMenuGroup className="w-48">
<DropdownMenuLabel className="text-muted-foreground text-xs">
Select Agent Mode
</DropdownMenuLabel>
@@ -346,11 +338,7 @@ export function NotionPromptForm() {
<IconWorld /> All Sources
</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="[--radius:1rem]"
>
<DropdownMenuContent side="top" align="end" className="w-72">
<DropdownMenuGroup>
<DropdownMenuItem
asChild

View File

@@ -1,5 +1,5 @@
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
import { Badge } from "@/examples/radix/ui/badge"
import { Spinner } from "@/examples/radix/ui/spinner"
export function SpinnerBadge() {
return (

View File

@@ -1,4 +1,4 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import { Button } from "@/examples/radix/ui/button"
import {
Empty,
EmptyContent,
@@ -6,8 +6,8 @@ import {
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
} from "@/examples/radix/ui/empty"
import { Spinner } from "@/examples/radix/ui/spinner"
export function SpinnerEmpty() {
return (

View File

@@ -64,7 +64,7 @@ export default function ChartsLayout({
<PageNav id="charts">
<ChartsNav />
</PageNav>
<div className="container-wrapper section-soft flex-1">
<div className="container-wrapper flex-1">
<div className="container pb-6">
<section className="theme-container">{children}</section>
</div>

View File

@@ -1,21 +1,15 @@
import Link from "next/link"
import { notFound } from "next/navigation"
import { mdxComponents } from "@/mdx-components"
import {
IconArrowLeft,
IconArrowRight,
IconArrowUpRight,
} from "@tabler/icons-react"
import fm from "front-matter"
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"
import { findNeighbour } from "fumadocs-core/page-tree"
import z from "zod"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
import { DocsBaseSwitcher } from "@/components/docs-base-switcher"
import { DocsCopyPage } from "@/components/docs-copy-page"
import { DocsTableOfContents } from "@/components/docs-toc"
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Button } from "@/registry/new-york-v4/ui/button"
export const revalidate = false
@@ -85,127 +79,116 @@ export default async function Page(props: {
const doc = page.data
const MDX = doc.body
const neighbours = findNeighbour(source.pageTree, page.url)
const isChangelog = params.slug?.[0] === "changelog"
const neighbours = isChangelog
? { previous: null, next: null }
: findNeighbour(source.pageTree, page.url)
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 className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full">
<div
data-slot="docs"
className="flex scroll-mt-24 items-stretch pb-8 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">
<div className="mx-auto flex w-full max-w-[40rem] min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2">
<div className="flex items-start justify-between">
<h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
<div className="flex items-center justify-between md:items-start">
<h1 className="scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl">
{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 page={raw} url={absoluteUrl(page.url)} />
{neighbours.previous && (
<Button
variant="secondary"
size="icon"
className="extend-touch-target ml-auto size-8 shadow-none md:size-7"
asChild
>
<Link href={neighbours.previous.url}>
<IconArrowLeft />
<span className="sr-only">Previous</span>
</Link>
</Button>
)}
{neighbours.next && (
<Button
variant="secondary"
size="icon"
className="extend-touch-target size-8 shadow-none md:size-7"
asChild
>
<Link href={neighbours.next.url}>
<span className="sr-only">Next</span>
<IconArrowRight />
</Link>
</Button>
)}
<div className="docs-nav flex items-center gap-2">
<div className="hidden sm:block">
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
</div>
<div className="ml-auto flex gap-2">
{neighbours.previous && (
<Button
variant="secondary"
size="icon"
className="extend-touch-target size-8 shadow-none md:size-7"
asChild
>
<Link href={neighbours.previous.url}>
<IconArrowLeft />
<span className="sr-only">Previous</span>
</Link>
</Button>
)}
{neighbours.next && (
<Button
variant="secondary"
size="icon"
className="extend-touch-target size-8 shadow-none md:size-7"
asChild
>
<Link href={neighbours.next.url}>
<span className="sr-only">Next</span>
<IconArrowRight />
</Link>
</Button>
)}
</div>
</div>
</div>
{doc.description && (
<p className="text-muted-foreground text-[1.05rem] text-balance sm:text-base">
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
{doc.description}
</p>
)}
</div>
{links ? (
<div className="flex items-center gap-2 pt-4">
{links?.doc && (
<Badge asChild variant="secondary" className="rounded-full">
<a href={links.doc} target="_blank" rel="noreferrer">
Docs <IconArrowUpRight />
</a>
</Badge>
)}
{links?.api && (
<Badge asChild variant="secondary" className="rounded-full">
<a href={links.api} target="_blank" rel="noreferrer">
API Reference <IconArrowUpRight />
</a>
</Badge>
)}
</div>
) : null}
</div>
<div className="w-full flex-1 *:data-[slot=alert]:first:mt-0">
<div className="w-full flex-1 pb-16 *:data-[slot=alert]:first:mt-0 sm:pb-0">
{params.slug &&
params.slug[0] === "components" &&
params.slug[1] &&
params.slug[2] && (
<DocsBaseSwitcher
base={params.slug[1]}
component={params.slug[2]}
className="mb-4"
/>
)}
<MDX components={mdxComponents} />
</div>
</div>
<div className="mx-auto hidden h-16 w-full max-w-2xl items-center gap-2 px-4 sm:flex md:px-0">
{neighbours.previous && (
<Button
variant="secondary"
size="sm"
asChild
className="shadow-none"
>
<Link href={neighbours.previous.url}>
<IconArrowLeft /> {neighbours.previous.name}
</Link>
</Button>
)}
{neighbours.next && (
<Button
variant="secondary"
size="sm"
className="ml-auto shadow-none"
asChild
>
<Link href={neighbours.next.url}>
{neighbours.next.name} <IconArrowRight />
</Link>
</Button>
)}
<div className="hidden h-16 w-full items-center gap-2 px-4 sm:flex sm:px-0">
{neighbours.previous && (
<Button
variant="secondary"
size="sm"
asChild
className="shadow-none"
>
<Link href={neighbours.previous.url}>
<IconArrowLeft /> {neighbours.previous.name}
</Link>
</Button>
)}
{neighbours.next && (
<Button
variant="secondary"
size="sm"
className="ml-auto shadow-none"
asChild
>
<Link href={neighbours.next.url}>
{neighbours.next.name} <IconArrowRight />
</Link>
</Button>
)}
</div>
</div>
</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" />
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-(--sidebar-width) flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="h-(--top-spacing) shrink-0"></div>
{doc.toc?.length ? (
<div className="no-scrollbar overflow-y-auto px-8">
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
<DocsTableOfContents toc={doc.toc} />
<div className="h-12" />
</div>
) : null}
<div className="flex flex-1 flex-col gap-12 px-6">
<div className="hidden flex-1 flex-col gap-6 px-6 xl:flex">
<OpenInV0Cta />
</div>
</div>

View File

@@ -0,0 +1,144 @@
import Link from "next/link"
import { Button } from "@/examples/radix/ui/button"
import { mdxComponents } from "@/mdx-components"
import { IconRss } from "@tabler/icons-react"
import { getChangelogPages, type ChangelogPageData } from "@/lib/changelog"
import { absoluteUrl } from "@/lib/utils"
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
export const revalidate = false
export const dynamic = "force-static"
export function generateMetadata() {
return {
title: "Changelog",
description: "Latest updates and announcements.",
openGraph: {
title: "Changelog",
description: "Latest updates and announcements.",
type: "article",
url: absoluteUrl("/docs/changelog"),
images: [
{
url: `/og?title=${encodeURIComponent(
"Changelog"
)}&description=${encodeURIComponent(
"Latest updates and announcements."
)}`,
},
],
},
}
}
export default function ChangelogPage() {
const pages = getChangelogPages()
const latestPages = pages.slice(0, 5)
const olderPages = pages.slice(5)
return (
<div
data-slot="docs"
className="flex scroll-mt-24 items-stretch pb-8 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-[40rem] min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h1 className="scroll-m-24 text-4xl font-semibold tracking-tight sm:text-3xl">
Changelog
</h1>
<Button variant="secondary" size="sm" asChild>
<a href="/rss.xml" target="_blank" rel="noopener noreferrer">
<IconRss />
RSS
</a>
</Button>
</div>
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
Latest updates and announcements.
</p>
</div>
<div className="w-full flex-1 pb-16 sm:pb-0">
{latestPages.map((page) => {
const data = page.data as ChangelogPageData
const MDX = page.data.body
return (
<article key={page.url} className="mb-12 border-b pb-12">
<h2 className="font-heading text-xl font-semibold tracking-tight">
{data.title}
</h2>
<div className="prose-changelog mt-6 *:first:mt-0">
<MDX components={mdxComponents} />
</div>
</article>
)
})}
{olderPages.length > 0 && (
<div id="more-updates" className="mb-24 scroll-mt-24">
<h2 className="font-heading mb-6 text-xl font-semibold tracking-tight">
More Updates
</h2>
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">
{olderPages.map((page) => {
const data = page.data as ChangelogPageData
const [date, ...titleParts] = data.title.split(" - ")
const title = titleParts.join(" - ")
return (
<Link
key={page.url}
href={page.url}
className="bg-surface text-surface-foreground hover:bg-surface/80 flex w-full flex-col rounded-xl px-4 py-3 transition-colors"
>
<span className="text-muted-foreground text-xs">
{date}
</span>
<span className="text-sm font-medium">{title}</span>
</Link>
)
})}
</div>
</div>
)}
</div>
</div>
</div>
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 lg:flex">
<div className="h-(--top-spacing) shrink-0"></div>
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
<div className="flex flex-col gap-2 p-4 pt-0 text-sm">
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
On This Page
</p>
{latestPages.map((page) => {
const data = page.data as ChangelogPageData
return (
<Link
key={page.url}
href={page.url}
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
>
{data.title}
</Link>
)
})}
{olderPages.length > 0 && (
<a
href="#more-updates"
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
>
More Updates
</a>
)}
</div>
</div>
<div className="hidden flex-1 flex-col gap-6 px-6 xl:flex">
<OpenInV0Cta />
</div>
</div>
</div>
)
}

View File

@@ -9,7 +9,14 @@ export default function DocsLayout({
}) {
return (
<div className="container-wrapper flex flex-1 flex-col px-2">
<SidebarProvider className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--sidebar-width:220px] [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--sidebar-width:240px] lg:[--top-spacing:calc(var(--spacing)*4)]">
<SidebarProvider
className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--top-spacing:calc(var(--spacing)*4)]"
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
} as React.CSSProperties
}
>
<DocsSidebar tree={source.pageTree} />
<div className="h-full w-full">{children}</div>
</SidebarProvider>

View File

@@ -70,7 +70,7 @@ export default function ExamplesLayout({
</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">
<div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding md:flex-1 xl:rounded-xl">
<div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding has-[[data-slot=rtl-components]]:overflow-visible has-[[data-slot=rtl-components]]:border-0 has-[[data-slot=rtl-components]]:bg-transparent md:flex-1 xl:rounded-xl">
{children}
</div>
</div>

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider"
import type { Slider as SliderPrimitive } from "radix-ui"
import {
HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider"
interface MaxLengthSelectorProps {
defaultValue: SliderProps["defaultValue"]
defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
}
export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) {

View File

@@ -1,8 +1,8 @@
"use client"
import * as React from "react"
import { type PopoverProps } from "@radix-ui/react-popover"
import { Check, ChevronsUpDown } from "lucide-react"
import type { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { useMutationObserver } from "@/hooks/use-mutation-observer"
@@ -29,7 +29,8 @@ import {
import { type Model, type ModelType } from "../data/models"
interface ModelSelectorProps extends PopoverProps {
interface ModelSelectorProps
extends React.ComponentProps<typeof PopoverPrimitive.Root> {
types: readonly ModelType[]
models: Model[]
}

View File

@@ -1,7 +1,6 @@
"use client"
import * as React from "react"
import { Dialog } from "@radix-ui/react-dialog"
import { MoreHorizontal } from "lucide-react"
import { toast } from "sonner"
@@ -16,6 +15,7 @@ import {
} from "@/registry/new-york-v4/ui/alert-dialog"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,

View File

@@ -1,8 +1,8 @@
"use client"
import * as React from "react"
import { type PopoverProps } from "@radix-ui/react-popover"
import { Check, ChevronsUpDown } from "lucide-react"
import type { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york-v4/ui/button"
@@ -23,7 +23,8 @@ import {
import { type Preset } from "../data/presets"
interface PresetSelectorProps extends PopoverProps {
interface PresetSelectorProps
extends React.ComponentProps<typeof PopoverPrimitive.Root> {
presets: Preset[]
}

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider"
import type { Slider as SliderPrimitive } from "radix-ui"
import {
HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider"
interface TemperatureSelectorProps {
defaultValue: SliderProps["defaultValue"]
defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
}
export function TemperatureSelector({

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider"
import type { Slider as SliderPrimitive } from "radix-ui"
import {
HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider"
interface TopPSelectorProps {
defaultValue: SliderProps["defaultValue"]
defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
}
export function TopPSelector({ defaultValue }: TopPSelectorProps) {

View File

@@ -0,0 +1,170 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/examples/base/ui-rtl/field"
import { Input } from "@/examples/base/ui-rtl/input"
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui-rtl/radio-group"
import { Switch } from "@/examples/base/ui-rtl/switch"
import { IconMinus, IconPlus } from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
computeEnvironment: "بيئة الحوسبة",
computeDescription: "اختر بيئة الحوسبة لمجموعتك.",
kubernetes: "كوبرنيتس",
kubernetesDescription:
"تشغيل أحمال عمل GPU على مجموعة مُهيأة بـ K8s. هذا هو الافتراضي.",
virtualMachine: "جهاز افتراضي",
vmDescription: "الوصول إلى مجموعة VM مُهيأة لتشغيل أحمال العمل. (قريبًا)",
numberOfGpus: "عدد وحدات GPU",
gpuDescription: "يمكنك إضافة المزيد لاحقًا.",
decrement: "إنقاص",
increment: "زيادة",
wallpaperTinting: "تلوين الخلفية",
wallpaperDescription: "السماح بتلوين الخلفية.",
},
he: {
dir: "rtl" as const,
computeEnvironment: "סביבת מחשוב",
computeDescription: "בחר את סביבת המחשוב לאשכול שלך.",
kubernetes: "קוברנטיס",
kubernetesDescription:
"הפעל עומסי עבודה של GPU באשכול מוגדר K8s. זו ברירת המחדל.",
virtualMachine: "מכונה וירטואלית",
vmDescription: "גש לאשכול VM מוגדר להפעלת עומסי עבודה. (בקרוב)",
numberOfGpus: "מספר GPUs",
gpuDescription: "תוכל להוסיף עוד מאוחר יותר.",
decrement: "הפחת",
increment: "הגדל",
wallpaperTinting: "צביעת טפט",
wallpaperDescription: "אפשר לטפט להיצבע.",
},
}
export function AppearanceSettings() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
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 (
<div dir={t.dir}>
<FieldSet>
<FieldGroup>
<FieldSet>
<FieldLegend>{t.computeEnvironment}</FieldLegend>
<FieldDescription>{t.computeDescription}</FieldDescription>
<RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="rtl-kubernetes">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{t.kubernetes}</FieldTitle>
<FieldDescription>
{t.kubernetesDescription}
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="kubernetes"
id="rtl-kubernetes"
aria-label={t.kubernetes}
/>
</Field>
</FieldLabel>
<FieldLabel htmlFor="rtl-vm">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{t.virtualMachine}</FieldTitle>
<FieldDescription>{t.vmDescription}</FieldDescription>
</FieldContent>
<RadioGroupItem
value="vm"
id="rtl-vm"
aria-label={t.virtualMachine}
/>
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="rtl-gpu-count">{t.numberOfGpus}</FieldLabel>
<FieldDescription>{t.gpuDescription}</FieldDescription>
</FieldContent>
<ButtonGroup>
<Input
id="rtl-gpu-count"
value={gpuCount}
onChange={handleGpuInputChange}
size={3}
className="h-7 !w-14 font-mono"
maxLength={3}
/>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label={t.decrement}
onClick={() => handleGpuAdjustment(-1)}
disabled={gpuCount <= 1}
>
<IconMinus />
</Button>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label={t.increment}
onClick={() => handleGpuAdjustment(1)}
disabled={gpuCount >= 99}
>
<IconPlus />
</Button>
</ButtonGroup>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="rtl-tinting">
{t.wallpaperTinting}
</FieldLabel>
<FieldDescription>{t.wallpaperDescription}</FieldDescription>
</FieldContent>
<Switch id="rtl-tinting" defaultChecked />
</Field>
</FieldGroup>
</FieldSet>
</div>
)
}

View File

@@ -0,0 +1,179 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
goBack: "رجوع",
archive: "أرشفة",
report: "إبلاغ",
snooze: "تأجيل",
moreOptions: "خيارات أخرى",
markAsRead: "تحديد كمقروء",
addToCalendar: "إضافة إلى التقويم",
addToList: "إضافة إلى القائمة",
labelAs: "تصنيف كـ...",
personal: "شخصي",
work: "عمل",
other: "أخرى",
trash: "حذف",
},
he: {
dir: "rtl" as const,
goBack: "חזור",
archive: "ארכיון",
report: "דווח",
snooze: "נודניק",
moreOptions: "אפשרויות נוספות",
markAsRead: "סמן כנקרא",
addToCalendar: "הוסף ליומן",
addToList: "הוסף לרשימה",
labelAs: "תייג כ...",
personal: "אישי",
work: "עבודה",
other: "אחר",
trash: "מחק",
},
}
export function ButtonGroupDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [label, setLabel] = React.useState("personal")
return (
<div dir={t.dir}>
<ButtonGroup>
<ButtonGroup className="hidden sm:flex">
<Button variant="outline" size="icon-sm" aria-label={t.goBack}>
<ArrowLeftIcon className="rtl:rotate-180" />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
{t.archive}
</Button>
<Button variant="outline" size="sm">
{t.report}
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
{t.snooze}
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
size="icon-sm"
aria-label={t.moreOptions}
/>
}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
dir={t.dir}
data-lang={lang}
className="w-44"
>
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />
{t.markAsRead}
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
{t.archive}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<ClockIcon />
{t.snooze}
</DropdownMenuItem>
<DropdownMenuItem>
<CalendarPlusIcon />
{t.addToCalendar}
</DropdownMenuItem>
<DropdownMenuItem>
<ListFilterIcon />
{t.addToList}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<TagIcon />
{t.labelAs}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent
side="left"
dir={t.dir}
data-lang={lang}
>
<DropdownMenuRadioGroup
value={label}
onValueChange={setLabel}
>
<DropdownMenuRadioItem value="personal">
{t.personal}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="work">
{t.work}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="other">
{t.other}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
{t.trash}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</ButtonGroup>
</div>
)
}

View File

@@ -0,0 +1,82 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/examples/base/ui-rtl/input-group"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
add: "إضافة",
voicePlaceholder: "سجل وأرسل صوتًا...",
messagePlaceholder: "أرسل رسالة...",
voiceMode: "الوضع الصوتي",
},
he: {
dir: "rtl" as const,
add: "הוסף",
voicePlaceholder: "הקלט ושלח אודיו...",
messagePlaceholder: "שלח הודעה...",
voiceMode: "מצב קולי",
},
}
export function ButtonGroupInputGroup() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
return (
<ButtonGroup dir={t.dir}>
<ButtonGroup>
<Button variant="outline" size="icon" aria-label={t.add}>
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup className="flex-1">
<InputGroup>
<InputGroupInput
placeholder={
voiceEnabled ? t.voicePlaceholder : t.messagePlaceholder
}
disabled={voiceEnabled}
/>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
render={
<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={t.voiceMode}
/>
}
>
<AudioLinesIcon />
</TooltipTrigger>
<TooltipContent>{t.voiceMode}</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,56 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
locale: "ar-SA",
previous: "السابق",
next: "التالي",
},
he: {
dir: "rtl" as const,
locale: "he-IL",
previous: "הקודם",
next: "הבא",
},
}
function formatNumber(value: number, locale: string) {
return new Intl.NumberFormat(locale).format(value)
}
export function ButtonGroupNested() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<ButtonGroup dir={t.dir}>
<ButtonGroup>
<Button variant="outline" size="sm">
{formatNumber(1, t.locale)}
</Button>
<Button variant="outline" size="sm">
{formatNumber(2, t.locale)}
</Button>
<Button variant="outline" size="sm">
{formatNumber(3, t.locale)}
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon-sm" aria-label={t.previous}>
<ArrowLeftIcon className="rtl:rotate-180" />
</Button>
<Button variant="outline" size="icon-sm" aria-label={t.next}>
<ArrowRightIcon className="rtl:rotate-180" />
</Button>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,83 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui-rtl/popover"
import { Separator } from "@/examples/base/ui-rtl/separator"
import { Textarea } from "@/examples/base/ui-rtl/textarea"
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
copilot: "المساعد",
openPopover: "فتح القائمة",
agentTasks: "مهام الوكيل",
placeholder: "صف مهمتك بلغة طبيعية.",
startTask: "ابدأ مهمة جديدة مع المساعد",
description:
"صف مهمتك بلغة طبيعية. سيعمل المساعد في الخلفية ويفتح طلب سحب لمراجعتك.",
},
he: {
dir: "rtl" as const,
copilot: "עוזר",
openPopover: "פתח תפריט",
agentTasks: "משימות סוכן",
placeholder: "תאר את המשימה שלך בשפה טבעית.",
startTask: "התחל משימה חדשה עם העוזר",
description:
"תאר את המשימה שלך בשפה טבעית. העוזר יעבוד ברקע ויפתח בקשת משיכה לבדיקתך.",
},
}
export function ButtonGroupPopover() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<ButtonGroup dir={t.dir}>
<Button variant="outline" size="sm">
<BotIcon /> {t.copilot}
</Button>
<Popover>
<PopoverTrigger
render={
<Button
variant="outline"
size="icon-sm"
aria-label={t.openPopover}
/>
}
>
<ChevronDownIcon />
</PopoverTrigger>
<PopoverContent
align="start"
dir={t.dir}
data-lang={lang}
className="p-0"
>
<div className="px-4 py-3">
<div className="text-sm font-medium">{t.agentTasks}</div>
</div>
<Separator />
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
<Textarea
placeholder={t.placeholder}
className="mb-4 resize-none"
/>
<p className="font-medium">{t.startTask}</p>
<p className="text-muted-foreground">{t.description}</p>
</div>
</PopoverContent>
</Popover>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,78 @@
"use client"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/examples/base/ui-rtl/avatar"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/examples/base/ui-rtl/empty"
import { PlusIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
title: "لا يوجد أعضاء في الفريق",
description: "قم بدعوة فريقك للتعاون في هذا المشروع.",
invite: "دعوة أعضاء",
},
he: {
dir: "rtl" as const,
title: "אין חברי צוות",
description: "הזמן את הצוות שלך לשתף פעולה בפרויקט זה.",
invite: "הזמן חברים",
},
}
export function EmptyAvatarGroup() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<Empty className="flex-none border py-10" dir={t.dir}>
<EmptyHeader>
<EmptyMedia>
<AvatarGroup className="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>
</AvatarGroup>
</EmptyMedia>
<EmptyTitle>{t.title}</EmptyTitle>
<EmptyDescription>{t.description}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">
<PlusIcon />
{t.invite}
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -0,0 +1,36 @@
"use client"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import { Field, FieldLabel } from "@/examples/base/ui-rtl/field"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
terms: "أوافق على الشروط والأحكام",
},
he: {
dir: "rtl" as const,
terms: "אני מסכים לתנאים וההגבלות",
},
}
export function FieldCheckbox() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const { dir, terms } = translations[lang]
return (
<div dir={dir}>
<FieldLabel htmlFor="checkbox-demo-rtl">
<Field orientation="horizontal">
<Checkbox id="checkbox-demo-rtl" defaultChecked />
<FieldLabel htmlFor="checkbox-demo-rtl" className="line-clamp-1">
{terms}
</FieldLabel>
</Field>
</FieldLabel>
</div>
)
}

View File

@@ -0,0 +1,217 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/examples/base/ui-rtl/field"
import { Input } from "@/examples/base/ui-rtl/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/examples/base/ui-rtl/select"
import { Textarea } from "@/examples/base/ui-rtl/textarea"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
locale: "ar-SA",
paymentMethod: "طريقة الدفع",
secureEncrypted: "جميع المعاملات آمنة ومشفرة",
nameOnCard: "الاسم على البطاقة",
namePlaceholder: "أحمد محمد",
cardNumber: "رقم البطاقة",
cardDescription: "أدخل رقمك المكون من 16 رقمًا.",
cvv: "رمز الأمان",
month: "الشهر",
year: "السنة",
billingAddress: "عنوان الفواتير",
billingDescription: "عنوان الفواتير المرتبط بطريقة الدفع الخاصة بك",
sameAsShipping: "نفس عنوان الشحن",
comments: "تعليقات",
commentsPlaceholder: "أضف أي تعليقات إضافية",
submit: "إرسال",
cancel: "إلغاء",
},
he: {
dir: "rtl" as const,
locale: "he-IL",
paymentMethod: "אמצעי תשלום",
secureEncrypted: "כל העסקאות מאובטחות ומוצפנות",
nameOnCard: "שם על הכרטיס",
namePlaceholder: "ישראל ישראלי",
cardNumber: "מספר כרטיס",
cardDescription: "הזן את המספר בן 16 הספרות שלך.",
cvv: "קוד אבטחה",
month: "חודש",
year: "שנה",
billingAddress: "כתובת לחיוב",
billingDescription: "כתובת החיוב המשויכת לאמצעי התשלום שלך",
sameAsShipping: "זהה לכתובת המשלוח",
comments: "הערות",
commentsPlaceholder: "הוסף הערות נוספות",
submit: "שלח",
cancel: "ביטול",
},
}
function formatCardNumber(locale: string) {
const formatter = new Intl.NumberFormat(locale, { useGrouping: false })
return `${formatter.format(1234)} ${formatter.format(5678)} ${formatter.format(9012)} ${formatter.format(3456)}`
}
function formatCvv(locale: string) {
return new Intl.NumberFormat(locale, { useGrouping: false }).format(123)
}
function getMonths(locale: string) {
const formatter = new Intl.NumberFormat(locale, {
minimumIntegerDigits: 2,
useGrouping: false,
})
return Array.from({ length: 12 }, (_, i) => {
const value = String(i + 1).padStart(2, "0")
return { label: formatter.format(i + 1), value }
})
}
function getYears(locale: string) {
const formatter = new Intl.NumberFormat(locale, { useGrouping: false })
return Array.from({ length: 6 }, (_, i) => {
const year = 2024 + i
return { label: formatter.format(year), value: String(year) }
})
}
export function FieldDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const months = getMonths(t.locale)
const years = getYears(t.locale)
const cardPlaceholder = formatCardNumber(t.locale)
const cvvPlaceholder = formatCvv(t.locale)
return (
<div dir={t.dir} className="w-full max-w-md rounded-lg border p-6">
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>{t.paymentMethod}</FieldLegend>
<FieldDescription>{t.secureEncrypted}</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="rtl-card-name">{t.nameOnCard}</FieldLabel>
<Input
id="rtl-card-name"
placeholder={t.namePlaceholder}
required
/>
</Field>
<div className="grid grid-cols-3 gap-4">
<Field className="col-span-2">
<FieldLabel htmlFor="rtl-card-number">
{t.cardNumber}
</FieldLabel>
<Input
id="rtl-card-number"
placeholder={cardPlaceholder}
required
/>
<FieldDescription>{t.cardDescription}</FieldDescription>
</Field>
<Field className="col-span-1">
<FieldLabel htmlFor="rtl-cvv">{t.cvv}</FieldLabel>
<Input id="rtl-cvv" placeholder={cvvPlaceholder} required />
</Field>
</div>
<div className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="rtl-exp-month">{t.month}</FieldLabel>
<Select defaultValue="" items={months}>
<SelectTrigger id="rtl-exp-month">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent data-lang={lang} dir={t.dir}>
<SelectGroup>
{months.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="rtl-exp-year">{t.year}</FieldLabel>
<Select defaultValue="" items={years}>
<SelectTrigger id="rtl-exp-year">
<SelectValue placeholder="YYYY" />
</SelectTrigger>
<SelectContent data-lang={lang} dir={t.dir}>
<SelectGroup>
{years.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
</div>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>{t.billingAddress}</FieldLegend>
<FieldDescription>{t.billingDescription}</FieldDescription>
<FieldGroup>
<Field orientation="horizontal">
<Checkbox id="rtl-same-as-shipping" defaultChecked />
<FieldLabel
htmlFor="rtl-same-as-shipping"
className="font-normal"
>
{t.sameAsShipping}
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="rtl-comments">{t.comments}</FieldLabel>
<Textarea
id="rtl-comments"
placeholder={t.commentsPlaceholder}
className="resize-none"
/>
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button type="submit">{t.submit}</Button>
<Button variant="outline" type="button">
{t.cancel}
</Button>
</Field>
</FieldGroup>
</form>
</div>
)
}

View File

@@ -0,0 +1,90 @@
"use client"
import { Card, CardContent } from "@/examples/base/ui-rtl/card"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/examples/base/ui-rtl/field"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
legend: "كيف سمعت عنا؟",
description: "اختر الخيار الذي يصف أفضل طريقة سمعت عنا من خلالها.",
socialMedia: "التواصل الاجتماعي",
searchEngine: "البحث",
referral: "إحالة",
other: "أخرى",
},
he: {
dir: "rtl" as const,
legend: "איך שמעת עלינו?",
description: "בחר את האפשרות שמתארת בצורה הטובה ביותר כיצד שמעת עלינו.",
socialMedia: "חברתיות",
searchEngine: "חיפוש",
referral: "הפניה",
other: "אחר",
},
}
export function FieldHear() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const options = [
{ label: t.socialMedia, value: "social-media" },
{ label: t.searchEngine, value: "search-engine" },
{ label: t.referral, value: "referral" },
{ label: t.other, value: "other" },
]
return (
<div dir={t.dir}>
<Card className="border-0 py-4 shadow-none">
<CardContent className="px-4">
<form>
<FieldGroup>
<FieldSet className="gap-4">
<FieldLegend>{t.legend}</FieldLegend>
<FieldDescription className="line-clamp-1">
{t.description}
</FieldDescription>
<FieldGroup className="flex flex-row flex-wrap gap-2 [--radius:9999rem]">
{options.map((option) => (
<FieldLabel
htmlFor={`rtl-${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={`rtl-${option.value}`}
defaultChecked={option.value === "social-media"}
className="-ms-6 translate-x-1 rounded-full transition-all duration-100 ease-linear data-checked:ms-0 data-checked:translate-x-0"
/>
<FieldTitle>{option.label}</FieldTitle>
</Field>
</FieldLabel>
))}
</FieldGroup>
</FieldSet>
</FieldGroup>
</form>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,67 @@
"use client"
import { useState } from "react"
import {
Field,
FieldDescription,
FieldTitle,
} from "@/examples/base/ui-rtl/field"
import { Slider } from "@/examples/base/ui-rtl/slider"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
locale: "ar-SA",
title: "نطاق السعر",
description: "حدد نطاق ميزانيتك",
ariaLabel: "نطاق السعر",
currency: "﷼",
},
he: {
dir: "rtl" as const,
locale: "he-IL",
title: "טווח מחירים",
description: "הגדר את טווח התקציב שלך",
ariaLabel: "טווח מחירים",
currency: "₪",
},
}
function formatNumber(value: number, locale: string) {
return new Intl.NumberFormat(locale).format(value)
}
export function FieldSlider() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [value, setValue] = useState([200, 800])
return (
<Field dir={t.dir}>
<FieldTitle>{t.title}</FieldTitle>
<FieldDescription>
{t.description} ({t.currency}
<span className="font-medium tabular-nums">
{formatNumber(value[0], t.locale)}
</span>{" "}
-{" "}
<span className="font-medium tabular-nums">
{formatNumber(value[1], t.locale)}
</span>
).
</FieldDescription>
<Slider
value={value}
onValueChange={(value) => setValue(value as [number, number])}
max={1000}
min={0}
step={10}
className="mt-2 w-full"
aria-label={t.ariaLabel}
/>
</Field>
)
}

View File

@@ -0,0 +1,92 @@
"use client"
import { DirectionProvider } from "@/examples/base/ui-rtl/direction"
import { FieldSeparator } from "@/examples/base/ui-rtl/field"
import {
LanguageProvider,
LanguageSelector,
useLanguageContext,
} from "@/components/language-selector"
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"
function RtlComponentsContent() {
const context = useLanguageContext()
if (!context) {
return null
}
const { language } = context
return (
<div
className="relative grid gap-8 p-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8"
dir="rtl"
data-lang={language}
data-slot="rtl-components"
>
<LanguageSelector
value={language}
onValueChange={context.setLanguage}
className="absolute -top-12 right-52 hidden h-8! data-[size=sm]:rounded-lg lg:flex"
languages={["ar", "he"]}
/>
<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">
{language === "he" ? "הגדרות מראה" : "إعدادات المظهر"}
</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>
)
}
export function RtlComponents() {
return (
<LanguageProvider defaultLanguage="ar">
<DirectionProvider direction="rtl">
<RtlComponentsContent />
</DirectionProvider>
</LanguageProvider>
)
}

View File

@@ -0,0 +1,97 @@
"use client"
import * as React from "react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/examples/base/ui-rtl/input-group"
import { Label } from "@/examples/base/ui-rtl/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui-rtl/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
inputLabel: "السعر",
info: "معلومات",
priceInfo: "أدخل السعر بالريال السعودي.",
priceDescription: "سيتم تحويل السعر تلقائياً.",
favorite: "مفضل",
currency: "ر.س",
},
he: {
dir: "rtl" as const,
inputLabel: "מחיר",
info: "מידע",
priceInfo: "הזן את המחיר בשקלים.",
priceDescription: "המחיר יומר אוטומטית.",
favorite: "מועדף",
currency: "₪",
},
}
export function InputGroupButtonExample() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [isFavorite, setIsFavorite] = React.useState(false)
return (
<div dir={t.dir} className="grid w-full max-w-sm gap-6">
<Label htmlFor="input-secure-rtl" className="sr-only">
{t.inputLabel}
</Label>
<InputGroup className="[--radius:9999px]">
<InputGroupInput id="input-secure-rtl" className="!pr-0.5" />
<InputGroupAddon>
<Popover>
<PopoverTrigger
render={
<InputGroupButton
variant="secondary"
size="icon-xs"
aria-label={t.info}
/>
}
>
<IconInfoCircle />
</PopoverTrigger>
<PopoverContent
align="end"
alignOffset={10}
className="flex flex-col gap-1 rounded-xl text-sm"
data-lang={lang}
dir={t.dir}
>
<p className="font-medium">{t.priceInfo}</p>
<p>{t.priceDescription}</p>
</PopoverContent>
</Popover>
</InputGroupAddon>
<InputGroupAddon className="text-muted-foreground">
{t.currency}
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton
onClick={() => setIsFavorite(!isFavorite)}
size="icon-xs"
aria-label={t.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,140 @@
"use client"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/examples/base/ui-rtl/input-group"
import { Separator } from "@/examples/base/ui-rtl/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import {
IconCheck,
IconChevronDown,
IconInfoCircle,
IconPlus,
} from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
search: "بحث...",
results: "12 نتيجة",
example: "example.com",
tooltipContent: "هذا محتوى في تلميح.",
askSearchChat: "اسأل، ابحث أو تحدث...",
add: "إضافة",
auto: "تلقائي",
agent: "وكيل",
manual: "يدوي",
used: "52% مستخدم",
send: "إرسال",
},
he: {
dir: "rtl" as const,
search: "חיפוש...",
results: "12 תוצאות",
example: "example.com",
tooltipContent: "זה תוכן בטולטיפ.",
askSearchChat: "שאל, חפש או שוחח...",
add: "הוסף",
auto: "אוטומטי",
agent: "סוכן",
manual: "ידני",
used: "52% בשימוש",
send: "שלח",
},
}
export function InputGroupDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder={t.search} />
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupAddon align="inline-end">{t.results}</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder={t.example} />
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton
className="rounded-full"
size="icon-xs"
aria-label={t.add}
/>
}
>
<IconInfoCircle />
</TooltipTrigger>
<TooltipContent>{t.tooltipContent}</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder={t.askSearchChat} />
<InputGroupAddon align="block-end">
<InputGroupButton
variant="outline"
className="rounded-full"
size="icon-xs"
aria-label={t.add}
>
<IconPlus />
</InputGroupButton>
<DropdownMenu>
<DropdownMenuTrigger render={<InputGroupButton variant="ghost" />}>
<IconChevronDown />
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem>{t.auto}</DropdownMenuItem>
<DropdownMenuItem>{t.agent}</DropdownMenuItem>
<DropdownMenuItem>{t.manual}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText className="ms-auto">{t.used}</InputGroupText>
<Separator orientation="vertical" className="!h-4" />
<InputGroupButton
variant="default"
className="rounded-full"
size="icon-xs"
>
<ArrowUpIcon />
<span className="sr-only">{t.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,64 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/examples/base/ui-rtl/item"
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
twoFactor: "المصادقة الثنائية",
twoFactorDescription: "التحقق عبر البريد الإلكتروني أو رقم الهاتف.",
enable: "تفعيل",
verified: "تم التحقق من ملفك الشخصي.",
},
he: {
dir: "rtl" as const,
twoFactor: "אימות דו-שלבי",
twoFactorDescription: "אמת באמצעות אימייל או מספר טלפון.",
enable: "הפעל",
verified: "הפרופיל שלך אומת.",
},
}
export function ItemDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="flex w-full max-w-md flex-col gap-6">
<Item variant="outline">
<ItemContent>
<ItemTitle>{t.twoFactor}</ItemTitle>
<ItemDescription className="text-pretty xl:hidden 2xl:block">
{t.twoFactorDescription}
</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm">{t.enable}</Button>
</ItemActions>
</Item>
<Item variant="outline" size="sm">
<ItemMedia>
<BadgeCheckIcon className="size-5" />
</ItemMedia>
<ItemContent>
<ItemTitle>{t.verified}</ItemTitle>
</ItemContent>
<ItemActions>
<ChevronRightIcon className="size-4 rtl:rotate-180" />
</ItemActions>
</Item>
</div>
)
}

View File

@@ -0,0 +1,516 @@
"use client"
import { useMemo, useState } from "react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/examples/base/ui-rtl/avatar"
import { Badge } from "@/examples/base/ui-rtl/badge"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/examples/base/ui-rtl/command"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import { Field, FieldLabel } from "@/examples/base/ui-rtl/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/examples/base/ui-rtl/input-group"
import { Popover, PopoverContent } from "@/examples/base/ui-rtl/popover"
import { Switch } from "@/examples/base/ui-rtl/switch"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
prompt: "الأمر",
placeholder: "اسأل، ابحث، أو أنشئ أي شيء...",
addContext: "أضف سياق",
mentionTooltip: "اذكر شخصًا أو صفحة أو تاريخًا",
searchPages: "البحث في الصفحات...",
noPagesFound: "لم يتم العثور على صفحات",
pages: "الصفحات",
users: "المستخدمون",
attachFile: "إرفاق ملف",
selectModel: "اختر نموذج الذكاء الاصطناعي",
selectAgentMode: "اختر وضع الوكيل",
webSearch: "البحث على الويب",
appsIntegrations: "التطبيقات والتكاملات",
allSourcesAccess: "جميع المصادر التي يمكنني الوصول إليها",
findKnowledge: "ابحث أو استخدم المعرفة في...",
noKnowledgeFound: "لم يتم العثور على معرفة",
helpCenter: "مركز المساعدة",
connectApps: "ربط التطبيقات",
searchSourcesNote: "سنبحث فقط في المصادر المحددة هنا.",
send: "إرسال",
allSources: "جميع المصادر",
auto: "تلقائي",
agentMode: "وضع الوكيل",
planMode: "وضع التخطيط",
beta: "تجريبي",
workspace: "مساحة العمل",
meetingNotes: "ملاحظات الاجتماع",
projectDashboard: "لوحة المشروع",
ideasBrainstorming: "أفكار وعصف ذهني",
calendarEvents: "التقويم والأحداث",
documentation: "التوثيق",
goalsObjectives: "الأهداف والغايات",
budgetPlanning: "تخطيط الميزانية",
teamDirectory: "دليل الفريق",
technicalSpecs: "المواصفات التقنية",
analyticsReport: "تقرير التحليلات",
},
he: {
dir: "rtl" as const,
prompt: "פקודה",
placeholder: "שאל, חפש, או צור משהו...",
addContext: "הוסף הקשר",
mentionTooltip: "הזכר אדם, עמוד או תאריך",
searchPages: "חפש עמודים...",
noPagesFound: "לא נמצאו עמודים",
pages: "עמודים",
users: "משתמשים",
attachFile: "צרף קובץ",
selectModel: "בחר מודל AI",
selectAgentMode: "בחר מצב סוכן",
webSearch: "חיפוש באינטרנט",
appsIntegrations: "אפליקציות ואינטגרציות",
allSourcesAccess: "כל המקורות שיש לי גישה אליהם",
findKnowledge: "מצא או השתמש בידע ב...",
noKnowledgeFound: "לא נמצא ידע",
helpCenter: "מרכז עזרה",
connectApps: "חבר אפליקציות",
searchSourcesNote: "נחפש רק במקורות שנבחרו כאן.",
send: "שלח",
allSources: "כל המקורות",
auto: "אוטומטי",
agentMode: "מצב סוכן",
planMode: "מצב תכנון",
beta: "בטא",
workspace: "סביבת עבודה",
meetingNotes: "הערות פגישה",
projectDashboard: "לוח מחוונים לפרויקט",
ideasBrainstorming: "רעיונות וסיעור מוחות",
calendarEvents: "יומן ואירועים",
documentation: "תיעוד",
goalsObjectives: "מטרות ויעדים",
budgetPlanning: "תכנון תקציב",
teamDirectory: "ספריית צוות",
technicalSpecs: "מפרט טכני",
analyticsReport: "דוח אנליטיקה",
},
}
function MentionableIcon({
item,
}: {
item: { type: string; title: string; image: string }
}) {
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 context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const SAMPLE_DATA = useMemo(
() => ({
mentionable: [
{ type: "page", title: t.meetingNotes, image: "📝" },
{ type: "page", title: t.projectDashboard, image: "📊" },
{ type: "page", title: t.ideasBrainstorming, image: "💡" },
{ type: "page", title: t.calendarEvents, image: "📅" },
{ type: "page", title: t.documentation, image: "📚" },
{ type: "page", title: t.goalsObjectives, image: "🎯" },
{ type: "page", title: t.budgetPlanning, image: "💰" },
{ type: "page", title: t.teamDirectory, image: "👥" },
{ type: "page", title: t.technicalSpecs, image: "🔧" },
{ type: "page", title: t.analyticsReport, image: "📈" },
{
type: "user",
title: "shadcn",
image: "https://github.com/shadcn.png",
workspace: t.workspace,
},
{
type: "user",
title: "maxleiter",
image: "https://github.com/maxleiter.png",
workspace: t.workspace,
},
{
type: "user",
title: "evilrabbit",
image: "https://github.com/evilrabbit.png",
workspace: t.workspace,
},
],
models: [
{ name: t.auto },
{ name: t.agentMode, badge: t.beta },
{ name: t.planMode },
],
}),
[t]
)
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, SAMPLE_DATA])
const hasMentions = mentions.length > 0
return (
<div dir={t.dir}>
<form>
<Field>
<FieldLabel htmlFor="rtl-notion-prompt" className="sr-only">
{t.prompt}
</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="rtl-notion-prompt"
placeholder={t.placeholder}
/>
<InputGroupAddon align="block-start">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
>
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="rounded-full transition-transform"
/>
}
onFocusCapture={(e) => e.stopPropagation()}
>
<IconAt /> {!hasMentions && t.addContext}
</TooltipTrigger>
<TooltipContent>{t.mentionTooltip}</TooltipContent>
</Tooltip>
<PopoverContent className="p-0" align="start" dir={t.dir}>
<Command>
<CommandInput placeholder={t.searchPages} />
<CommandList>
<CommandEmpty>{t.noPagesFound}</CommandEmpty>
{Object.entries(grouped).map(([type, items]) => (
<CommandGroup
key={type}
heading={type === "page" ? t.pages : t.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 !pr-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
render={
<InputGroupButton
size="icon-sm"
className="rounded-full"
aria-label={t.attachFile}
/>
}
>
<IconPaperclip />
</TooltipTrigger>
<TooltipContent>{t.attachFile}</TooltipContent>
</Tooltip>
<DropdownMenu
open={modelPopoverOpen}
onOpenChange={setModelPopoverOpen}
>
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton size="sm" className="rounded-full" />
}
>
{selectedModel.name}
</TooltipTrigger>
<TooltipContent>{t.selectModel}</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="top"
align="start"
className="w-48"
dir={t.dir}
>
<DropdownMenuGroup className="w-48">
<DropdownMenuLabel className="text-muted-foreground text-xs">
{t.selectAgentMode}
</DropdownMenuLabel>
{SAMPLE_DATA.models.map((model) => (
<DropdownMenuCheckboxItem
key={model.name}
checked={model.name === selectedModel.name}
onCheckedChange={(checked) => {
if (checked) {
setSelectedModel(model)
}
}}
className="pr-2 *:[span:first-child]:right-auto *:[span:first-child]:left-2"
>
{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
render={
<InputGroupButton size="sm" className="rounded-full" />
}
>
<IconWorld /> {t.allSources}
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="w-72"
dir={t.dir}
>
<DropdownMenuGroup>
<DropdownMenuItem
render={
<label htmlFor="rtl-web-search">
<IconWorld /> {t.webSearch}{" "}
<Switch
id="rtl-web-search"
className="ms-auto"
defaultChecked
size="sm"
/>
</label>
}
onSelect={(e) => e.preventDefault()}
></DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
render={
<label htmlFor="rtl-apps">
<IconApps /> {t.appsIntegrations}
<Switch
id="rtl-apps"
className="ms-auto"
defaultChecked
size="sm"
/>
</label>
}
onSelect={(e) => e.preventDefault()}
></DropdownMenuItem>
<DropdownMenuItem>
<IconCircleDashedPlus /> {t.allSourcesAccess}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Avatar className="size-4">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
shadcn
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="w-72 rounded-lg p-0"
dir={t.dir}
side="left"
>
<Command>
<CommandInput
placeholder={t.findKnowledge}
autoFocus
/>
<CommandList>
<CommandEmpty>{t.noKnowledgeFound}</CommandEmpty>
<CommandGroup>
{SAMPLE_DATA.mentionable
.filter((item) => item.type === "user")
.map((user) => (
<CommandItem
key={user.title}
value={user.title}
onSelect={() => {
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 as { workspace?: string })
.workspace
}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem>
<IconBook /> {t.helpCenter}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconPlus /> {t.connectApps}
</DropdownMenuItem>
<DropdownMenuLabel className="text-muted-foreground text-xs">
{t.searchSourcesNote}
</DropdownMenuLabel>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupButton
aria-label={t.send}
className="ms-auto rounded-full"
variant="default"
size="icon-sm"
>
<IconArrowUp />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</form>
</div>
)
}

View File

@@ -0,0 +1,44 @@
"use client"
import { Badge } from "@/examples/base/ui-rtl/badge"
import { Spinner } from "@/examples/base/ui-rtl/spinner"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
syncing: "جارٍ المزامنة",
updating: "جارٍ التحديث",
loading: "جارٍ التحميل",
},
he: {
dir: "rtl" as const,
syncing: "מסנכרן",
updating: "מעדכן",
loading: "טוען",
},
}
export function SpinnerBadge() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="flex items-center gap-2">
<Badge>
<Spinner />
{t.syncing}
</Badge>
<Badge variant="secondary">
<Spinner />
{t.updating}
</Badge>
<Badge variant="outline">
<Spinner />
{t.loading}
</Badge>
</div>
)
}

View File

@@ -0,0 +1,52 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/examples/base/ui-rtl/empty"
import { Spinner } from "@/examples/base/ui-rtl/spinner"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
title: "جارٍ معالجة طلبك",
description: "يرجى الانتظار بينما نعالج طلبك. لا تقم بتحديث الصفحة.",
cancel: "إلغاء",
},
he: {
dir: "rtl" as const,
title: "מעבד את הבקשה שלך",
description: "אנא המתן בזמן שאנו מעבדים את בקשתך. אל תרענן את הדף.",
cancel: "ביטול",
},
}
export function SpinnerEmpty() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<Empty className="w-full border md:p-6" dir={t.dir}>
<EmptyHeader>
<EmptyMedia variant="icon">
<Spinner />
</EmptyMedia>
<EmptyTitle>{t.title}</EmptyTitle>
<EmptyDescription>{t.description}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm">
{t.cancel}
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -0,0 +1,14 @@
import { type Metadata } from "next"
import { RtlComponents } from "./components"
export const metadata: Metadata = {
title: "RTL",
description: "RTL example page with right-to-left language support.",
}
export function RtlPage() {
return <RtlComponents />
}
export default RtlPage

View File

@@ -1,6 +1,5 @@
"use client"
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
import { type Table } from "@tanstack/react-table"
import { Settings2 } from "lucide-react"
@@ -11,6 +10,7 @@ import {
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
export function DataTableViewOptions<TData>({

View File

@@ -3,10 +3,23 @@ import { NextResponse, type NextRequest } from "next/server"
import { processMdxForLLMs } from "@/lib/llm"
import { source } from "@/lib/source"
import { getActiveStyle } from "@/registry/_legacy-styles"
import { getActiveStyle, type Style } from "@/registry/_legacy-styles"
export const revalidate = false
function getStyleFromSlug(slug: string[] | undefined, fallbackStyle: string) {
// Detect base from URL: /docs/components/base/... or /docs/components/radix/...
if (slug && slug[0] === "components" && slug[1]) {
if (slug[1] === "base") {
return "base-nova"
}
if (slug[1] === "radix") {
return "new-york-v4"
}
}
return fallbackStyle
}
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> }
@@ -19,9 +32,11 @@ export async function GET(
notFound()
}
const effectiveStyle = getStyleFromSlug(slug, activeStyle.name)
const processedContent = processMdxForLLMs(
await page.data.getText("raw"),
activeStyle.name
effectiveStyle as Style["name"]
)
return new NextResponse(processedContent, {

View File

@@ -38,21 +38,6 @@ export function Customizer() {
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

View File

@@ -59,7 +59,7 @@ export function FontPicker({
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-80 md:w-72"
className="max-h-96 md:w-72"
>
<PickerRadioGroup
value={currentFont?.value}

View File

@@ -79,7 +79,7 @@ export function ItemExplorer({
)}
<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"
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}
>

View File

@@ -99,7 +99,7 @@ export function ItemPicker({
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"
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-64"
/>
}
>
@@ -123,9 +123,9 @@ export function ItemPicker({
<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"
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 xl:w-96"
side="bottom"
align="center"
align="end"
>
<ComboboxInput
showTrigger={false}

View File

@@ -80,7 +80,7 @@ function PickerLabel({
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",
"text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8",
className
)}
{...props}
@@ -103,7 +103,7 @@ function PickerItem({
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",
"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}
@@ -128,7 +128,7 @@ function PickerSubTrigger({
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",
"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}
@@ -180,7 +180,7 @@ function PickerCheckboxItem({
<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",
"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}
@@ -220,7 +220,7 @@ function PickerRadioItem({
<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",
"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}

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type ImperativePanelHandle } from "react-resizable-panels"
import { type PanelImperativeHandle } from "react-resizable-panels"
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
import { Badge } from "@/registry/new-york-v4/ui/badge"
@@ -16,7 +16,7 @@ import {
export function Preview() {
const [params] = useDesignSystemSearchParams()
const iframeRef = React.useRef<HTMLIFrameElement>(null)
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
const resizablePanelRef = React.useRef<PanelImperativeHandle>(null)
// Sync resizable panel with URL param changes.
React.useEffect(() => {

View File

@@ -56,7 +56,7 @@ export function ShareButton() {
<Button
size="sm"
variant="outline"
className="rounded-lg shadow-none"
className="rounded-lg shadow-none lg:w-8 xl:w-fit"
onClick={handleCopy}
>
{hasCopied ? (
@@ -64,7 +64,7 @@ export function ShareButton() {
) : (
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
)}
Share
<span className="lg:hidden xl:block">Share</span>
</Button>
</TooltipTrigger>
<TooltipContent>Copy Link</TooltipContent>

View File

@@ -76,7 +76,7 @@ export function ThemePicker({
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-96"
className="max-h-[23rem]"
>
<PickerRadioGroup
value={currentTheme?.name}

View File

@@ -1,6 +1,7 @@
"use client"
import * as React from "react"
import Link from "next/link"
import {
ComputerTerminal01Icon,
Copy01Icon,
@@ -23,6 +24,8 @@ import {
} from "@/registry/new-york-v4/ui/dialog"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldTitle,
@@ -31,6 +34,7 @@ import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import {
Tabs,
TabsContent,
@@ -71,14 +75,15 @@ export function ToolbarControls() {
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 origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000"
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}&rtl=${params.rtl}`
const templateFlag = params.template ? ` --template ${params.template}` : ""
const rtlFlag = params.rtl ? " --rtl" : ""
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}`,
pnpm: `pnpm dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
npm: `npx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
yarn: `yarn dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
bun: `bunx --bun shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
}
}, [
params.base,
@@ -91,6 +96,7 @@ export function ToolbarControls() {
params.menuColor,
params.radius,
params.template,
params.rtl,
])
const command = commands[packageManager]
@@ -164,7 +170,7 @@ export function ToolbarControls() {
{selectedTemplate?.title} + shadcn/ui project.
</DialogDescription>
</DialogHeader>
<FieldGroup>
<FieldGroup className="gap-3">
<Field>
<FieldLabel htmlFor="template" className="sr-only">
Template
@@ -183,7 +189,7 @@ export function ToolbarControls() {
<FieldLabel
key={template.value}
htmlFor={template.value}
className="rounded-lg!"
className="has-data-[state=checked]:border-primary/10 rounded-lg!"
>
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!">
<RadioGroupItem
@@ -205,59 +211,81 @@ export function ToolbarControls() {
))}
</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>
<FieldLabel className="has-data-[state=checked]:border-primary/10 rounded-lg!">
<Field orientation="horizontal">
<FieldContent className="gap-1">
<FieldTitle>Enable RTL</FieldTitle>
<FieldDescription>
<a
href={`/docs/rtl/${params.template}`}
className="text-foreground underline"
target="_blank"
rel="noopener noreferrer"
>
View the RTL setup guide for {selectedTemplate?.title}.
</a>
</FieldDescription>
</FieldContent>
<Switch
checked={params.rtl}
onCheckedChange={(rtl) => setParams({ rtl })}
className="shadow-none"
/>
</Field>
</FieldLabel>
<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 group-data-[orientation=horizontal]/tabs:h-8 *:data-[slot=tabs-trigger]:h-7 *: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>
</div>
</TabsContent>
)
})}
</Tabs>
</TabsContent>
)
})}
</Tabs>
</FieldGroup>
<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"

View File

@@ -14,7 +14,7 @@ import {
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function V0Button({ className }: { className?: string }) {
const [params, setParams] = useDesignSystemSearchParams()
const [params] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const isMounted = useMounted()
@@ -32,7 +32,7 @@ export function V0Button({ className }: { className?: string }) {
size="sm"
variant={isMobile ? "default" : "outline"}
className={cn(
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px]",
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px] lg:w-8 xl:w-24",
className
)}
asChild
@@ -41,7 +41,8 @@ export function V0Button({ className }: { className?: string }) {
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" />
<span className="lg:hidden xl:block">Open in</span>
<Icons.v0 className="size-5" />
</a>
</Button>
</TooltipTrigger>

View File

@@ -4,7 +4,11 @@ import { ArrowLeftIcon } from "lucide-react"
import type { SearchParams } from "nuqs/server"
import { siteConfig } from "@/lib/config"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { MainNav } from "@/components/main-nav"
import { MobileNav } from "@/components/mobile-nav"
import { ModeSwitcher } from "@/components/mode-switcher"
import { SiteConfig } from "@/components/site-config"
import { BASES } from "@/registry/config"
@@ -64,6 +68,7 @@ export default async function CreatePage({
const params = await loadDesignSystemSearchParams(searchParams)
const base = BASES.find((b) => b.name === params.base) ?? BASES[0]
const pageTree = source.pageTree
const items = await getItemsForBase(base.name)
const filteredItems = items
@@ -73,6 +78,7 @@ export default async function CreatePage({
title: item.title,
type: item.type,
}))
.filter((item) => !/\d+$/.test(item.name))
return (
<div
@@ -81,39 +87,35 @@ export default async function CreatePage({
>
<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">
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:h-4!">
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:h-4!">
<MobileNav
tree={pageTree}
items={siteConfig.navItems}
className="flex lg:hidden"
/>
<Button
asChild
variant="outline"
size="sm"
className="rounded-lg shadow-none"
variant="ghost"
size="icon"
className="hidden size-8 lg:flex"
>
<Link href="/">
<ArrowLeftIcon />
Back
<Icons.logo className="size-5" />
<span className="sr-only">{siteConfig.name}</span>
</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>
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
</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">
<div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center justify-end gap-2 px-4.5 pb-4 sm:static sm:p-0 lg:ml-0">
<ItemPicker items={filteredItems} />
<div className="items-center gap-0 sm:hidden">
<RandomButton />
<ResetButton />
</div>
<Separator
orientation="vertical"
className="mr-2 hidden sm:flex xl:hidden"
/>
<Separator orientation="vertical" className="mr-2 flex" />
</div>
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end xl:ml-auto xl:w-1/3">
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end">
<SiteConfig className="3xl:flex hidden" />
<Separator orientation="vertical" className="3xl:flex hidden" />
<ModeSwitcher />

View File

@@ -45,18 +45,6 @@ export async function GET(request: NextRequest) {
}
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)
@@ -75,28 +63,23 @@ export async function GET(request: NextRequest) {
}
async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
const files: z.infer<typeof registryItemFileSchema>[] = []
const registryBase = buildRegistryBase(designSystemConfig)
// 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)
// Build all files in parallel.
const [globalsCss, layoutFile, componentFiles] = await Promise.all([
buildGlobalsCss(registryBase),
buildLayoutFile(designSystemConfig),
buildComponentFiles(designSystemConfig),
])
return registryItemSchema.parse({
name: designSystemConfig.item ?? "Item",
type: "registry:item",
files,
files: [globalsCss, layoutFile, ...componentFiles],
})
}
function buildGlobalsCss(designSystemConfig: DesignSystemConfig) {
const registryBase = buildRegistryBase(designSystemConfig)
function buildGlobalsCss(registryBase: RegistryItem) {
const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")

View File

@@ -19,6 +19,7 @@ export async function GET(request: NextRequest) {
menuColor: searchParams.get("menuColor"),
radius: searchParams.get("radius"),
template: searchParams.get("template"),
rtl: searchParams.get("rtl") === "true",
})
if (!result.success) {

View File

@@ -38,15 +38,15 @@ const jetbrainsMono = JetBrains_Mono({
variable: "--font-jetbrains-mono",
})
// const geistSans = Geist({
// subsets: ["latin"],
// variable: "--font-geist-sans",
// })
const geistSans = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
})
// const geistMono = Geist_Mono({
// subsets: ["latin"],
// variable: "--font-geist-mono",
// })
const geistMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-geist-mono",
})
const roboto = Roboto({
subsets: ["latin"],
@@ -74,12 +74,12 @@ const outfit = Outfit({
})
export const FONTS = [
// {
// name: "Geist Sans",
// value: "geist",
// font: geistSans,
// type: "sans",
// },
{
name: "Geist",
value: "geist",
font: geistSans,
type: "sans",
},
{
name: "Inter",
value: "inter",
@@ -134,18 +134,18 @@ export const FONTS = [
font: outfit,
type: "sans",
},
{
name: "Geist Mono",
value: "geist-mono",
font: geistMono,
type: "mono",
},
{
name: "JetBrains Mono",
value: "jetbrains-mono",
font: jetbrainsMono,
type: "mono",
},
// {
// name: "Geist Mono",
// value: "geist-mono",
// font: geistMono,
// type: "mono",
// },
] as const
export type Font = (typeof FONTS)[number]

View File

@@ -66,6 +66,7 @@ const designSystemSearchParams = {
"start",
"vite",
] as const).withDefault("next"),
rtl: parseAsBoolean.withDefault(false),
size: parseAsInteger.withDefault(100),
custom: parseAsBoolean.withDefault(false),
}

View File

@@ -77,7 +77,7 @@ export function ExampleForm() {
password: "",
},
validators: {
onChange: exampleFormSchema,
onBlur: exampleFormSchema,
},
onSubmit: async ({ value }) => {
setValues(value)

View File

@@ -1,3 +1,5 @@
import { TrashIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
@@ -6,6 +8,7 @@ import {
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/registry/new-york-v4/ui/alert-dialog"
@@ -13,23 +16,66 @@ import { Button } from "@/registry/new-york-v4/ui/button"
export function AlertDialogDemo() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Show Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<div className="flex flex-wrap gap-4">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Default</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">With Media</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogMedia>
<TrashIcon className="size-8" />
</AlertDialogMedia>
<AlertDialogTitle>Delete this item?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
item from your account.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive">Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Small Size</Button>
</AlertDialogTrigger>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogMedia>
<TrashIcon className="size-8" />
</AlertDialogMedia>
<AlertDialogTitle>Delete this item?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive">Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}

View File

@@ -1,90 +1,174 @@
import { PlusIcon } from "lucide-react"
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
export function AvatarDemo() {
return (
<div className="flex flex-row flex-wrap items-center gap-4">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className="size-12">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className="rounded-lg">
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<Avatar>
<div className="flex flex-col gap-6">
{/* Sizes. */}
<div className="flex flex-row flex-wrap items-center gap-4">
<Avatar size="sm">
<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>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
<Avatar size="lg">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</div>
<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" />
{/* Fallback. */}
<div className="flex flex-row flex-wrap items-center gap-4">
<Avatar size="sm">
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
<Avatar size="lg">
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</div>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 hover:space-x-1 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale *:data-[slot=avatar]:transition-all *:data-[slot=avatar]:duration-300 *:data-[slot=avatar]:ease-in-out">
{/* With badge. */}
<div className="flex flex-row flex-wrap items-center gap-4">
<Avatar size="sm">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
<AvatarBadge />
</Avatar>
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
<AvatarBadge />
</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 size="lg">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
<AvatarBadge>
<PlusIcon />
</AvatarBadge>
</Avatar>
</div>
{/* Avatar group. */}
<div className="flex flex-row flex-wrap items-center gap-4">
<AvatarGroup>
<Avatar size="sm">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar size="sm">
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>ML</AvatarFallback>
</Avatar>
<Avatar size="sm">
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</AvatarGroup>
<AvatarGroup>
<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>ML</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</AvatarGroup>
<AvatarGroup>
<Avatar size="lg">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar size="lg">
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>ML</AvatarFallback>
</Avatar>
<Avatar size="lg">
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</AvatarGroup>
</div>
{/* Avatar group with count. */}
<div className="flex flex-row flex-wrap items-center gap-4">
<AvatarGroup>
<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>ML</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<AvatarGroupCount>+3</AvatarGroupCount>
</AvatarGroup>
<AvatarGroup>
<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>ML</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<AvatarGroupCount>
<PlusIcon />
</AvatarGroupCount>
</AvatarGroup>
</div>
</div>
)
}

View File

@@ -10,6 +10,10 @@ export function BadgeDemo() {
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="ghost">Ghost</Badge>
<Badge variant="link">Link</Badge>
</div>
<div className="flex w-full flex-wrap gap-2">
<Badge variant="outline">
<CheckIcon />
Badge
@@ -55,6 +59,16 @@ export function BadgeDemo() {
Link <ArrowRightIcon />
</a>
</Badge>
<Badge asChild variant="ghost">
<a href="#">
Link <ArrowRightIcon />
</a>
</Badge>
<Badge asChild variant="link">
<a href="#">
Link <ArrowRightIcon />
</a>
</Badge>
</div>
</div>
)

View File

@@ -1,4 +1,4 @@
import { ArrowRightIcon, Loader2Icon, SendIcon } from "lucide-react"
import { ArrowRightIcon, Loader2Icon, PlusIcon, SendIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
@@ -6,22 +6,25 @@ export function ButtonDemo() {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-wrap items-center gap-2 md:flex-row">
<Button>Button</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="link">Link</Button>
<Button variant="outline">
<Button size="xs">Extra Small</Button>
<Button variant="outline" size="xs">
Outline
</Button>
<Button variant="ghost" size="xs">
Ghost
</Button>
<Button variant="destructive" size="xs">
Destructive
</Button>
<Button variant="secondary" size="xs">
Secondary
</Button>
<Button variant="link" size="xs">
Link
</Button>
<Button variant="outline" size="xs">
<SendIcon /> Send
</Button>
<Button variant="outline">
Learn More <ArrowRightIcon />
</Button>
<Button disabled variant="outline">
<Loader2Icon className="animate-spin" />
Please wait
</Button>
</div>
<div className="flex flex-wrap items-center gap-2 md:flex-row">
<Button size="sm">Small</Button>
@@ -43,10 +46,21 @@ export function ButtonDemo() {
<Button variant="outline" size="sm">
<SendIcon /> Send
</Button>
<Button variant="outline" size="sm">
</div>
<div className="flex flex-wrap items-center gap-2 md:flex-row">
<Button>Button</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="link">Link</Button>
<Button variant="outline">
<SendIcon /> Send
</Button>
<Button variant="outline">
Learn More <ArrowRightIcon />
</Button>
<Button disabled size="sm" variant="outline">
<Button disabled variant="outline">
<Loader2Icon className="animate-spin" />
Please wait
</Button>
@@ -71,12 +85,19 @@ export function ButtonDemo() {
<Button variant="outline" size="lg">
<SendIcon /> Send
</Button>
<Button variant="outline" size="lg">
Learn More <ArrowRightIcon />
</div>
<div className="flex flex-wrap items-center gap-2 md:flex-row">
<Button size="icon-xs" variant="outline">
<PlusIcon />
</Button>
<Button disabled size="lg" variant="outline">
<Loader2Icon className="animate-spin" />
Please wait
<Button size="icon-sm" variant="outline">
<PlusIcon />
</Button>
<Button size="icon" variant="outline">
<PlusIcon />
</Button>
<Button size="icon-lg" variant="outline">
<PlusIcon />
</Button>
</div>
</div>

View File

@@ -1,405 +1,183 @@
"use client"
import * as React from "react"
import {
CheckIcon,
ChevronDownIcon,
ChevronsUpDown,
PlusCircleIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/registry/new-york-v4/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
} from "@/registry/new-york-v4/ui/combobox"
const frameworks = [
{
value: "next.js",
label: "Next.js",
},
{
value: "sveltekit",
label: "SvelteKit",
},
{
value: "nuxt.js",
label: "Nuxt.js",
},
{
value: "remix",
label: "Remix",
},
{
value: "astro",
label: "Astro",
},
]
type Framework = (typeof frameworks)[number]
const users = [
{
id: "1",
username: "shadcn",
},
{
id: "2",
username: "maxleiter",
},
{
id: "3",
username: "evilrabbit",
},
"Next.js",
"SvelteKit",
"Nuxt.js",
"Remix",
"Astro",
] as const
type User = (typeof users)[number]
const timezones = [
{
label: "Americas",
timezones: [
{ value: "America/New_York", label: "(GMT-5) New York" },
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" },
],
value: "Americas",
items: ["(GMT-5) New York", "(GMT-8) Los Angeles", "(GMT-6) Chicago"],
},
{
label: "Europe",
timezones: [
{ value: "Europe/London", label: "(GMT+0) London" },
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" },
],
value: "Europe",
items: ["(GMT+0) London", "(GMT+1) Paris", "(GMT+1) Berlin"],
},
{
label: "Asia/Pacific",
timezones: [
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" },
],
value: "Asia/Pacific",
items: ["(GMT+9) Tokyo", "(GMT+8) Shanghai", "(GMT+8) Singapore"],
},
] as const
type Timezone = (typeof timezones)[number]
const countries = [
{ code: "", value: "", label: "Select country" },
{ code: "us", value: "united-states", label: "United States" },
{ code: "ca", value: "canada", label: "Canada" },
{ code: "gb", value: "united-kingdom", label: "United Kingdom" },
{ code: "de", value: "germany", label: "Germany" },
{ code: "fr", value: "france", label: "France" },
{ code: "jp", value: "japan", label: "Japan" },
]
export function ComboboxDemo() {
return (
<div className="flex w-full flex-wrap items-start gap-4">
<FrameworkCombobox frameworks={[...frameworks]} />
<UserCombobox users={[...users]} selectedUserId={users[0].id} />
<TimezoneCombobox
timezones={[...timezones]}
selectedTimezone={timezones[0].timezones[0]}
/>
<ComboboxWithCheckbox frameworks={[...frameworks]} />
<div className="flex w-full flex-col gap-6">
{/* Basic combobox. */}
<div className="flex flex-wrap items-start gap-4">
<Combobox items={frameworks}>
<ComboboxInput placeholder="Select a framework" />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{/* With clear button. */}
<div className="flex flex-wrap items-start gap-4">
<Combobox items={frameworks} defaultValue={frameworks[0]}>
<ComboboxInput placeholder="Select a framework" showClear />
<ComboboxContent>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{/* With groups. */}
<div className="flex flex-wrap items-start gap-4">
<Combobox items={timezones}>
<ComboboxInput placeholder="Select a timezone" />
<ComboboxContent>
<ComboboxEmpty>No timezones found.</ComboboxEmpty>
<ComboboxList>
{(group) => (
<ComboboxGroup key={group.value} items={group.items}>
<ComboboxLabel>{group.value}</ComboboxLabel>
<ComboboxCollection>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{/* With trigger button. */}
<div className="flex flex-wrap items-start gap-4">
<Combobox items={countries} defaultValue={countries[0]}>
<ComboboxTrigger
render={
<Button
variant="outline"
className="w-64 justify-between font-normal"
/>
}
>
<ComboboxValue />
</ComboboxTrigger>
<ComboboxContent>
<ComboboxInput showTrigger={false} placeholder="Search" />
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.code} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{/* Multiple selection with chips. */}
<ComboboxMultiple />
</div>
)
}
function FrameworkCombobox({ frameworks }: { frameworks: Framework[] }) {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState("")
function ComboboxMultiple() {
const anchor = useComboboxAnchor()
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between md:max-w-[200px]"
>
{value
? frameworks.find((framework) => framework.value === value)?.label
: "Select framework..."}
<ChevronsUpDown className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-(--radix-popover-trigger-width) p-0">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue)
setOpen(false)
}}
>
{framework.label}
<CheckIcon
className={cn(
"ml-auto",
value === framework.value ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
function UserCombobox({
users,
selectedUserId,
}: {
users: User[]
selectedUserId: string
}) {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState(selectedUserId)
const selectedUser = React.useMemo(
() => users.find((user) => user.id === value),
[value, users]
)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between px-2 md:max-w-[200px]"
>
{selectedUser ? (
<div className="flex items-center gap-2">
<Avatar className="size-5">
<AvatarImage
src={`https://github.com/${selectedUser.username}.png`}
/>
<AvatarFallback>{selectedUser.username[0]}</AvatarFallback>
</Avatar>
{selectedUser.username}
</div>
) : (
"Select user..."
)}
<ChevronsUpDown className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-(--radix-popover-trigger-width) p-0">
<Command>
<CommandInput placeholder="Search user..." />
<CommandList>
<CommandEmpty>No user found.</CommandEmpty>
<CommandGroup>
{users.map((user) => (
<CommandItem
key={user.id}
value={user.id}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue)
setOpen(false)
}}
>
<Avatar className="size-5">
<AvatarImage
src={`https://github.com/${user.username}.png`}
/>
<AvatarFallback>{user.username[0]}</AvatarFallback>
</Avatar>
{user.username}
<CheckIcon
className={cn(
"ml-auto",
value === user.id ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<CommandItem>
<PlusCircleIcon />
Create user
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
function TimezoneCombobox({
timezones,
selectedTimezone,
}: {
timezones: Timezone[]
selectedTimezone: Timezone["timezones"][number]
}) {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState(selectedTimezone.value)
const selectedGroup = React.useMemo(
() =>
timezones.find((group) =>
group.timezones.find((tz) => tz.value === value)
),
[value, timezones]
)
const selectedTimezoneLabel = React.useMemo(
() => selectedGroup?.timezones.find((tz) => tz.value === value)?.label,
[value, selectedGroup]
)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-12 w-full justify-between px-2.5 md:max-w-[200px]"
>
{selectedTimezone ? (
<div className="flex flex-col items-start gap-0.5">
<span className="text-muted-foreground text-xs font-normal">
{selectedGroup?.label}
</span>
<span>{selectedTimezoneLabel}</span>
</div>
) : (
"Select timezone"
)}
<ChevronDownIcon className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput placeholder="Search timezone..." />
<CommandList className="scroll-pb-12">
<CommandEmpty>No timezone found.</CommandEmpty>
{timezones.map((region) => (
<CommandGroup key={region.label} heading={region.label}>
{region.timezones.map((timezone) => (
<CommandItem
key={timezone.value}
value={timezone.value}
onSelect={(currentValue) => {
setValue(
currentValue as Timezone["timezones"][number]["value"]
)
setOpen(false)
}}
>
{timezone.label}
<CheckIcon
className="ml-auto opacity-0 data-[selected=true]:opacity-100"
data-selected={value === timezone.value}
/>
</CommandItem>
<div className="flex flex-wrap items-start gap-4">
<Combobox
multiple
autoHighlight
items={frameworks}
defaultValue={[frameworks[0]]}
>
<ComboboxChips ref={anchor}>
<ComboboxValue>
{(values) => (
<React.Fragment>
{values.map((value: string) => (
<ComboboxChip key={value}>{value}</ComboboxChip>
))}
</CommandGroup>
))}
<CommandSeparator className="sticky bottom-10" />
<CommandGroup className="bg-popover sticky bottom-0">
<CommandItem>
<PlusCircleIcon />
Create timezone
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
function ComboboxWithCheckbox({ frameworks }: { frameworks: Framework[] }) {
const [open, setOpen] = React.useState(false)
const [selectedFrameworks, setSelectedFrameworks] = React.useState<
Framework[]
>([])
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-fit min-w-[280px] justify-between"
>
{selectedFrameworks.length > 0
? selectedFrameworks.map((framework) => framework.label).join(", ")
: "Select frameworks (multi-select)..."}
<ChevronsUpDown className="text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setSelectedFrameworks(
selectedFrameworks.some((f) => f.value === currentValue)
? selectedFrameworks.filter(
(f) => f.value !== currentValue
)
: [...selectedFrameworks, framework]
)
}}
>
<div
className="border-input data-[selected=true]:border-primary data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground pointer-events-none size-4 shrink-0 rounded-[4px] border transition-all select-none *:[svg]:opacity-0 data-[selected=true]:*:[svg]:opacity-100"
data-selected={selectedFrameworks.some(
(f) => f.value === framework.value
)}
>
<CheckIcon className="size-3.5 text-current" />
</div>
{framework.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<ComboboxChipsInput placeholder="Add framework..." />
</React.Fragment>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent anchor={anchor}>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
)
}

View File

@@ -4,59 +4,64 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
export function PopoverDemo() {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open popover</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="start">
<div className="grid gap-4">
<div className="grid gap-1.5">
<h4 className="leading-none font-medium">Dimensions</h4>
<p className="text-muted-foreground text-sm">
Set the dimensions for the layer.
</p>
</div>
<div className="grid gap-2">
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="width">Width</Label>
<Input
id="width"
defaultValue="100%"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxWidth">Max. width</Label>
<Input
id="maxWidth"
defaultValue="300px"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="height">Height</Label>
<Input
id="height"
defaultValue="25px"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxHeight">Max. height</Label>
<Input
id="maxHeight"
defaultValue="none"
className="col-span-2 h-8"
/>
<div className="flex gap-4">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open popover</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="start">
<div className="grid gap-4">
<PopoverHeader>
<PopoverTitle>Dimensions</PopoverTitle>
<PopoverDescription>
Set the dimensions for the layer.
</PopoverDescription>
</PopoverHeader>
<div className="grid gap-2">
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="width">Width</Label>
<Input
id="width"
defaultValue="100%"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxWidth">Max. width</Label>
<Input
id="maxWidth"
defaultValue="300px"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="height">Height</Label>
<Input
id="height"
defaultValue="25px"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxHeight">Max. height</Label>
<Input
id="maxHeight"
defaultValue="none"
className="col-span-2 h-8"
/>
</div>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
</div>
)
}

View File

@@ -8,24 +8,24 @@ export function ResizableDemo() {
return (
<div className="flex w-full flex-col gap-6">
<ResizablePanelGroup
direction="horizontal"
orientation="horizontal"
className="max-w-md rounded-lg border md:min-w-[450px]"
>
<ResizablePanel defaultSize={50}>
<ResizablePanel defaultSize="50%">
<div className="flex h-[200px] items-center justify-center p-6">
<span className="font-semibold">One</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={25}>
<ResizablePanel defaultSize="50%">
<ResizablePanelGroup orientation="vertical">
<ResizablePanel defaultSize="25%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Two</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={75}>
<ResizablePanel defaultSize="75%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Three</span>
</div>
@@ -34,32 +34,32 @@ export function ResizableDemo() {
</ResizablePanel>
</ResizablePanelGroup>
<ResizablePanelGroup
direction="horizontal"
orientation="horizontal"
className="min-h-[200px] max-w-md rounded-lg border md:min-w-[450px]"
>
<ResizablePanel defaultSize={25}>
<ResizablePanel defaultSize="25%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Sidebar</span>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={75}>
<ResizablePanel defaultSize="75%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Content</span>
</div>
</ResizablePanel>
</ResizablePanelGroup>
<ResizablePanelGroup
direction="vertical"
orientation="vertical"
className="min-h-[200px] max-w-md rounded-lg border md:min-w-[450px]"
>
<ResizablePanel defaultSize={25}>
<ResizablePanel defaultSize="25%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Header</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={75}>
<ResizablePanel defaultSize="75%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Content</span>
</div>

View File

@@ -47,6 +47,27 @@ export function SheetDemo() {
</SheetFooter>
</SheetContent>
</Sheet>
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">No Close Button</Button>
</SheetTrigger>
<SheetContent showCloseButton={false}>
<SheetHeader>
<SheetTitle>Custom Close</SheetTitle>
<SheetDescription>
This sheet has no default close button. Use the footer buttons
instead.
</SheetDescription>
</SheetHeader>
<div className="flex-1 px-4" />
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Cancel</Button>
</SheetClose>
<Button type="submit">Save</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<div className="flex gap-2">
{SHEET_SIDES.map((side) => (
<Sheet key={side}>

View File

@@ -4,6 +4,17 @@ import { Switch } from "@/registry/new-york-v4/ui/switch"
export function SwitchDemo() {
return (
<div className="flex flex-col gap-6">
{/* Sizes. */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Switch id="switch-demo-sm" size="sm" />
<Label htmlFor="switch-demo-sm">Small</Label>
</div>
<div className="flex items-center gap-2">
<Switch id="switch-demo-default" />
<Label htmlFor="switch-demo-default">Default</Label>
</div>
</div>
<div className="flex items-center gap-2">
<Switch id="switch-demo-airplane-mode" />
<Label htmlFor="switch-demo-airplane-mode">Airplane Mode</Label>

View File

@@ -101,6 +101,45 @@ export function TabsDemo() {
</TabsTrigger>
</TabsList>
</Tabs>
{/* Line variant. */}
<Tabs defaultValue="preview">
<TabsList variant="line">
<TabsTrigger value="preview">
<AppWindowIcon />
Preview
</TabsTrigger>
<TabsTrigger value="code">
<CodeIcon />
Code
</TabsTrigger>
</TabsList>
</Tabs>
{/* Vertical orientation. */}
<Tabs defaultValue="preview" orientation="vertical">
<TabsList>
<TabsTrigger value="preview">
<AppWindowIcon />
Preview
</TabsTrigger>
<TabsTrigger value="code">
<CodeIcon />
Code
</TabsTrigger>
</TabsList>
</Tabs>
{/* Vertical orientation with line variant. */}
<Tabs defaultValue="preview" orientation="vertical">
<TabsList variant="line">
<TabsTrigger value="preview">
<AppWindowIcon />
Preview
</TabsTrigger>
<TabsTrigger value="code">
<CodeIcon />
Code
</TabsTrigger>
</TabsList>
</Tabs>
</div>
)
}

View File

@@ -4,7 +4,11 @@ import { type Metadata } from "next"
import { notFound } from "next/navigation"
import { siteConfig } from "@/lib/config"
import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
import {
getDemoItem,
getRegistryComponent,
getRegistryItem,
} from "@/lib/registry"
import { absoluteUrl } from "@/lib/utils"
import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles"
@@ -18,7 +22,12 @@ export const dynamicParams = false
const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => {
return await getRegistryItem(name, styleName)
// Try registry item first, then fallback to demo item (for examples).
const item = await getRegistryItem(name, styleName)
if (item) {
return item
}
return await getDemoItem(name, styleName)
}
)
@@ -75,9 +84,54 @@ export async function generateMetadata({
export async function generateStaticParams() {
const { Index } = await import("@/registry/__index__")
// const { Index: BasesIndex } = await import("@/registry/bases/__index__")
const { ExamplesIndex } = await import("@/examples/__index__")
const params: Array<{ style: string; name: string }> = []
for (const style of legacyStyles) {
// Check if this is a base-prefixed style (e.g., base-nova, radix-nova).
const baseMatch = style.name.match(/^(base|radix)-/)
if (baseMatch) {
const baseName = baseMatch[1]
// Add examples from ExamplesIndex.
const examples = ExamplesIndex[baseName]
if (examples) {
for (const exampleName of Object.keys(examples)) {
if (exampleName.startsWith("sidebar-")) {
params.push({
style: style.name,
name: exampleName,
})
}
}
}
// // Add UI components from BasesIndex.
// const baseIndex = BasesIndex[baseName]
// if (baseIndex) {
// for (const itemName in baseIndex) {
// const item = baseIndex[itemName]
// if (
// [
// "registry:block",
// "registry:component",
// "registry:example",
// "registry:internal",
// ].includes(item.type)
// ) {
// params.push({
// style: style.name,
// name: item.name,
// })
// }
// }
// }
continue
}
// Handle legacy styles (e.g., new-york-v4).
if (!Index[style.name]) {
continue
}

View File

@@ -9,7 +9,9 @@ import { ActiveThemeProvider } from "@/components/active-theme"
import { Analytics } from "@/components/analytics"
import { TailwindIndicator } from "@/components/tailwind-indicator"
import { ThemeProvider } from "@/components/theme-provider"
import { TooltipProvider as BaseTooltipProvider } from "@/registry/bases/base/ui/tooltip"
import { Toaster } from "@/registry/bases/radix/ui/sonner"
import { TooltipProvider as RadixTooltipProvider } from "@/registry/bases/radix/ui/tooltip"
import "@/styles/globals.css"
@@ -57,6 +59,11 @@ export const metadata: Metadata = {
apple: "/apple-touch-icon.png",
},
manifest: `${siteConfig.url}/site.webmanifest`,
alternates: {
types: {
"application/rss+xml": `${siteConfig.url}/rss.xml`,
},
},
}
export default function RootLayout({
@@ -92,8 +99,12 @@ export default function RootLayout({
<LayoutProvider>
<ActiveThemeProvider>
<NuqsAdapter>
{children}
<Toaster position="top-center" />
<BaseTooltipProvider delay={0}>
<RadixTooltipProvider delayDuration={0}>
{children}
<Toaster position="top-center" />
</RadixTooltipProvider>
</BaseTooltipProvider>
</NuqsAdapter>
<TailwindIndicator />
<Analytics />

View File

@@ -0,0 +1,44 @@
import { NextResponse } from "next/server"
import { getChangelogPages, type ChangelogPageData } from "@/lib/changelog"
import { siteConfig } from "@/lib/config"
export const revalidate = false
export async function GET() {
const pages = getChangelogPages()
const items = pages
.map((page) => {
const data = page.data as ChangelogPageData
const date = page.date?.toUTCString() ?? new Date().toUTCString()
const link = `${siteConfig.url}/docs/${page.slugs.join("/")}`
return ` <item>
<title><![CDATA[${data.title}]]></title>
<link>${link}</link>
<guid>${link}</guid>
<description><![CDATA[${data.description || ""}]]></description>
<pubDate>${date}</pubDate>
</item>`
})
.join("\n")
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${siteConfig.name} Changelog</title>
<link>${siteConfig.url}</link>
<description>${siteConfig.description}</description>
<language>en-us</language>
<atom:link href="${siteConfig.url}/rss.xml" rel="self" type="application/rss+xml"/>
${items}
</channel>
</rss>`
return new NextResponse(xml, {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
},
})
}

View File

@@ -3,12 +3,26 @@ import { ArrowRightIcon } from "lucide-react"
import { Badge } from "@/registry/new-york-v4/ui/badge"
function BaseUILogo() {
return (
<svg width="17" height="24" viewBox="0 0 17 24" className="size-3">
<path
fill="currentColor"
d="M9.5001 7.01537C9.2245 6.99837 9 7.22385 9 7.49999V23C13.4183 23 17 19.4183 17 15C17 10.7497 13.6854 7.27351 9.5001 7.01537Z"
/>
<path
fill="currentColor"
d="M8 9.8V12V23C3.58172 23 0 19.0601 0 14.2V12V1C4.41828 1 8 4.93989 8 9.8Z"
/>
</svg>
)
}
export function Announcement() {
return (
<Badge asChild variant="secondary" className="bg-transparent">
<Link href="/docs/changelog">
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
npx shadcn create <ArrowRightIcon />
<Badge asChild variant="secondary" className="bg-muted">
<Link href="/docs/changelog/2026-01-rtl">
RTL Support <ArrowRightIcon />
</Link>
</Badge>
)

View File

@@ -16,7 +16,7 @@ import {
Tablet,
Terminal,
} from "lucide-react"
import { type ImperativePanelHandle } from "react-resizable-panels"
import { type PanelImperativeHandle } from "react-resizable-panels"
import {
type registryItemFileSchema,
type registryItemSchema,
@@ -68,7 +68,7 @@ type BlockViewerContext = {
setView: (view: "code" | "preview") => void
activeFile: string | null
setActiveFile: (file: string) => void
resizablePanelRef: React.RefObject<ImperativePanelHandle | null> | null
resizablePanelRef: React.RefObject<PanelImperativeHandle | null> | null
tree: ReturnType<typeof createFileTreeForRegistryItemFiles> | null
highlightedFiles:
| (z.infer<typeof registryItemFileSchema> & {
@@ -101,7 +101,7 @@ function BlockViewerProvider({
const [activeFile, setActiveFile] = React.useState<
BlockViewerContext["activeFile"]
>(highlightedFiles?.[0].target ?? null)
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
const resizablePanelRef = React.useRef<PanelImperativeHandle>(null)
const [iframeKey, setIframeKey] = React.useState(0)
return (
@@ -154,12 +154,12 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
value={view}
onValueChange={(value) => setView(value as "preview" | "code")}
>
<TabsList className="grid h-8 grid-cols-2 items-center rounded-md p-1 *:data-[slot=tabs-trigger]:h-6 *:data-[slot=tabs-trigger]:rounded-sm *:data-[slot=tabs-trigger]:px-2 *:data-[slot=tabs-trigger]:text-xs">
<TabsList className="grid h-8! grid-cols-2 items-center rounded-lg p-1 *:data-[slot=tabs-trigger]:h-6 *:data-[slot=tabs-trigger]:rounded-sm *:data-[slot=tabs-trigger]:px-2 *:data-[slot=tabs-trigger]:text-xs">
<TabsTrigger value="preview">Preview</TabsTrigger>
<TabsTrigger value="code">Code</TabsTrigger>
</TabsList>
</Tabs>
<Separator orientation="vertical" className="mx-2 !h-4" />
<Separator orientation="vertical" className="mx-2 h-4!" />
<a
href={`#${item.name}`}
className="flex-1 text-center text-sm font-medium underline-offset-2 hover:underline md:flex-auto md:text-left"
@@ -167,7 +167,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
{item.description?.replace(/\.$/, "")}
</a>
<div className="ml-auto flex items-center gap-2">
<div className="h-8 items-center gap-1.5 rounded-md border p-1 shadow-none">
<div className="h-8 items-center gap-1.5 rounded-md border p-[3px] shadow-none">
<ToggleGroup
type="single"
defaultValue="100"
@@ -177,7 +177,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
resizablePanelRef.current.resize(parseInt(value))
}
}}
className="gap-1 *:data-[slot=toggle-group-item]:!size-6 *:data-[slot=toggle-group-item]:!rounded-sm"
className="gap-1 *:data-[slot=toggle-group-item]:size-6! *:data-[slot=toggle-group-item]:rounded-sm!"
>
<ToggleGroupItem value="100" title="Desktop">
<Monitor />
@@ -188,7 +188,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
<ToggleGroupItem value="30" title="Mobile">
<Smartphone />
</ToggleGroupItem>
<Separator orientation="vertical" className="!h-4" />
<Separator orientation="vertical" className="h-4!" />
<Button
size="icon"
variant="ghost"
@@ -201,7 +201,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
<Fullscreen />
</Link>
</Button>
<Separator orientation="vertical" className="!h-4" />
<Separator orientation="vertical" className="h-4!" />
<Button
size="icon"
variant="ghost"
@@ -218,7 +218,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
</Button>
</ToggleGroup>
</div>
<Separator orientation="vertical" className="mx-1 !h-4" />
<Separator orientation="vertical" className="mx-1 h-4!" />
<Button
variant="outline"
className="w-fit gap-1 px-2 shadow-none"
@@ -230,7 +230,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
{isCopied ? <Check /> : <Terminal />}
<span>npx shadcn add {item.name}</span>
</Button>
<Separator orientation="vertical" className="mx-1 !h-4" />
<Separator orientation="vertical" className="mx-1 h-4!" />
<OpenInV0Button name={item.name} />
</div>
</div>
@@ -268,19 +268,19 @@ function BlockViewerView({ styleName }: { styleName: Style["name"] }) {
<div className="relative grid w-full gap-4">
<div className="absolute inset-0 right-4 [background-image:radial-gradient(#d4d4d4_1px,transparent_1px)] [background-size:20px_20px] dark:[background-image:radial-gradient(#404040_1px,transparent_1px)]"></div>
<ResizablePanelGroup
direction="horizontal"
orientation="horizontal"
className="after:bg-surface/50 relative z-10 after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-xl"
>
<ResizablePanel
ref={resizablePanelRef}
panelRef={resizablePanelRef}
className="bg-background relative aspect-[4/2.5] overflow-hidden rounded-lg border md:aspect-auto md:rounded-xl"
defaultSize={100}
minSize={30}
defaultSize="100%"
minSize="30%"
>
<BlockViewerIframe styleName={styleName} />
</ResizablePanel>
<ResizableHandle className="after:bg-border relative hidden w-3 bg-transparent p-0 after:absolute after:top-1/2 after:right-0 after:h-8 after:w-[6px] after:translate-x-[-1px] after:-translate-y-1/2 after:rounded-full after:transition-all after:hover:h-10 md:block" />
<ResizablePanel defaultSize={0} minSize={0} />
<ResizablePanel defaultSize="0%" minSize="0%" />
</ResizablePanelGroup>
</div>
</div>

View File

@@ -20,7 +20,7 @@ export function Callout({
<Alert
data-variant={variant}
className={cn(
"bg-background text-foreground mt-6 w-auto border md:-mx-1",
"bg-surface text-surface-foreground border-surface mt-6 w-auto rounded-xl md:-mx-1 **:[code]:border",
className
)}
{...props}

View File

@@ -35,7 +35,7 @@ export function ChartDisplay({
<div className="bg-background relative z-10 overflow-hidden rounded-xl">
<ChartIframe
src={`/view/${style}/${chart.name}`}
height={430}
height={460}
title={chart.name}
/>
</div>

View File

@@ -88,7 +88,7 @@ export function CodeBlockCommand({
<TabsTrigger
key={key}
value={key}
className="data-[state=active]:bg-accent data-[state=active]:border-input h-7 border border-transparent pt-0.5 data-[state=active]:shadow-none"
className="data-[state=active]:bg-background! data-[state=active]:border-input h-7 border border-transparent pt-0.5 shadow-none!"
>
{key}
</TabsTrigger>
@@ -113,23 +113,16 @@ export function CodeBlockCommand({
})}
</div>
</Tabs>
<Tooltip>
<TooltipTrigger asChild>
<Button
data-slot="copy-button"
size="icon"
variant="ghost"
className="absolute top-2 right-2 z-10 size-7 opacity-70 hover:opacity-100 focus-visible:opacity-100"
onClick={copyCommand}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied" : "Copy to Clipboard"}
</TooltipContent>
</Tooltip>
<Button
data-slot="copy-button"
size="icon"
variant="ghost"
className="absolute top-2 right-2 z-10 size-7 opacity-70 hover:opacity-100 focus-visible:opacity-100"
onClick={copyCommand}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</div>
)
}

View File

@@ -39,7 +39,7 @@ export function CodeCollapsibleWrapper({
</CollapsibleTrigger>
<CollapsibleContent
forceMount
className="relative mt-6 overflow-hidden data-[state=closed]:max-h-64 [&>figure]:mt-0 [&>figure]:md:!mx-0"
className="relative mt-6 overflow-hidden data-[state=closed]:max-h-64 data-[state=closed]:[content-visibility:auto] [&>figure]:mt-0 [&>figure]:md:!mx-0"
>
{children}
</CollapsibleContent>

View File

@@ -18,7 +18,7 @@ export function CodeTabs({ children }: React.ComponentProps<typeof Tabs>) {
onValueChange={(value) =>
setConfig({ ...config, installationType: value as "cli" | "manual" })
}
className="relative mt-6 w-full"
className="relative mt-6 w-full *:data-[slot=tabs-list]:gap-6"
>
{children}
</Tabs>

View File

@@ -1,15 +1,16 @@
"use client"
import * as React from "react"
import { useRouter } from "next/navigation"
import { type DialogProps } from "@radix-ui/react-dialog"
import { usePathname, useRouter } from "next/navigation"
import { IconArrowRight } from "@tabler/icons-react"
import { useDocsSearch } from "fumadocs-core/search/client"
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { type Color, type ColorPalette } from "@/lib/colors"
import { trackEvent } from "@/lib/events"
import { showMcpDocs } from "@/lib/flags"
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
import { type source } from "@/lib/source"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
@@ -26,13 +27,13 @@ import {
} from "@/registry/new-york-v4/ui/command"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "@/registry/new-york-v4/ui/dialog"
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
@@ -42,15 +43,18 @@ export function CommandMenu({
blocks,
navItems,
...props
}: DialogProps & {
}: React.ComponentProps<typeof Dialog> & {
tree: typeof source.pageTree
colors: ColorPalette[]
blocks?: { name: string; description: string; categories: string[] }[]
navItems?: { href: string; label: string }[]
}) {
const router = useRouter()
const pathname = usePathname()
const [config] = useConfig()
const currentBase = getCurrentBase(pathname)
const [open, setOpen] = React.useState(false)
const [renderDelayedGroups, setRenderDelayedGroups] = React.useState(false)
const [selectedType, setSelectedType] = React.useState<
"color" | "page" | "component" | "block" | null
>(null)
@@ -90,14 +94,30 @@ export function CommandMenu({
// Set new timeout to debounce both search and tracking.
searchTimeoutRef.current = setTimeout(() => {
setSearch(value)
trackSearchQuery(value)
React.startTransition(() => {
setSearch(value)
trackSearchQuery(value)
})
}, 500)
},
[setSearch, trackSearchQuery]
)
// Cleanup timeout on unmount.
React.useEffect(() => {
if (open) {
const frame = requestAnimationFrame(() => {
setRenderDelayedGroups(true)
})
return () => {
cancelAnimationFrame(frame)
}
}
setRenderDelayedGroups(false)
}, [open])
React.useEffect(() => {
return () => {
if (searchTimeoutRef.current) {
@@ -106,6 +126,17 @@ export function CommandMenu({
}
}, [])
const commandFilter = React.useCallback(
(value: string, searchValue: string, keywords?: string[]) => {
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(searchValue.toLowerCase())) {
return 1
}
return 0
},
[]
)
const handlePageHighlight = React.useCallback(
(isComponent: boolean, item: { url: string; name?: React.ReactNode }) => {
if (isComponent) {
@@ -138,10 +169,175 @@ export function CommandMenu({
[setSelectedType, setCopyPayload, packageManager]
)
const runCommand = React.useCallback((command: () => unknown) => {
setOpen(false)
command()
}, [])
const runCommand = React.useCallback(
(command: () => unknown) => {
setOpen(false)
command()
},
[setOpen]
)
const navItemsSection = React.useMemo(() => {
if (!navItems || navItems.length === 0) {
return null
}
return (
<CommandGroup
heading="Pages"
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{navItems.map((item) => (
<CommandMenuItem
key={item.href}
value={`Navigation ${item.label}`}
keywords={["nav", "navigation", item.label.toLowerCase()]}
onHighlight={() => {
setSelectedType("page")
setCopyPayload("")
}}
onSelect={() => {
runCommand(() => router.push(item.href))
}}
>
<IconArrowRight />
{item.label}
</CommandMenuItem>
))}
</CommandGroup>
)
}, [navItems, runCommand, router])
const pageGroupsSection = React.useMemo(() => {
return tree.children.map((group) => {
if (group.type !== "folder") {
return null
}
const pages = getPagesFromFolder(group, currentBase).filter((item) => {
if (!showMcpDocs && item.url.includes("/mcp")) {
return false
}
return true
})
if (pages.length === 0) {
return null
}
return (
<CommandGroup
key={group.$id}
heading={group.name}
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{pages.map((item) => {
const isComponent = item.url.includes("/components/")
return (
<CommandMenuItem
key={item.url}
value={
item.name?.toString() ? `${group.name} ${item.name}` : ""
}
keywords={isComponent ? ["component"] : undefined}
onHighlight={() => handlePageHighlight(isComponent, item)}
onSelect={() => {
runCommand(() => router.push(item.url))
}}
>
{isComponent ? (
<div className="border-muted-foreground aspect-square size-4 rounded-full border border-dashed" />
) : (
<IconArrowRight />
)}
{item.name}
</CommandMenuItem>
)
})}
</CommandGroup>
)
})
}, [tree.children, currentBase, handlePageHighlight, runCommand, router])
const colorGroupsSection = React.useMemo(() => {
return colors.map((colorPalette) => (
<CommandGroup
key={colorPalette.name}
heading={
colorPalette.name.charAt(0).toUpperCase() + colorPalette.name.slice(1)
}
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{colorPalette.colors.map((color) => (
<CommandMenuItem
key={color.hex}
value={color.className}
keywords={["color", color.name, color.className]}
onHighlight={() => handleColorHighlight(color)}
onSelect={() => {
runCommand(() =>
copyToClipboardWithMeta(color.oklch, {
name: "copy_color",
properties: { color: color.oklch },
})
)
}}
>
<div
className="border-ghost aspect-square size-4 rounded-sm bg-(--color) after:rounded-sm"
style={{ "--color": color.oklch } as React.CSSProperties}
/>
{color.className}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{color.oklch}
</span>
</CommandMenuItem>
))}
</CommandGroup>
))
}, [colors, handleColorHighlight, runCommand])
const blocksSection = React.useMemo(() => {
if (!blocks || blocks.length === 0) {
return null
}
return (
<CommandGroup
heading="Blocks"
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{blocks.map((block) => (
<CommandMenuItem
key={block.name}
value={block.name}
onHighlight={() => {
handleBlockHighlight(block)
}}
keywords={[
"block",
block.name,
block.description,
...block.categories,
]}
onSelect={() => {
runCommand(() =>
router.push(`/blocks/${block.categories[0]}#${block.name}`)
)
}}
>
<SquareDashedIcon />
{block.description}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{block.name}
</span>
</CommandMenuItem>
))}
</CommandGroup>
)
}, [blocks, handleBlockHighlight, runCommand, router])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
@@ -195,39 +391,29 @@ export function CommandMenu({
<Button
variant="outline"
className={cn(
"text-foreground dark:bg-card hover:bg-muted/50 relative h-8 w-full justify-start pl-3 font-normal shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64"
"text-foreground dark:bg-card hover:bg-muted/50 relative h-8 w-full justify-start rounded-lg pl-3 font-normal shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64"
)}
onClick={() => setOpen(true)}
{...props}
>
<span className="hidden lg:inline-flex">Search documentation...</span>
<span className="inline-flex lg:hidden">Search...</span>
<div className="absolute top-1.5 right-1.5 hidden gap-1 group-has-[[data-slot=designer]]/body:hidden sm:flex">
<Kbd>K</Kbd>
</div>
</Button>
</DialogTrigger>
<DialogContent
showCloseButton={false}
className="rounded-xl border-none bg-clip-padding p-2 pb-11 shadow-2xl ring-4 ring-neutral-200/80 dark:bg-neutral-900 dark:ring-neutral-800"
>
<DialogContent className="rounded-xl border-none bg-clip-padding p-2 pb-11 shadow-2xl ring-4 ring-neutral-200/80 dark:bg-neutral-900 dark:ring-neutral-800">
<DialogHeader className="sr-only">
<DialogTitle>Search documentation...</DialogTitle>
<DialogDescription>Search for a command to run...</DialogDescription>
</DialogHeader>
<Command
className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border"
filter={(value, search, keywords) => {
handleSearchChange(search)
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
return 1
}
return 0
}}
filter={commandFilter}
>
<div className="relative">
<CommandInput placeholder="Search documentation..." />
<CommandInput
placeholder="Search documentation..."
onValueChange={handleSearchChange}
/>
{query.isLoading && (
<div className="pointer-events-none absolute top-1/2 right-3 z-10 flex -translate-y-1/2 items-center justify-center">
<Spinner className="text-muted-foreground size-4" />
@@ -238,151 +424,19 @@ export function CommandMenu({
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
{query.isLoading ? "Searching..." : "No results found."}
</CommandEmpty>
{navItems && navItems.length > 0 && (
<CommandGroup
heading="Pages"
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{navItems.map((item) => (
<CommandMenuItem
key={item.href}
value={`Navigation ${item.label}`}
keywords={["nav", "navigation", item.label.toLowerCase()]}
onHighlight={() => {
setSelectedType("page")
setCopyPayload("")
}}
onSelect={() => {
runCommand(() => router.push(item.href))
}}
>
<IconArrowRight />
{item.label}
</CommandMenuItem>
))}
</CommandGroup>
)}
{tree.children.map((group) => (
<CommandGroup
key={group.$id}
heading={group.name}
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{group.type === "folder" &&
group.children.map((item) => {
if (item.type === "page") {
const isComponent = item.url.includes("/components/")
if (!showMcpDocs && item.url.includes("/mcp")) {
return null
}
return (
<CommandMenuItem
key={item.url}
value={
item.name?.toString()
? `${group.name} ${item.name}`
: ""
}
keywords={isComponent ? ["component"] : undefined}
onHighlight={() =>
handlePageHighlight(isComponent, item)
}
onSelect={() => {
runCommand(() => router.push(item.url))
}}
>
{isComponent ? (
<div className="border-muted-foreground aspect-square size-4 rounded-full border border-dashed" />
) : (
<IconArrowRight />
)}
{item.name}
</CommandMenuItem>
)
}
return null
})}
</CommandGroup>
))}
{colors.map((colorPalette) => (
<CommandGroup
key={colorPalette.name}
heading={
colorPalette.name.charAt(0).toUpperCase() +
colorPalette.name.slice(1)
}
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{colorPalette.colors.map((color) => (
<CommandMenuItem
key={color.hex}
value={color.className}
keywords={["color", color.name, color.className]}
onHighlight={() => handleColorHighlight(color)}
onSelect={() => {
runCommand(() =>
copyToClipboardWithMeta(color.oklch, {
name: "copy_color",
properties: { color: color.oklch },
})
)
}}
>
<div
className="border-ghost aspect-square size-4 rounded-sm bg-(--color) after:rounded-sm"
style={{ "--color": color.oklch } as React.CSSProperties}
/>
{color.className}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{color.oklch}
</span>
</CommandMenuItem>
))}
</CommandGroup>
))}
{blocks?.length ? (
<CommandGroup
heading="Blocks"
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{blocks.map((block) => (
<CommandMenuItem
key={block.name}
value={block.name}
onHighlight={() => {
handleBlockHighlight(block)
}}
keywords={[
"block",
block.name,
block.description,
...block.categories,
]}
onSelect={() => {
runCommand(() =>
router.push(
`/blocks/${block.categories[0]}#${block.name}`
)
)
}}
>
<SquareDashedIcon />
{block.description}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{block.name}
</span>
</CommandMenuItem>
))}
</CommandGroup>
{navItemsSection}
{renderDelayedGroups ? (
<>
{pageGroupsSection}
{colorGroupsSection}
{blocksSection}
<SearchResults
setOpen={setOpen}
query={query}
search={search}
/>
</>
) : null}
<SearchResults
open={open}
setOpen={setOpen}
query={query}
search={search}
/>
</CommandList>
</Command>
<div className="text-muted-foreground absolute inset-x-0 bottom-0 z-20 flex h-10 items-center gap-2 rounded-b-xl border-t border-t-neutral-100 bg-neutral-50 px-4 text-xs font-medium dark:border-t-neutral-700 dark:bg-neutral-800">
@@ -468,23 +522,24 @@ function SearchResults({
query,
search,
}: {
open: boolean
setOpen: (open: boolean) => void
query: Query
search: string
}) {
const router = useRouter()
const uniqueResults =
query.data && Array.isArray(query.data)
? query.data.filter(
(item, index, self) =>
!(
item.type === "text" &&
item.content.trim().split(/\s+/).length <= 1
) && index === self.findIndex((t) => t.content === item.content)
)
: []
const uniqueResults = React.useMemo(() => {
if (!query.data || !Array.isArray(query.data)) {
return []
}
return query.data.filter(
(item, index, self) =>
!(
item.type === "text" && item.content.trim().split(/\s+/).length <= 1
) && index === self.findIndex((t) => t.content === item.content)
)
}, [query.data])
if (!search.trim()) {
return null
@@ -523,3 +578,27 @@ function SearchResults({
</CommandGroup>
)
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
{/* <DialogOverlay /> */}
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background fixed top-[15%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
)
}

View File

@@ -1,51 +1,267 @@
"use client"
import * as React from "react"
import Link from "next/link"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui/popover"
import { IconAlertCircle } from "@tabler/icons-react"
import { cn } from "@/lib/utils"
import {
LanguageProvider,
LanguageSelector,
useLanguageContext,
useTranslation,
type Translations,
} from "@/components/language-selector"
import { DirectionProvider as BaseDirectionProvider } from "@/registry/bases/base/ui/direction"
import { DirectionProvider as RadixDirectionProvider } from "@/registry/bases/radix/ui/direction"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator"
export function ComponentPreviewTabs({
className,
previewClassName,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
component,
source,
sourcePreview,
direction = "ltr",
styleName,
...props
}: React.ComponentProps<"div"> & {
previewClassName?: string
align?: "center" | "start" | "end"
hideCode?: boolean
chromeLessOnMobile?: boolean
component: React.ReactNode
source: React.ReactNode
sourcePreview?: React.ReactNode
direction?: "ltr" | "rtl"
styleName?: string
}) {
const [isMobileCodeVisible, setIsMobileCodeVisible] = React.useState(false)
const base = styleName?.split("-")[0]
return (
<div
data-slot="component-preview"
className={cn(
"group relative mt-4 mb-12 flex flex-col gap-2 rounded-lg border",
"group relative mt-4 mb-12 flex flex-col overflow-hidden rounded-xl border",
className
)}
{...props}
>
<div data-slot="preview">
<div
data-align={align}
className={cn(
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
)}
>
{component}
</div>
{!hideCode && (
<div
data-slot="code"
className="overflow-hidden [&_[data-rehype-pretty-code-figure]]:!m-0 [&_[data-rehype-pretty-code-figure]]:rounded-t-none [&_[data-rehype-pretty-code-figure]]:border-t [&_pre]:max-h-[400px]"
>
{source}
{direction === "rtl" ? (
<LanguageProvider defaultLanguage="ar">
<div className="flex h-16 items-center border-b px-4">
<RtlLanguageSelector />
<Popover>
<PopoverTrigger
render={
<Button
variant="ghost"
size="icon-sm"
className="ml-auto size-7"
>
<IconAlertCircle />
<span className="sr-only">Toggle</span>
</Button>
}
></PopoverTrigger>
<PopoverContent
side="bottom"
align="end"
className="w-56 text-xs"
>
<div>
I used AI to translate the text for demonstration purposes.
It&apos;s not perfect and may contain errors.
</div>
<Separator className="-mx-2.5 w-auto!" />
<div data-lang="ar">
لقد استخدمت الذكاء الاصطناعي لترجمة النص للأغراض التجريبية
فقط. قد لا تكون الترجمة دقيقة وقد تحتوي على أخطاء.
</div>
<Separator className="-mx-2.5 w-auto!" />
<div data-lang="he">
השתמשתי בבינה מלאכותית כדי לתרגם את הטקסט למטרות הדגמה. זה לא
מושלם ויכול להכיל שגיאות.
</div>
</PopoverContent>
</Popover>
</div>
<PreviewWrapper
align={align}
chromeLessOnMobile={chromeLessOnMobile}
previewClassName={previewClassName}
>
<DirectionProviderWrapper base={base}>
{component}
</DirectionProviderWrapper>
</PreviewWrapper>
</LanguageProvider>
) : (
<DirectionProviderWrapper base={base} dir="ltr">
<PreviewWrapper
align={align}
chromeLessOnMobile={chromeLessOnMobile}
previewClassName={previewClassName}
dir="ltr"
>
{component}
</PreviewWrapper>
</DirectionProviderWrapper>
)}
{!hideCode && (
<div
data-slot="code"
data-mobile-code-visible={isMobileCodeVisible}
className="relative overflow-hidden **:data-[slot=copy-button]:right-4 **:data-[slot=copy-button]:hidden data-[mobile-code-visible=true]:**:data-[slot=copy-button]:flex [&_[data-rehype-pretty-code-figure]]:!m-0 [&_[data-rehype-pretty-code-figure]]:rounded-t-none [&_[data-rehype-pretty-code-figure]]:border-t [&_pre]:max-h-72"
>
{isMobileCodeVisible ? (
<>
{direction === "rtl" && (
<div className="bg-code text-muted-foreground no-scrollbar relative z-10 overflow-x-auto border-t p-6 font-mono text-sm">
<pre>{`// You will notice this example uses dir and data-lang attributes.
// This is because this site is not RTL by default.
// In your application, you won't need these.`}</pre>
<span>
{"// See the "}
<Link
href="/docs/rtl"
className="underline underline-offset-4"
>
RTL guide
</Link>
{" for more information."}
</span>
</div>
)}
{source}
</>
) : (
<div className="relative">
{sourcePreview}
<div className="absolute inset-0 flex items-center justify-center pb-4">
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(to top, var(--color-code), color-mix(in oklab, var(--color-code) 60%, transparent), transparent)",
}}
/>
<Button
type="button"
size="sm"
variant="outline"
className="bg-background text-foreground dark:bg-background dark:text-foreground hover:bg-muted dark:hover:bg-muted relative z-10 rounded-lg shadow-none"
onClick={() => {
setIsMobileCodeVisible(true)
}}
>
View Code
</Button>
</div>
</div>
)}
</div>
)}
</div>
)
}
const directionTranslations: Translations<Record<string, never>> = {
en: {
dir: "ltr",
values: {},
},
ar: {
dir: "rtl",
values: {},
},
he: {
dir: "rtl",
values: {},
},
}
function RtlLanguageSelector({ className }: { className?: string }) {
const context = useLanguageContext()
if (!context) {
return null
}
return (
<LanguageSelector
value={context.language}
onValueChange={context.setLanguage}
className={className}
/>
)
}
function PreviewWrapper({
align,
chromeLessOnMobile,
previewClassName,
dir: explicitDir,
children,
}: {
align: "center" | "start" | "end"
chromeLessOnMobile: boolean
previewClassName?: string
dir?: "ltr" | "rtl"
children: React.ReactNode
}) {
// useTranslation handles the case when there's no LanguageProvider context.
// It will fall back to local state with defaultLanguage.
const translation = useTranslation(directionTranslations, "ar")
const dir = explicitDir ?? translation.dir
return (
<div
data-slot="preview"
dir={dir}
data-lang={dir === "rtl" ? translation.language : undefined}
>
<div
data-align={align}
data-chromeless={chromeLessOnMobile}
className={cn(
"preview relative flex h-72 w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start data-[chromeless=true]:h-auto data-[chromeless=true]:p-0",
previewClassName
)}
>
{children}
</div>
</div>
)
}
function DirectionProviderWrapper({
base,
dir: explicitDir,
children,
}: {
base?: string
dir?: "ltr" | "rtl"
children: React.ReactNode
}) {
// useTranslation handles the case when there's no LanguageProvider context.
// It will fall back to local state with defaultLanguage.
const translation = useTranslation(directionTranslations, "ar")
const dir = explicitDir ?? translation.dir
if (base === "base") {
return (
<BaseDirectionProvider direction={dir}>{children}</BaseDirectionProvider>
)
}
return <RadixDirectionProvider dir={dir}>{children}</RadixDirectionProvider>
}

View File

@@ -1,45 +1,37 @@
import * as React from "react"
import Image from "next/image"
import { getRegistryComponent } from "@/lib/registry"
import { ComponentPreviewTabs } from "@/components/component-preview-tabs"
import { ComponentSource } from "@/components/component-source"
import { Index } from "@/registry/__index__"
import { type Style } from "@/registry/_legacy-styles"
export function ComponentPreview({
name,
styleName = "new-york-v4",
type,
className,
previewClassName,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
styleName = "new-york-v4",
direction = "ltr",
caption,
...props
}: React.ComponentProps<"div"> & {
name: string
styleName?: Style["name"]
styleName?: string
align?: "center" | "start" | "end"
description?: string
hideCode?: boolean
type?: "block" | "component" | "example"
chromeLessOnMobile?: boolean
previewClassName?: string
direction?: "ltr" | "rtl"
caption?: string
}) {
const Component = Index[styleName]?.[name]?.component
if (!Component) {
return (
<p className="text-muted-foreground mt-6 text-sm">
Component{" "}
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
</code>{" "}
not found in registry.
</p>
)
}
if (type === "block") {
return (
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
const content = (
<div className="relative mt-6 aspect-[4/2.5] w-full overflow-hidden rounded-xl border md:-mx-1">
<Image
src={`/r/styles/new-york-v4/${name}-light.png`}
alt={name}
@@ -59,14 +51,42 @@ export function ComponentPreview({
</div>
</div>
)
if (caption) {
return (
<figure className="flex flex-col gap-4">
{content}
<figcaption className="text-muted-foreground text-center text-sm">
{caption}
</figcaption>
</figure>
)
}
return content
}
return (
const Component = getRegistryComponent(name, styleName)
if (!Component) {
return (
<p className="text-muted-foreground mt-6 text-sm">
Component{" "}
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
{name}
</code>{" "}
not found in registry.
</p>
)
}
const content = (
<ComponentPreviewTabs
className={className}
previewClassName={previewClassName}
align={align}
hideCode={hideCode}
component={<Component />}
component={React.createElement(Component)}
source={
<ComponentSource
name={name}
@@ -74,8 +94,34 @@ export function ComponentPreview({
styleName={styleName}
/>
}
sourcePreview={
<ComponentSource
name={name}
collapsible={false}
styleName={styleName}
maxLines={3}
/>
}
chromeLessOnMobile={chromeLessOnMobile}
direction={direction}
styleName={styleName}
{...props}
/>
)
if (caption) {
return (
<figure
data-hide-code={hideCode}
className="flex flex-col data-[hide-code=true]:gap-4"
>
{content}
<figcaption className="text-muted-foreground -mt-8 text-center text-sm data-[hide-code=true]:mt-0">
{caption}
</figcaption>
</figure>
)
}
return content
}

View File

@@ -3,12 +3,12 @@ import path from "node:path"
import * as React from "react"
import { highlightCode } from "@/lib/highlight-code"
import { getRegistryItem } from "@/lib/registry"
import { getDemoItem, getRegistryItem } from "@/lib/registry"
import { formatCode } from "@/lib/rehype"
import { cn } from "@/lib/utils"
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
import { CopyButton } from "@/components/copy-button"
import { getIconForLanguageExtension } from "@/components/icons"
import { type Style } from "@/registry/_legacy-styles"
export async function ComponentSource({
name,
@@ -18,13 +18,15 @@ export async function ComponentSource({
collapsible = true,
className,
styleName = "new-york-v4",
maxLines,
}: React.ComponentProps<"div"> & {
name?: string
src?: string
title?: string
language?: string
collapsible?: boolean
styleName?: Style["name"]
styleName?: string
maxLines?: number
}) {
if (!name && !src) {
return null
@@ -33,7 +35,9 @@ export async function ComponentSource({
let code: string | undefined
if (name) {
const item = await getRegistryItem(name, styleName)
const item =
(await getDemoItem(name, styleName)) ??
(await getRegistryItem(name, styleName))
code = item?.files?.[0]?.content
}
@@ -46,14 +50,14 @@ export async function ComponentSource({
return null
}
// Fix imports.
// Replace @/registry/${style}/ with @/components/.
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
// Replace export default with export.
code = code.replaceAll("export default", "export")
code = await formatCode(code, styleName)
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
// Truncate code if maxLines is set.
if (maxLines) {
code = code.split("\n").slice(0, maxLines).join("\n")
}
const lang = language ?? title?.split(".").pop() ?? "tsx"
const highlightedCode = await highlightCode(code, lang)

View File

@@ -1,23 +1,19 @@
import Link from "next/link"
import { PAGES_NEW } from "@/lib/docs"
import { source } from "@/lib/source"
import { getPagesFromFolder, type PageTreeFolder } from "@/lib/page-tree"
export function ComponentsList() {
const components = source.pageTree.children.find(
(page) => page.$id === "components"
)
if (components?.type !== "folder") {
return
}
const list = components.children.filter(
(component) => component.type === "page"
)
export function ComponentsList({
componentsFolder,
currentBase,
}: {
componentsFolder: PageTreeFolder
currentBase: string
}) {
const list = getPagesFromFolder(componentsFolder, currentBase)
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-x-8 lg:gap-x-16 lg:gap-y-6 xl:gap-x-20">
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 md:gap-x-8 lg:gap-x-16 lg:gap-y-6 xl:gap-x-20">
{list.map((component) => (
<Link
key={component.$id}

View File

@@ -6,11 +6,6 @@ import { IconCheck, IconCopy } from "@tabler/icons-react"
import { trackEvent, type Event } from "@/lib/events"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function copyToClipboardWithMeta(value: string, event?: Event) {
navigator.clipboard.writeText(value)
@@ -24,7 +19,6 @@ export function CopyButton({
className,
variant = "ghost",
event,
tooltip = "Copy to Clipboard",
...props
}: React.ComponentProps<typeof Button> & {
value: string
@@ -35,44 +29,40 @@ export function CopyButton({
const [hasCopied, setHasCopied] = React.useState(false)
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false)
}, 2000)
}, [])
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
data-slot="copy-button"
data-copied={hasCopied}
size="icon"
variant={variant}
className={cn(
"bg-code absolute top-3 right-2 z-10 size-7 hover:opacity-100 focus-visible:opacity-100",
className
)}
onClick={() => {
copyToClipboardWithMeta(
value,
event
? {
name: event,
properties: {
code: value,
},
}
: undefined
)
setHasCopied(true)
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</TooltipTrigger>
<TooltipContent>{hasCopied ? "Copied" : tooltip}</TooltipContent>
</Tooltip>
<Button
data-slot="copy-button"
data-copied={hasCopied}
size="icon"
variant={variant}
className={cn(
"bg-code absolute top-3 right-2 z-10 size-7 hover:opacity-100 focus-visible:opacity-100",
className
)}
onClick={() => {
copyToClipboardWithMeta(
value,
event
? {
name: event,
properties: {
code: value,
},
}
: undefined
)
setHasCopied(true)
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
)
}

View File

@@ -0,0 +1,39 @@
import Link from "next/link"
import { cn } from "@/lib/utils"
import { BASES } from "@/registry/bases"
export function DocsBaseSwitcher({
base,
component,
className,
}: {
base: string
component: string
className?: string
}) {
const activeBase = BASES.find((baseItem) => base === baseItem.name)
return (
<div className={cn("inline-flex w-full items-center gap-6", className)}>
{BASES.map((baseItem) => (
<Link
key={baseItem.name}
href={`/docs/components/${baseItem.name}/${component}`}
data-active={base === baseItem.name}
className="text-muted-foreground hover:text-foreground data-[active=true]:text-foreground after:bg-foreground relative inline-flex items-center justify-center gap-1 pt-1 pb-0.5 text-base font-medium transition-colors after:absolute after:inset-x-0 after:bottom-[-4px] after:h-0.5 after:opacity-0 after:transition-opacity data-[active=true]:after:opacity-100"
>
{baseItem.title}
</Link>
))}
{activeBase?.meta?.logo && (
<div
className="text-muted-foreground ml-auto size-4 shrink-0 opacity-80 [&_svg]:size-4"
dangerouslySetInnerHTML={{
__html: activeBase.meta.logo,
}}
/>
)}
</div>
)
}

View File

@@ -183,7 +183,10 @@ export function DocsCopyPage({ page, url }: { page: string; url: string }) {
<DropdownMenuTrigger asChild className="hidden sm:flex">
{trigger}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="shadow-none">
<DropdownMenuContent
align="end"
className="animate-none! rounded-lg shadow-none"
>
{Object.entries(menuItems).map(([key, value]) => (
<DropdownMenuItem key={key} asChild>
{value(url)}
@@ -193,13 +196,13 @@ export function DocsCopyPage({ page, url }: { page: string; url: string }) {
</DropdownMenu>
<Separator
orientation="vertical"
className="!bg-foreground/10 absolute top-0 right-8 z-0 !h-8 peer-focus-visible:opacity-0 sm:right-7 sm:!h-7"
className="!bg-foreground/5 absolute top-1 right-8 z-0 !h-6 peer-focus-visible:opacity-0 sm:right-7 sm:!h-5"
/>
<PopoverTrigger asChild className="flex sm:hidden">
{trigger}
</PopoverTrigger>
<PopoverContent
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-sm backdrop-blur-sm"
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-none backdrop-blur-sm"
align="start"
>
{Object.entries(menuItems).map(([key, value]) => (

View File

@@ -0,0 +1,179 @@
"use client"
import { IconCheck, IconCopy } from "@tabler/icons-react"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
function getPromptUrl(baseURL: string, url: string) {
return `${baseURL}?q=${encodeURIComponent(
`I'm looking at this shadcn/ui documentation: ${url}.
Help me understand how to use it. Be ready to explain concepts, give examples, or help debug based on it.
`
)}`
}
export function DocsPageLinks({ page, url }: { page: string; url: string }) {
const { copyToClipboard, isCopied } = useCopyToClipboard()
return (
<div className="flex flex-col gap-3 px-6">
<ul className="text-muted-foreground flex flex-col gap-2 text-[0.8rem]">
<li>
<button
onClick={() => copyToClipboard(page)}
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
>
{isCopied ? (
<IconCheck className="size-4" />
) : (
<IconCopy className="size-4" />
)}
Copy page
</button>
</li>
<li>
<a
href={`${url}.md`}
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
>
<svg strokeLinejoin="round" viewBox="0 0 22 16" className="size-4">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.5 2.25H2.5C1.80964 2.25 1.25 2.80964 1.25 3.5V12.5C1.25 13.1904 1.80964 13.75 2.5 13.75H19.5C20.1904 13.75 20.75 13.1904 20.75 12.5V3.5C20.75 2.80964 20.1904 2.25 19.5 2.25ZM2.5 1C1.11929 1 0 2.11929 0 3.5V12.5C0 13.8807 1.11929 15 2.5 15H19.5C20.8807 15 22 13.8807 22 12.5V3.5C22 2.11929 20.8807 1 19.5 1H2.5ZM3 4.5H4H4.25H4.6899L4.98715 4.82428L7 7.02011L9.01285 4.82428L9.3101 4.5H9.75H10H11V5.5V11.5H9V7.79807L7.73715 9.17572L7 9.97989L6.26285 9.17572L5 7.79807V11.5H3V5.5V4.5ZM15 8V4.5H17V8H19.5L17 10.5L16 11.5L15 10.5L12.5 8H15Z"
fill="currentColor"
/>
</svg>
View as Markdown
</a>
</li>
<li>
<a
href={getPromptUrl("https://v0.dev", url)}
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 147 70"
className="size-4"
>
<path d="M56 50.203V14h14v46.156C70 65.593 65.593 70 60.156 70c-2.596 0-5.158-1-7-2.843L0 14h19.797L56 50.203ZM147 56h-14V23.953L100.953 56H133v14H96.687C85.814 70 77 61.186 77 50.312V14h14v32.156L123.156 14H91V0h36.312C138.186 0 147 8.814 147 19.688V56Z" />
</svg>
Open in v0
</a>
</li>
<li>
<a
href={getPromptUrl("https://chatgpt.com", url)}
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="size-4"
>
<path
d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
fill="currentColor"
/>
</svg>
Open in ChatGPT
</a>
</li>
<li>
<a
href={getPromptUrl("https://claude.ai/new", url)}
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="size-4"
>
<path
d="m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z"
fill="currentColor"
/>
</svg>
Open in Claude
</a>
</li>
<li>
<a
href={getPromptUrl("https://scira.ai/", url)}
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
>
<svg
width="910"
height="934"
viewBox="0 0 910 934"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-4"
>
<path
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="8"
strokeLinejoin="round"
/>
<path
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="8"
strokeLinejoin="round"
/>
<path
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
stroke="currentColor"
strokeWidth="20"
strokeLinejoin="round"
/>
<path
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
stroke="currentColor"
strokeWidth="20"
strokeLinejoin="round"
/>
<path
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="8"
strokeLinejoin="round"
/>
<path
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="8"
strokeLinejoin="round"
/>
<path
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
stroke="currentColor"
strokeWidth="30"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Open in Scira
</a>
</li>
</ul>
</div>
)
}

View File

@@ -5,6 +5,7 @@ import { usePathname } from "next/navigation"
import { PAGES_NEW } from "@/lib/docs"
import { showMcpDocs } from "@/lib/flags"
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
import type { source } from "@/lib/source"
import {
Sidebar,
@@ -18,19 +19,31 @@ import {
} from "@/registry/new-york-v4/ui/sidebar"
const TOP_LEVEL_SECTIONS = [
{ name: "Get Started", href: "/docs" },
{ name: "Introduction", href: "/docs" },
{
name: "Components",
href: "/docs/components",
},
{
name: "Installation",
href: "/docs/installation",
},
{
name: "Directory",
href: "/docs/directory",
},
{
name: "RTL",
href: "/docs/rtl",
},
{
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Registry",
href: "/docs/registry",
},
{
name: "Forms",
href: "/docs/forms",
@@ -40,24 +53,27 @@ const TOP_LEVEL_SECTIONS = [
href: "/docs/changelog",
},
]
const EXCLUDED_SECTIONS = ["installation", "dark-mode"]
const EXCLUDED_PAGES = ["/docs", "/docs/changelog"]
const EXCLUDED_SECTIONS = ["installation", "dark-mode", "changelog", "rtl"]
const EXCLUDED_PAGES = ["/docs", "/docs/changelog", "/docs/rtl"]
export function DocsSidebar({
tree,
...props
}: React.ComponentProps<typeof Sidebar> & { tree: typeof source.pageTree }) {
const pathname = usePathname()
const currentBase = getCurrentBase(pathname)
return (
<Sidebar
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)-4rem)] overscroll-none bg-transparent lg:flex"
className="sticky top-[calc(var(--header-height)+0.6rem)] z-30 hidden h-[calc(100svh-10rem)] overscroll-none bg-transparent [--sidebar-menu-width:--spacing(56)] lg:flex"
collapsible="none"
{...props}
>
<SidebarContent className="no-scrollbar overflow-x-hidden px-2">
<div className="from-background via-background/80 to-background/50 sticky -top-1 z-10 h-8 shrink-0 bg-gradient-to-b blur-xs" />
<SidebarGroup>
<div className="h-9" />
<div className="from-background via-background/80 to-background/50 absolute top-8 z-10 h-8 w-(--sidebar-menu-width) shrink-0 bg-gradient-to-b blur-xs" />
<div className="via-border absolute top-12 right-2 bottom-0 hidden h-full w-px bg-gradient-to-b from-transparent to-transparent lg:flex" />
<SidebarContent className="no-scrollbar mx-auto w-(--sidebar-menu-width) overflow-x-hidden px-2">
<SidebarGroup className="pt-6">
<SidebarGroupLabel className="text-muted-foreground font-medium">
Sections
</SidebarGroupLabel>
@@ -79,8 +95,14 @@ export function DocsSidebar({
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={href}>
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
<span className="absolute inset-0 flex w-(--sidebar-menu-width) bg-transparent" />
{name}
{PAGES_NEW.includes(href) && (
<span
className="flex size-2 rounded-full bg-blue-500"
title="New"
/>
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
@@ -102,37 +124,34 @@ export function DocsSidebar({
<SidebarGroupContent>
{item.type === "folder" && (
<SidebarMenu className="gap-0.5">
{item.children.map((item) => {
if (
!showMcpDocs &&
item.type === "page" &&
item.url?.includes("/mcp")
) {
{getPagesFromFolder(item, currentBase).map((page) => {
if (!showMcpDocs && page.url.includes("/mcp")) {
return null
}
if (EXCLUDED_PAGES.includes(page.url)) {
return null
}
return (
item.type === "page" &&
!EXCLUDED_PAGES.includes(item.url) && (
<SidebarMenuItem key={item.url}>
<SidebarMenuButton
asChild
isActive={item.url === pathname}
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={item.url}>
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
{item.name}
{PAGES_NEW.includes(item.url) && (
<span
className="flex size-2 rounded-full bg-blue-500"
title="New"
/>
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
<SidebarMenuItem key={page.url}>
<SidebarMenuButton
asChild
isActive={page.url === pathname}
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={page.url}>
<span className="absolute inset-0 flex w-(--sidebar-menu-width) bg-transparent" />
{page.name}
{PAGES_NEW.includes(page.url) && (
<span
className="flex size-2 rounded-full bg-blue-500"
title="New"
/>
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>

View File

@@ -107,14 +107,14 @@ export function DocsTableOfContents({
return (
<div className={cn("flex flex-col gap-2 p-4 pt-0 text-sm", className)}>
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs">
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
On This Page
</p>
{toc.map((item) => (
<a
key={item.url}
href={item.url}
className="text-muted-foreground hover:text-foreground data-[active=true]:text-foreground text-[0.8rem] no-underline transition-colors data-[depth=3]:pl-4 data-[depth=4]:pl-6"
className="text-muted-foreground hover:text-foreground data-[active=true]:text-foreground text-[0.8rem] no-underline transition-colors data-[active=true]:font-medium data-[depth=3]:pl-4 data-[depth=4]:pl-6"
data-active={item.url === `#${activeHeading}`}
data-depth={item.depth}
>

View File

@@ -31,6 +31,12 @@ const examples = [
code: "https://github.com/shadcn/ui/tree/main/apps/v4/app/(app)/examples/authentication",
hidden: false,
},
{
name: "RTL",
href: "/examples/rtl",
code: "https://github.com/shadcn/ui/tree/main/apps/v4/app/(app)/examples/rtl",
hidden: false,
},
]
export function ExamplesNav({
@@ -76,10 +82,13 @@ function ExampleLink({
<Link
href={example.href}
key={example.href}
className="text-muted-foreground hover:text-primary data-[active=true]:text-primary flex h-7 items-center justify-center px-4 text-center text-base font-medium transition-colors"
className="text-muted-foreground hover:text-primary data-[active=true]:text-primary flex h-7 items-center justify-center gap-2 px-4 text-center text-base font-medium transition-colors"
data-active={isActive}
>
{example.name}
{example.name === "RTL" && (
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
)}
</Link>
)
}

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