Compare commits

..

132 Commits

Author SHA1 Message Date
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
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
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
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
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
shadcn
25e88fe4e9 Revert "Refactor Tooltip component to remove TooltipProvider (#9329)" (#9388)
This reverts commit d3590ceff9.
2026-01-20 12:58:22 +04:00
Francois Botha
d3590ceff9 Refactor Tooltip component to remove TooltipProvider (#9329) 2026-01-20 11:38:32 +04:00
phjjj
d04bc84a51 fix(registry): add missing {name} placeholder to motion-primitives url (#9381)
Co-authored-by: 박해준 <aaagowns@viewlingo.com>
2026-01-19 11:34:29 +04:00
Sunny Patel
f68465e815 docs(theming): add missing destructive-foreground CSS variable (#9379)
Fixes #9337

The `destructive-foreground` variable is used in components but was
missing from the theming documentation. Added the variable to all
color schemes (Neutral, Stone, Zinc, Gray, Slate) in both light and
dark modes.
2026-01-19 11:32:01 +04:00
shadcn
094edfcfe6 fix: charts 2026-01-18 12:11:20 +04:00
shadcn
5a42652c41 fix: theme for charts 2026-01-18 12:02:49 +04:00
shadcn
3409681949 fix: iframe display in dark mode 2026-01-18 11:53:59 +04:00
shadcn
1c989f9155 feat: inline component list on components page (#9368)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 18:40:11 +04:00
shadcn
0aea23013c fix: debug charts (#9364)
* fix: ts-morph for charts

* fix

* perf: parallelize chart loading and add LRU caching

- Prefetch all chart data in parallel using Promise.all()
- Add LRU cache for syntax highlighting (cross-request caching)
- Add LRU cache for registry items (cross-request caching)
- Parallelize file reads within registry items

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

* fix

* fix

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 18:28:51 +04:00
shadcn
bfce3031a3 Merge branch 'main' of github.com:shadcn-ui/ui 2026-01-17 13:49:37 +04:00
shadcn
cfb81c61de docs: add shadcn/create callout 2026-01-17 13:49:30 +04:00
Luis Llanes
7860ab83d1 chore(registry): update @shadcraft registry url (#9348)
* chore(registry): update @shadcraft registry url

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-17 13:30:24 +04:00
Паламар Роман
2acaf954d7 Fix: Preserve 'use client' directive in universal registry items (#8798)
* fix: preserve 'use client' directive in universal registry items

Universal items (registry:file and registry:item) are framework-agnostic
components that can be installed without shadcn project initialization.
However, the RSC transformer was incorrectly removing 'use client'
directives from these files when config.rsc was false/undefined, breaking
client-side functionality.

This fix ensures transformers are skipped for universal items, preserving
their original content including 'use client' directives, while regular
shadcn components continue to have transformers applied as expected.

Changes:
- Skip all transformers for registry:file and registry:item types
- Add tests to verify 'use client' preservation in universal items
- Ensure regular components still have transformers applied

Fixes issue where universal items would lose 'use client' directives when
copied without a full shadcn project setup.

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-17 13:12:01 +04:00
github-actions[bot]
1e9e337923 chore(release): version packages (#9352)
* chore(release): version packages

* ci: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2026-01-16 18:07:12 +04:00
Neeraj Dalal
66d2400784 feat(icons): the icons we all love and adore - remixicon (#9156)
* feat: remixicon

* chore: update deps

* chore: update icon

* chore: fix issues

* chore: build registry

* chore: changeset

* deps

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-16 18:00:06 +04:00
shadcn
682c98989d feat: registry add command (#9351)
* feat: implement registry add

* chore: changeset

* fix: registries docs

* feat: update add command

* fix
2026-01-16 17:55:48 +04:00
shadcn
77d7b39ef7 chore: rebuild registry 2026-01-16 17:07:32 +04:00
Huy Hoàng
5b3ba49aec fix(calendar): fix typo 'elative' to 'relative' in range_start classname (#9292)
Fixes #9278
2026-01-14 20:43:36 +04:00
shadcn
54edfd228d feat: add new registries (#9325)
* add new registries

* fix

* fix

* docs: add warning

* fix
2026-01-13 16:19:15 +04:00
Aniket Pawar
fd3e5515f3 feat: add @heroicons-animated to directory.json and registries.json (#9268)
* Add new registry for heroicons-animated

* Add '@heroicons-animated' collection to directory

Added new animated icon collection '@heroicons-animated' with homepage, URL, description, and logo.

* Update URL for @heroicons-animated registry

* Update directory.json

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-12 18:07:44 +04:00
Amarnath Dhumal
65ad910bca Add Chamaac registry (#9208)
Co-authored-by: shadcn <m@shadcn.com>
2026-01-12 18:05:47 +04:00
Md Kawsar Islam Yeasin
d4a1c89e8e feat: add neobrutalism to registry directory (#9168) 2026-01-12 18:01:59 +04:00
LN
78023693c6 Feat/add registry directory icons animated (#9143)
* feat: add new registry entry for icons-animated

* feat: add new registry entry for icons-animated with logo and description

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-12 18:01:37 +04:00
Aman Shakya
0fc52a7f4d Add new registry entry for @forgeui (#9074)
* added forgeui in registries

* Remove duplicate entries in registries.json

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-12 17:59:54 +04:00
github-actions[bot]
8fcfc563a9 chore(release): version packages (#9283)
* chore(release): version packages

* deps: update lock

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2026-01-06 14:40:12 +04:00
shadcn
f393c251fe test: revisit --force (#9287) 2026-01-06 14:29:27 +04:00
Md Kawsar Islam Yeasin
f2583391ea fix(cli): validate project name using npm package name rules (#9161)
* fix(cli): #9160 updated   CLI name validation

* chore: minor refactor and error message

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-06 13:28:31 +04:00
sam
c2fd847d65 feat: add OpenCode MCP client support (#8422)
* feat: add OpenCode MCP client support

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-06 12:16:05 +04:00
Phuc Bui
f6f2dfa5b2 Update URL for @phucbm registry (#9250)
* Fix URL for '@phucbm' in registries.json

Updated the URL for the '@phucbm' registry entry.

* Update homepage and URL in directory.json
2026-01-06 11:33:08 +04:00
shadcn
d07a7af82b chore: add bundui to directory (#9280) 2026-01-05 23:25:04 +04:00
Vitalii Rainchuk
b6d845f8a6 fix(base-ui): resolve pagination example and button client boundary issues (#9207)
* fix(pagination-example): mark example as client component

* fix(button): mark wrapper as client since Base UI button is a client component

* chore: rebuild registry

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-05 21:19:49 +04:00
Dhwani Popat
bd29630e4e fix: update Claude Code MCP documentation link (#9272) 2026-01-05 21:03:26 +04:00
shadcn
93ad19e4da chore: refactor shuffle button (#9276)
* chore: refactor shuffle button

* chore: format
2026-01-05 21:00:06 +04:00
3002 changed files with 123023 additions and 16963 deletions

View File

@@ -1,5 +0,0 @@
---
"shadcn": patch
---
validate app name on create

View File

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

View File

@@ -1,12 +1,12 @@
// ORIGINALLY FROM CLOUDFLARE WRANGLER: // ORIGINALLY FROM CLOUDFLARE WRANGLER:
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js // 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. // 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. // The standard step is only to run `changeset version` but this does not update the pnpm-lock.yaml file.
// So we also run `npm install`, which does this update. // So we also run `pnpm install`, which does this update.
// This is a workaround until this is handled automatically by `changeset version`. // This is a workaround until this is handled automatically by `changeset version`.
// See https://github.com/changesets/changesets/issues/421. // See https://github.com/changesets/changesets/issues/421.
exec("npx changeset version") execSync("npx changeset version", { stdio: "inherit" })
exec("npm install") execSync("pnpm install --lockfile-only", { stdio: "inherit" })

View File

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

View File

@@ -1,20 +1,8 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { import { Button } from "@/examples/radix/ui/button"
ArchiveIcon, import { ButtonGroup } from "@/examples/radix/ui/button-group"
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 { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -27,7 +15,18 @@ import {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, 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() { export function ButtonGroupDemo() {
const [label, setLabel] = React.useState("personal") const [label, setLabel] = React.useState("personal")
@@ -57,7 +56,7 @@ export function ButtonGroupDemo() {
<MoreHorizontalIcon /> <MoreHorizontalIcon />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 [--radius:1rem]"> <DropdownMenuContent align="end" className="w-48">
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem> <DropdownMenuItem>
<MailCheckIcon /> <MailCheckIcon />

View File

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

View File

@@ -1,10 +1,9 @@
"use client" "use client"
import { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react" 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() { export function ButtonGroupNested() {
return ( return (
<ButtonGroup> <ButtonGroup>

View File

@@ -1,14 +1,13 @@
import { BotIcon, ChevronDownIcon } from "lucide-react" import { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover" } from "@/examples/radix/ui/popover"
import { Separator } from "@/registry/new-york-v4/ui/separator" import { Separator } from "@/examples/radix/ui/separator"
import { Textarea } from "@/registry/new-york-v4/ui/textarea" import { Textarea } from "@/examples/radix/ui/textarea"
import { BotIcon, ChevronDownIcon } from "lucide-react"
export function ButtonGroupPopover() { export function ButtonGroupPopover() {
return ( return (

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/examples/radix/ui/button"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox" import { Checkbox } from "@/examples/radix/ui/checkbox"
import { import {
Field, Field,
FieldDescription, FieldDescription,
@@ -8,16 +8,17 @@ import {
FieldLegend, FieldLegend,
FieldSeparator, FieldSeparator,
FieldSet, FieldSet,
} from "@/registry/new-york-v4/ui/field" } from "@/examples/radix/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input" import { Input } from "@/examples/radix/ui/input"
import { import {
Select, Select,
SelectContent, SelectContent,
SelectGroup,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/registry/new-york-v4/ui/select" } from "@/examples/radix/ui/select"
import { Textarea } from "@/registry/new-york-v4/ui/textarea" import { Textarea } from "@/examples/radix/ui/textarea"
export function FieldDemo() { export function FieldDemo() {
return ( return (
@@ -69,18 +70,20 @@ export function FieldDemo() {
<SelectValue placeholder="MM" /> <SelectValue placeholder="MM" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="01">01</SelectItem> <SelectGroup>
<SelectItem value="02">02</SelectItem> <SelectItem value="01">01</SelectItem>
<SelectItem value="03">03</SelectItem> <SelectItem value="02">02</SelectItem>
<SelectItem value="04">04</SelectItem> <SelectItem value="03">03</SelectItem>
<SelectItem value="05">05</SelectItem> <SelectItem value="04">04</SelectItem>
<SelectItem value="06">06</SelectItem> <SelectItem value="05">05</SelectItem>
<SelectItem value="07">07</SelectItem> <SelectItem value="06">06</SelectItem>
<SelectItem value="08">08</SelectItem> <SelectItem value="07">07</SelectItem>
<SelectItem value="09">09</SelectItem> <SelectItem value="08">08</SelectItem>
<SelectItem value="10">10</SelectItem> <SelectItem value="09">09</SelectItem>
<SelectItem value="11">11</SelectItem> <SelectItem value="10">10</SelectItem>
<SelectItem value="12">12</SelectItem> <SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
</Field> </Field>
@@ -93,12 +96,14 @@ export function FieldDemo() {
<SelectValue placeholder="YYYY" /> <SelectValue placeholder="YYYY" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="2024">2024</SelectItem> <SelectGroup>
<SelectItem value="2025">2025</SelectItem> <SelectItem value="2024">2024</SelectItem>
<SelectItem value="2026">2026</SelectItem> <SelectItem value="2025">2025</SelectItem>
<SelectItem value="2027">2027</SelectItem> <SelectItem value="2026">2026</SelectItem>
<SelectItem value="2028">2028</SelectItem> <SelectItem value="2027">2027</SelectItem>
<SelectItem value="2029">2029</SelectItem> <SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
</SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
</Field> </Field>

View File

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

View File

@@ -1,13 +1,8 @@
"use client" "use client"
import { useState } from "react" import { useState } from "react"
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
import { import { Slider } from "@/examples/radix/ui/slider"
Field,
FieldDescription,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Slider } from "@/registry/new-york-v4/ui/slider"
export function FieldSlider() { export function FieldSlider() {
const [value, setValue] = useState([200, 800]) 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 { AppearanceSettings } from "./appearance-settings"
import { ButtonGroupDemo } from "./button-group-demo" import { ButtonGroupDemo } from "./button-group-demo"

View File

@@ -1,20 +1,19 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupButton, InputGroupButton,
InputGroupInput, InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group" } from "@/examples/radix/ui/input-group"
import { Label } from "@/registry/new-york-v4/ui/label" import { Label } from "@/examples/radix/ui/label"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover" } from "@/examples/radix/ui/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
export function InputGroupButtonExample() { export function InputGroupButtonExample() {
const [isFavorite, setIsFavorite] = React.useState(false) 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 { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu" } from "@/examples/radix/ui/dropdown-menu"
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
@@ -14,13 +11,15 @@ import {
InputGroupInput, InputGroupInput,
InputGroupText, InputGroupText,
InputGroupTextarea, InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group" } from "@/examples/radix/ui/input-group"
import { Separator } from "@/registry/new-york-v4/ui/separator" import { Separator } from "@/examples/radix/ui/separator"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, 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() { export function InputGroupDemo() {
return ( return (
@@ -67,11 +66,7 @@ export function InputGroupDemo() {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<InputGroupButton variant="ghost">Auto</InputGroupButton> <InputGroupButton variant="ghost">Auto</InputGroupButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent side="top" align="start">
side="top"
align="start"
className="[--radius:0.95rem]"
>
<DropdownMenuItem>Auto</DropdownMenuItem> <DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem> <DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem> <DropdownMenuItem>Manual</DropdownMenuItem>

View File

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

View File

@@ -1,24 +1,8 @@
"use client" "use client"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
IconApps, import { Badge } from "@/examples/radix/ui/badge"
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 { import {
Command, Command,
CommandEmpty, CommandEmpty,
@@ -26,7 +10,7 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
} from "@/registry/new-york-v4/ui/command" } from "@/examples/radix/ui/command"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
@@ -39,25 +23,36 @@ import {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu" } from "@/examples/radix/ui/dropdown-menu"
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field" import { Field, FieldLabel } from "@/examples/radix/ui/field"
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupButton, InputGroupButton,
InputGroupTextarea, InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group" } from "@/examples/radix/ui/input-group"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover" } from "@/examples/radix/ui/popover"
import { Switch } from "@/registry/new-york-v4/ui/switch" import { Switch } from "@/examples/radix/ui/switch"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, 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 = { const SAMPLE_DATA = {
mentionable: [ mentionable: [
@@ -190,7 +185,7 @@ export function NotionPromptForm() {
const hasMentions = mentions.length > 0 const hasMentions = mentions.length > 0
return ( return (
<form className="[--radius:1.2rem]"> <form>
<Field> <Field>
<FieldLabel htmlFor="notion-prompt" className="sr-only"> <FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt Prompt
@@ -222,7 +217,7 @@ export function NotionPromptForm() {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Mention a person, page, or date</TooltipContent> <TooltipContent>Mention a person, page, or date</TooltipContent>
</Tooltip> </Tooltip>
<PopoverContent className="p-0 [--radius:1.2rem]" align="start"> <PopoverContent className="p-0" align="start">
<Command> <Command>
<CommandInput placeholder="Search pages..." /> <CommandInput placeholder="Search pages..." />
<CommandList> <CommandList>
@@ -306,12 +301,8 @@ export function NotionPromptForm() {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Select AI model</TooltipContent> <TooltipContent>Select AI model</TooltipContent>
</Tooltip> </Tooltip>
<DropdownMenuContent <DropdownMenuContent side="top" align="start" className="w-48">
side="top" <DropdownMenuGroup className="w-48">
align="start"
className="[--radius:1rem]"
>
<DropdownMenuGroup className="w-42">
<DropdownMenuLabel className="text-muted-foreground text-xs"> <DropdownMenuLabel className="text-muted-foreground text-xs">
Select Agent Mode Select Agent Mode
</DropdownMenuLabel> </DropdownMenuLabel>
@@ -346,11 +337,7 @@ export function NotionPromptForm() {
<IconWorld /> All Sources <IconWorld /> All Sources
</InputGroupButton> </InputGroupButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent side="top" align="end" className="w-72">
side="top"
align="end"
className="[--radius:1rem]"
>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuItem
asChild asChild

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
import { type Metadata } from "next" import { type Metadata } from "next"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { PlusSignIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { Announcement } from "@/components/announcement" import { Announcement } from "@/components/announcement"
import { ExamplesNav } from "@/components/examples-nav" import { ExamplesNav } from "@/components/examples-nav"
@@ -58,10 +56,7 @@ export default function IndexPage() {
<PageHeaderDescription>{description}</PageHeaderDescription> <PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions> <PageActions>
<Button asChild size="sm" className="h-[31px] rounded-lg"> <Button asChild size="sm" className="h-[31px] rounded-lg">
<Link href="/create"> <Link href="/docs/installation">Get Started</Link>
<HugeiconsIcon icon={PlusSignIcon} />
New Project
</Link>
</Button> </Button>
<Button asChild size="sm" variant="ghost" className="rounded-lg"> <Button asChild size="sm" variant="ghost" className="rounded-lg">
<Link href="/docs/components">View Components</Link> <Link href="/docs/components">View Components</Link>

View File

@@ -2,7 +2,11 @@ import * as React from "react"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChartDisplay } from "@/components/chart-display" import {
ChartDisplay,
getCachedRegistryItem,
getChartHighlightedCode,
} from "@/components/chart-display"
import { getActiveStyle } from "@/registry/_legacy-styles" import { getActiveStyle } from "@/registry/_legacy-styles"
import { charts } from "@/app/(app)/charts/charts" import { charts } from "@/app/(app)/charts/charts"
@@ -44,6 +48,26 @@ export default async function ChartPage({ params }: ChartPageProps) {
const chartList = charts[chartType] const chartList = charts[chartType]
const activeStyle = await getActiveStyle() const activeStyle = await getActiveStyle()
// Prefetch all chart data in parallel for better performance.
// Charts are rendered via iframes, so we only need the metadata and highlighted code.
const chartDataPromises = chartList.map(async (chart) => {
const registryItem = await getCachedRegistryItem(chart.id, activeStyle.name)
if (!registryItem) return null
const highlightedCode = await getChartHighlightedCode(
registryItem.files?.[0]?.content ?? ""
)
if (!highlightedCode) return null
return {
...registryItem,
highlightedCode,
fullWidth: chart.fullWidth,
}
})
const prefetchedCharts = await Promise.all(chartDataPromises)
return ( return (
<div className="grid flex-1 gap-12 lg:gap-24"> <div className="grid flex-1 gap-12 lg:gap-24">
<h2 className="sr-only"> <h2 className="sr-only">
@@ -51,16 +75,14 @@ export default async function ChartPage({ params }: ChartPageProps) {
</h2> </h2>
<div className="grid flex-1 scroll-mt-20 items-stretch gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10"> <div className="grid flex-1 scroll-mt-20 items-stretch gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10">
{Array.from({ length: 12 }).map((_, index) => { {Array.from({ length: 12 }).map((_, index) => {
const chart = chartList[index] const chart = prefetchedCharts[index]
return chart ? ( return chart ? (
<ChartDisplay <ChartDisplay
key={chart.id} key={chart.name}
name={chart.id} chart={chart}
styleName={activeStyle.name} style={activeStyle.name}
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")} className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
> />
<chart.component />
</ChartDisplay>
) : ( ) : (
<div <div
key={`empty-${index}`} key={`empty-${index}`}

View File

@@ -63,9 +63,8 @@ export default function ChartsLayout({
</PageHeader> </PageHeader>
<PageNav id="charts"> <PageNav id="charts">
<ChartsNav /> <ChartsNav />
<ThemeSelector className="mr-4 hidden md:flex" />
</PageNav> </PageNav>
<div className="container-wrapper section-soft flex-1"> <div className="container-wrapper flex-1">
<div className="container pb-6"> <div className="container pb-6">
<section className="theme-container">{children}</section> <section className="theme-container">{children}</section>
</div> </div>

View File

@@ -1,21 +1,15 @@
import Link from "next/link" import Link from "next/link"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { mdxComponents } from "@/mdx-components" import { mdxComponents } from "@/mdx-components"
import { import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"
IconArrowLeft,
IconArrowRight,
IconArrowUpRight,
} from "@tabler/icons-react"
import fm from "front-matter"
import { findNeighbour } from "fumadocs-core/page-tree" import { findNeighbour } from "fumadocs-core/page-tree"
import z from "zod"
import { source } from "@/lib/source" import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils" import { absoluteUrl } from "@/lib/utils"
import { DocsBaseSwitcher } from "@/components/docs-base-switcher"
import { DocsCopyPage } from "@/components/docs-copy-page" import { DocsCopyPage } from "@/components/docs-copy-page"
import { DocsTableOfContents } from "@/components/docs-toc" import { DocsTableOfContents } from "@/components/docs-toc"
import { OpenInV0Cta } from "@/components/open-in-v0-cta" 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" import { Button } from "@/registry/new-york-v4/ui/button"
export const revalidate = false export const revalidate = false
@@ -85,127 +79,116 @@ export default async function Page(props: {
const doc = page.data const doc = page.data
const MDX = doc.body 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 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 ( 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="flex min-w-0 flex-1 flex-col">
<div className="h-(--top-spacing) shrink-0" /> <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 flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-start justify-between"> <div className="flex items-center justify-between md:items-start">
<h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl"> <h1 className="scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl">
{doc.title} {doc.title}
</h1> </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"> <div className="docs-nav flex items-center gap-2">
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} /> <div className="hidden sm:block">
{neighbours.previous && ( <DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
<Button </div>
variant="secondary" <div className="ml-auto flex gap-2">
size="icon" {neighbours.previous && (
className="extend-touch-target ml-auto size-8 shadow-none md:size-7" <Button
asChild variant="secondary"
> size="icon"
<Link href={neighbours.previous.url}> className="extend-touch-target size-8 shadow-none md:size-7"
<IconArrowLeft /> asChild
<span className="sr-only">Previous</span> >
</Link> <Link href={neighbours.previous.url}>
</Button> <IconArrowLeft />
)} <span className="sr-only">Previous</span>
{neighbours.next && ( </Link>
<Button </Button>
variant="secondary" )}
size="icon" {neighbours.next && (
className="extend-touch-target size-8 shadow-none md:size-7" <Button
asChild variant="secondary"
> size="icon"
<Link href={neighbours.next.url}> className="extend-touch-target size-8 shadow-none md:size-7"
<span className="sr-only">Next</span> asChild
<IconArrowRight /> >
</Link> <Link href={neighbours.next.url}>
</Button> <span className="sr-only">Next</span>
)} <IconArrowRight />
</Link>
</Button>
)}
</div>
</div> </div>
</div> </div>
{doc.description && ( {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} {doc.description}
</p> </p>
)} )}
</div> </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>
<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} /> <MDX components={mdxComponents} />
</div> </div>
</div> <div className="hidden h-16 w-full items-center gap-2 px-4 sm:flex sm:px-0">
<div className="mx-auto hidden h-16 w-full max-w-2xl items-center gap-2 px-4 sm:flex md:px-0"> {neighbours.previous && (
{neighbours.previous && ( <Button
<Button variant="secondary"
variant="secondary" size="sm"
size="sm" asChild
asChild className="shadow-none"
className="shadow-none" >
> <Link href={neighbours.previous.url}>
<Link href={neighbours.previous.url}> <IconArrowLeft /> {neighbours.previous.name}
<IconArrowLeft /> {neighbours.previous.name} </Link>
</Link> </Button>
</Button> )}
)} {neighbours.next && (
{neighbours.next && ( <Button
<Button variant="secondary"
variant="secondary" size="sm"
size="sm" className="ml-auto shadow-none"
className="ml-auto shadow-none" asChild
asChild >
> <Link href={neighbours.next.url}>
<Link href={neighbours.next.url}> {neighbours.next.name} <IconArrowRight />
{neighbours.next.name} <IconArrowRight /> </Link>
</Link> </Button>
</Button> )}
)} </div>
</div> </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="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 className="h-(--top-spacing) shrink-0"></div>
{doc.toc?.length ? ( {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} /> <DocsTableOfContents toc={doc.toc} />
<div className="h-12" />
</div> </div>
) : null} ) : 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 /> <OpenInV0Cta />
</div> </div>
</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 ( return (
<div className="container-wrapper flex flex-1 flex-col px-2"> <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} /> <DocsSidebar tree={source.pageTree} />
<div className="h-full w-full">{children}</div> <div className="h-full w-full">{children}</div>
</SidebarProvider> </SidebarProvider>

View File

@@ -70,7 +70,7 @@ export default function ExamplesLayout({
</PageNav> </PageNav>
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6"> <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="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} {children}
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider" import type { Slider as SliderPrimitive } from "radix-ui"
import { import {
HoverCard, HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider" import { Slider } from "@/registry/new-york-v4/ui/slider"
interface TopPSelectorProps { interface TopPSelectorProps {
defaultValue: SliderProps["defaultValue"] defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
} }
export function TopPSelector({ defaultValue }: TopPSelectorProps) { 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" "use client"
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
import { type Table } from "@tanstack/react-table" import { type Table } from "@tanstack/react-table"
import { Settings2 } from "lucide-react" import { Settings2 } from "lucide-react"
@@ -11,6 +10,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu" } from "@/registry/new-york-v4/ui/dropdown-menu"
export function DataTableViewOptions<TData>({ export function DataTableViewOptions<TData>({

View File

@@ -3,10 +3,23 @@ import { NextResponse, type NextRequest } from "next/server"
import { processMdxForLLMs } from "@/lib/llm" import { processMdxForLLMs } from "@/lib/llm"
import { source } from "@/lib/source" import { source } from "@/lib/source"
import { getActiveStyle } from "@/registry/_legacy-styles" import { getActiveStyle, type Style } from "@/registry/_legacy-styles"
export const revalidate = false 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( export async function GET(
_req: NextRequest, _req: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> } { params }: { params: Promise<{ slug?: string[] }> }
@@ -19,9 +32,11 @@ export async function GET(
notFound() notFound()
} }
const effectiveStyle = getStyleFromSlug(slug, activeStyle.name)
const processedContent = processMdxForLLMs( const processedContent = processMdxForLLMs(
await page.data.getText("raw"), await page.data.getText("raw"),
activeStyle.name effectiveStyle as Style["name"]
) )
return new NextResponse(processedContent, { return new NextResponse(processedContent, {

View File

@@ -7,15 +7,17 @@ import { HugeiconsIcon } from "@hugeicons/react"
import { useIsMobile } from "@/hooks/use-mobile" import { useIsMobile } from "@/hooks/use-mobile"
import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config" import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config"
import { FieldGroup } from "@/registry/new-york-v4/ui/field" import { FieldGroup } from "@/registry/new-york-v4/ui/field"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker" import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker" import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
import { BasePicker } from "@/app/(create)/components/base-picker" import { BasePicker } from "@/app/(create)/components/base-picker"
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
import { FontPicker } from "@/app/(create)/components/font-picker" import { FontPicker } from "@/app/(create)/components/font-picker"
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker" import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
import { MenuColorPicker } from "@/app/(create)/components/menu-picker" import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
import { PresetPicker } from "@/app/(create)/components/preset-picker" import { PresetPicker } from "@/app/(create)/components/preset-picker"
import { RadiusPicker } from "@/app/(create)/components/radius-picker" import { RadiusPicker } from "@/app/(create)/components/radius-picker"
import { RandomButton } from "@/app/(create)/components/random-button"
import { ResetButton } from "@/app/(create)/components/reset-button"
import { StylePicker } from "@/app/(create)/components/style-picker" import { StylePicker } from "@/app/(create)/components/style-picker"
import { ThemePicker } from "@/app/(create)/components/theme-picker" import { ThemePicker } from "@/app/(create)/components/theme-picker"
import { FONTS } from "@/app/(create)/lib/fonts" import { FONTS } from "@/app/(create)/lib/fonts"
@@ -75,7 +77,10 @@ export function Customizer() {
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} /> <RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} /> <MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} /> <MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
<CustomizerControls className="mt-auto hidden w-full flex-col md:flex" /> <div className="mt-auto hidden w-full flex-col items-center gap-0 md:flex">
<RandomButton />
<ResetButton />
</div>
</FieldGroup> </FieldGroup>
</div> </div>
</div> </div>

View File

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

View File

@@ -45,6 +45,12 @@ const IconPhosphor = lazy(() =>
})) }))
) )
const IconRemixicon = lazy(() =>
import("@/registry/icons/icon-remixicon").then((mod) => ({
default: mod.IconRemixicon,
}))
)
const PREVIEW_ICONS = { const PREVIEW_ICONS = {
lucide: [ lucide: [
"CopyIcon", "CopyIcon",
@@ -110,6 +116,22 @@ const PREVIEW_ICONS = {
"CaretDownIcon", "CaretDownIcon",
"CaretRightIcon", "CaretRightIcon",
], ],
remixicon: [
"RiFileCopyLine",
"RiErrorWarningLine",
"RiDeleteBinLine",
"RiShareLine",
"RiShoppingBagLine",
"RiMoreLine",
"RiLoaderLine",
"RiAddLine",
"RiSubtractLine",
"RiArrowLeftLine",
"RiArrowRightLine",
"RiCheckLine",
"RiArrowDownSLine",
"RiArrowRightSLine",
],
} }
const logos = { const logos = {
@@ -194,6 +216,17 @@ const logos = {
/> />
</svg> </svg>
), ),
remixicon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"
>
<path d="M12 2C17.5228 2 22 6.47715 22 12C22 15.3137 19.3137 18 16 18C12.6863 18 10 15.3137 10 12C10 11.4477 9.55228 11 9 11C8.44772 11 8 11.4477 8 12C8 16.4183 11.5817 20 16 20C16.8708 20 17.7084 19.8588 18.4932 19.6016C16.7458 21.0956 14.4792 22 12 22C6.6689 22 2.3127 17.8283 2.0166 12.5713C2.23647 9.45772 4.83048 7 8 7C11.3137 7 14 9.68629 14 13C14 13.5523 14.4477 14 15 14C15.5523 14 16 13.5523 16 13C16 8.58172 12.4183 5 8 5C6.50513 5 5.1062 5.41032 3.90918 6.12402C5.72712 3.62515 8.67334 2 12 2Z" />
</svg>
),
} }
export function IconLibraryPicker({ export function IconLibraryPicker({
@@ -301,7 +334,9 @@ const IconLibraryPreview = memo(function IconLibraryPreview({
? IconTabler ? IconTabler
: iconLibrary === "hugeicons" : iconLibrary === "hugeicons"
? IconHugeicons ? IconHugeicons
: IconPhosphor : iconLibrary === "phosphor"
? IconPhosphor
: IconRemixicon
return ( return (
<Suspense <Suspense

View File

@@ -30,6 +30,12 @@ const IconPhosphor = lazy(() =>
})) }))
) )
const IconRemixicon = lazy(() =>
import("@/registry/icons/icon-remixicon").then((mod) => ({
default: mod.IconRemixicon,
}))
)
export function IconPlaceholder({ export function IconPlaceholder({
...props ...props
}: { }: {
@@ -52,6 +58,9 @@ export function IconPlaceholder({
{iconLibrary === "phosphor" && ( {iconLibrary === "phosphor" && (
<IconPhosphor name={iconName} {...props} /> <IconPhosphor name={iconName} {...props} />
)} )}
{iconLibrary === "remixicon" && (
<IconRemixicon name={iconName} {...props} />
)}
</Suspense> </Suspense>
) )
} }

View File

@@ -99,7 +99,7 @@ export function ItemPicker({
variant="outline" variant="outline"
aria-label="Select item" aria-label="Select item"
size="sm" 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} /> <HugeiconsIcon icon={Search01Icon} />
</ComboboxTrigger> </ComboboxTrigger>
<ComboboxContent <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" side="bottom"
align="center" align="end"
> >
<ComboboxInput <ComboboxInput
showTrigger={false} showTrigger={false}

View File

@@ -80,7 +80,7 @@ function PickerLabel({
data-slot="dropdown-menu-label" data-slot="dropdown-menu-label"
data-inset={inset} data-inset={inset}
className={cn( 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 className
)} )}
{...props} {...props}
@@ -103,7 +103,7 @@ function PickerItem({
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( 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 className
)} )}
{...props} {...props}
@@ -128,7 +128,7 @@ function PickerSubTrigger({
data-slot="dropdown-menu-sub-trigger" data-slot="dropdown-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( 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 className
)} )}
{...props} {...props}
@@ -139,6 +139,7 @@ function PickerSubTrigger({
tabler="IconChevronRight" tabler="IconChevronRight"
hugeicons="ArrowRight01Icon" hugeicons="ArrowRight01Icon"
phosphor="CaretRightIcon" phosphor="CaretRightIcon"
remixicon="RiArrowRightSLine"
className="ml-auto" className="ml-auto"
/> />
</MenuPrimitive.SubmenuTrigger> </MenuPrimitive.SubmenuTrigger>
@@ -179,7 +180,7 @@ function PickerCheckboxItem({
<MenuPrimitive.CheckboxItem <MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( 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 className
)} )}
checked={checked} checked={checked}
@@ -192,6 +193,7 @@ function PickerCheckboxItem({
tabler="IconCheck" tabler="IconCheck"
hugeicons="Tick02Icon" hugeicons="Tick02Icon"
phosphor="CheckIcon" phosphor="CheckIcon"
remixicon="RiCheckLine"
/> />
</MenuPrimitive.CheckboxItemIndicator> </MenuPrimitive.CheckboxItemIndicator>
</span> </span>
@@ -218,7 +220,7 @@ function PickerRadioItem({
<MenuPrimitive.RadioItem <MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( 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 className
)} )}
{...props} {...props}
@@ -233,6 +235,7 @@ function PickerRadioItem({
tabler="IconCheck" tabler="IconCheck"
hugeicons="Tick02Icon" hugeicons="Tick02Icon"
phosphor="CheckIcon" phosphor="CheckIcon"
remixicon="RiCheckLine"
className="size-4 pointer-coarse:size-5" className="size-4 pointer-coarse:size-5"
/> />
</MenuPrimitive.RadioItemIndicator> </MenuPrimitive.RadioItemIndicator>

View File

@@ -1,12 +1,12 @@
"use client" "use client"
import * as React from "react" 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 { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
import { Badge } from "@/registry/new-york-v4/ui/badge" import { Badge } from "@/registry/new-york-v4/ui/badge"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/customizer-controls"
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker" import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync" import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
import { import {
serializeDesignSystemSearchParams, serializeDesignSystemSearchParams,
@@ -16,7 +16,7 @@ import {
export function Preview() { export function Preview() {
const [params] = useDesignSystemSearchParams() const [params] = useDesignSystemSearchParams()
const iframeRef = React.useRef<HTMLIFrameElement>(null) 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. // Sync resizable panel with URL param changes.
React.useEffect(() => { React.useEffect(() => {

View File

@@ -2,13 +2,11 @@
import * as React from "react" import * as React from "react"
import Script from "next/script" import Script from "next/script"
import { DiceFaces05Icon, Undo02Icon } from "@hugeicons/core-free-icons" import { DiceFaces05Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react" import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { import {
BASE_COLORS, BASE_COLORS,
DEFAULT_CONFIG,
getThemesForBaseColor, getThemesForBaseColor,
iconLibraries, iconLibraries,
MENU_ACCENTS, MENU_ACCENTS,
@@ -18,6 +16,11 @@ import {
} from "@/registry/config" } from "@/registry/config"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
import { Kbd } from "@/registry/new-york-v4/ui/kbd" import { Kbd } from "@/registry/new-york-v4/ui/kbd"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { useLocks } from "@/app/(create)/hooks/use-locks" import { useLocks } from "@/app/(create)/hooks/use-locks"
import { FONTS } from "@/app/(create)/lib/fonts" import { FONTS } from "@/app/(create)/lib/fonts"
import { import {
@@ -33,26 +36,10 @@ function randomItem<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)] return array[Math.floor(Math.random() * array.length)]
} }
export function CustomizerControls({ className }: { className?: string }) { export function RandomButton() {
const { locks } = useLocks() const { locks } = useLocks()
const [params, setParams] = useDesignSystemSearchParams() const [params, setParams] = useDesignSystemSearchParams()
const handleReset = React.useCallback(() => {
setParams({
base: params.base, // Keep the current base value
style: DEFAULT_CONFIG.style,
baseColor: DEFAULT_CONFIG.baseColor,
theme: DEFAULT_CONFIG.theme,
iconLibrary: DEFAULT_CONFIG.iconLibrary,
font: DEFAULT_CONFIG.font,
menuAccent: DEFAULT_CONFIG.menuAccent,
menuColor: DEFAULT_CONFIG.menuColor,
radius: DEFAULT_CONFIG.radius,
template: DEFAULT_CONFIG.template,
item: "preview",
})
}, [setParams, params.base])
const handleRandomize = React.useCallback(() => { const handleRandomize = React.useCallback(() => {
// Use current value if locked, otherwise randomize. // Use current value if locked, otherwise randomize.
const baseColor = locks.has("baseColor") const baseColor = locks.has("baseColor")
@@ -130,33 +117,30 @@ export function CustomizerControls({ className }: { className?: string }) {
}, [handleRandomize]) }, [handleRandomize])
return ( return (
<div className={cn("items-center gap-0", className)}> <Tooltip>
<Button <TooltipTrigger asChild>
variant="ghost" <Button
size="sm" variant="ghost"
onClick={handleRandomize} size="sm"
className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!" onClick={handleRandomize}
> className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
<div className="flex flex-col justify-start text-left"> >
<div className="text-muted-foreground text-xs">Shuffle</div> <div className="flex flex-col justify-start text-left">
<div className="text-foreground text-sm font-medium">Try Random</div> <div className="text-muted-foreground text-xs">Shuffle</div>
</div> <div className="text-foreground text-sm font-medium">
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" /> Try Random
<Kbd className="bg-foreground/10 text-foreground hidden md:flex">R</Kbd> </div>
</Button> </div>
<Button <HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
variant="ghost" <Kbd className="bg-foreground/10 text-foreground hidden md:flex">
size="sm" R
onClick={handleReset} </Kbd>
className="border-foreground/10 bg-muted/50 hidden h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:flex md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!" </Button>
> </TooltipTrigger>
<div className="flex flex-col justify-start text-left"> <TooltipContent side="left">
<div className="text-muted-foreground text-xs">Reset</div> Use browser back/forward to navigate history
<div className="text-foreground text-sm font-medium">Start Over</div> </TooltipContent>
</div> </Tooltip>
<HugeiconsIcon icon={Undo02Icon} className="-translate-x-0.5" />
</Button>
</div>
) )
} }

View File

@@ -0,0 +1,75 @@
"use client"
import * as React from "react"
import { Undo02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { DEFAULT_CONFIG } from "@/registry/config"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/registry/new-york-v4/ui/alert-dialog"
import { Button } from "@/registry/new-york-v4/ui/button"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function ResetButton() {
const [params, setParams] = useDesignSystemSearchParams()
const handleReset = React.useCallback(() => {
setParams({
base: params.base, // Keep the current base value.
style: DEFAULT_CONFIG.style,
baseColor: DEFAULT_CONFIG.baseColor,
theme: DEFAULT_CONFIG.theme,
iconLibrary: DEFAULT_CONFIG.iconLibrary,
font: DEFAULT_CONFIG.font,
menuAccent: DEFAULT_CONFIG.menuAccent,
menuColor: DEFAULT_CONFIG.menuColor,
radius: DEFAULT_CONFIG.radius,
template: DEFAULT_CONFIG.template,
item: "preview",
})
}, [setParams, params.base])
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
className="border-foreground/10 bg-muted/50 hidden h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:flex md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Reset</div>
<div className="text-foreground text-sm font-medium">
Start Over
</div>
</div>
<HugeiconsIcon icon={Undo02Icon} className="-translate-x-0.5" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="dialog-ring p-4 sm:max-w-sm">
<AlertDialogHeader>
<AlertDialogTitle>Reset to defaults?</AlertDialogTitle>
<AlertDialogDescription>
This will reset all customization options to their default values.
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="rounded-lg">Cancel</AlertDialogCancel>
<AlertDialogAction className="rounded-lg" onClick={handleReset}>
Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import Link from "next/link"
import { import {
ComputerTerminal01Icon, ComputerTerminal01Icon,
Copy01Icon, Copy01Icon,
@@ -23,6 +24,8 @@ import {
} from "@/registry/new-york-v4/ui/dialog" } from "@/registry/new-york-v4/ui/dialog"
import { import {
Field, Field,
FieldContent,
FieldDescription,
FieldGroup, FieldGroup,
FieldLabel, FieldLabel,
FieldTitle, FieldTitle,
@@ -31,6 +34,7 @@ import {
RadioGroup, RadioGroup,
RadioGroupItem, RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group" } from "@/registry/new-york-v4/ui/radio-group"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import { import {
Tabs, Tabs,
TabsContent, TabsContent,
@@ -71,14 +75,15 @@ export function ToolbarControls() {
const packageManager = config.packageManager || "pnpm" const packageManager = config.packageManager || "pnpm"
const commands = React.useMemo(() => { const commands = React.useMemo(() => {
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000" 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}` 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 templateFlag = params.template ? ` --template ${params.template}` : ""
const rtlFlag = params.rtl ? " --rtl" : ""
return { return {
pnpm: `pnpm dlx shadcn@latest create --preset "${url}"${templateFlag}`, pnpm: `pnpm dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
npm: `npx shadcn@latest create --preset "${url}"${templateFlag}`, npm: `npx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
yarn: `yarn dlx shadcn@latest create --preset "${url}"${templateFlag}`, yarn: `yarn dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
bun: `bunx --bun shadcn@latest create --preset "${url}"${templateFlag}`, bun: `bunx --bun shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
} }
}, [ }, [
params.base, params.base,
@@ -91,6 +96,7 @@ export function ToolbarControls() {
params.menuColor, params.menuColor,
params.radius, params.radius,
params.template, params.template,
params.rtl,
]) ])
const command = commands[packageManager] const command = commands[packageManager]
@@ -164,7 +170,7 @@ export function ToolbarControls() {
{selectedTemplate?.title} + shadcn/ui project. {selectedTemplate?.title} + shadcn/ui project.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FieldGroup> <FieldGroup className="gap-3">
<Field> <Field>
<FieldLabel htmlFor="template" className="sr-only"> <FieldLabel htmlFor="template" className="sr-only">
Template Template
@@ -183,7 +189,7 @@ export function ToolbarControls() {
<FieldLabel <FieldLabel
key={template.value} key={template.value}
htmlFor={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!"> <Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!">
<RadioGroupItem <RadioGroupItem
@@ -205,59 +211,81 @@ export function ToolbarControls() {
))} ))}
</RadioGroup> </RadioGroup>
</Field> </Field>
</FieldGroup> <FieldLabel className="has-data-[state=checked]:border-primary/10 rounded-lg!">
<Tabs <Field orientation="horizontal">
value={packageManager} <FieldContent className="gap-1">
onValueChange={(value) => { <FieldTitle>Enable RTL</FieldTitle>
setConfig({ <FieldDescription>
...config, <a
packageManager: value as "pnpm" | "npm" | "yarn" | "bun", href={`/docs/rtl/${params.template}`}
}) className="text-foreground underline"
}} target="_blank"
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border" rel="noopener noreferrer"
> >
<div className="flex items-center gap-2 p-2"> View the RTL setup guide for {selectedTemplate?.title}.
<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!"> </a>
<TabsTrigger value="pnpm">pnpm</TabsTrigger> </FieldDescription>
<TabsTrigger value="npm">npm</TabsTrigger> </FieldContent>
<TabsTrigger value="yarn">yarn</TabsTrigger> <Switch
<TabsTrigger value="bun">bun</TabsTrigger> checked={params.rtl}
</TabsList> onCheckedChange={(rtl) => setParams({ rtl })}
<Tooltip> className="shadow-none"
<TooltipTrigger asChild> />
<Button </Field>
size="icon-sm" </FieldLabel>
variant="ghost" <Tabs
className="ml-auto size-7 rounded-lg" value={packageManager}
onClick={handleCopyFromTabs} onValueChange={(value) => {
> setConfig({
{hasCopied ? ( ...config,
<HugeiconsIcon icon={Tick02Icon} className="size-4" /> packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
) : ( })
<HugeiconsIcon icon={Copy01Icon} className="size-4" /> }}
)} className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
<span className="sr-only">Copy command</span> >
</Button> <div className="flex items-center gap-2 p-2">
</TooltipTrigger> <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!">
<TooltipContent> <TabsTrigger value="pnpm">pnpm</TabsTrigger>
{hasCopied ? "Copied!" : "Copy command"} <TabsTrigger value="npm">npm</TabsTrigger>
</TooltipContent> <TabsTrigger value="yarn">yarn</TabsTrigger>
</Tooltip> <TabsTrigger value="bun">bun</TabsTrigger>
</div> </TabsList>
{Object.entries(commands).map(([key, cmd]) => { <Tooltip>
return ( <TooltipTrigger asChild>
<TabsContent key={key} value={key}> <Button
<div className="bg-surface border-border/50 text-surface-foreground relative overflow-hidden border-t px-3 py-3"> size="icon-sm"
<div className="no-scrollbar overflow-x-auto"> variant="ghost"
<code className="font-mono text-sm whitespace-nowrap"> className="ml-auto size-7 rounded-lg"
{cmd} onClick={handleCopyFromTabs}
</code> >
{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>
</div> </TabsContent>
</TabsContent> )
) })}
})} </Tabs>
</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"> <DialogFooter className="bg-muted/50 -mx-6 mt-2 -mb-6 flex flex-col gap-2 border-t p-6 sm:flex-col">
<Button <Button
size="sm" size="sm"

View File

@@ -14,7 +14,7 @@ import {
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params" import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function V0Button({ className }: { className?: string }) { export function V0Button({ className }: { className?: string }) {
const [params, setParams] = useDesignSystemSearchParams() const [params] = useDesignSystemSearchParams()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const isMounted = useMounted() const isMounted = useMounted()
@@ -32,7 +32,7 @@ export function V0Button({ className }: { className?: string }) {
size="sm" size="sm"
variant={isMobile ? "default" : "outline"} variant={isMobile ? "default" : "outline"}
className={cn( 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 className
)} )}
asChild 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}`} href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`}
target="_blank" target="_blank"
> >
Open in <Icons.v0 className="size-5" /> <span className="lg:hidden xl:block">Open in</span>
<Icons.v0 className="size-5" />
</a> </a>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>

View File

@@ -4,7 +4,11 @@ import { ArrowLeftIcon } from "lucide-react"
import type { SearchParams } from "nuqs/server" import type { SearchParams } from "nuqs/server"
import { siteConfig } from "@/lib/config" import { siteConfig } from "@/lib/config"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils" 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 { ModeSwitcher } from "@/components/mode-switcher"
import { SiteConfig } from "@/components/site-config" import { SiteConfig } from "@/components/site-config"
import { BASES } from "@/registry/config" import { BASES } from "@/registry/config"
@@ -12,10 +16,11 @@ import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator" import { Separator } from "@/registry/new-york-v4/ui/separator"
import { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar" import { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar"
import { Customizer } from "@/app/(create)/components/customizer" import { Customizer } from "@/app/(create)/components/customizer"
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
import { ItemExplorer } from "@/app/(create)/components/item-explorer" import { ItemExplorer } from "@/app/(create)/components/item-explorer"
import { ItemPicker } from "@/app/(create)/components/item-picker" import { ItemPicker } from "@/app/(create)/components/item-picker"
import { Preview } from "@/app/(create)/components/preview" import { Preview } from "@/app/(create)/components/preview"
import { RandomButton } from "@/app/(create)/components/random-button"
import { ResetButton } from "@/app/(create)/components/reset-button"
import { ShareButton } from "@/app/(create)/components/share-button" import { ShareButton } from "@/app/(create)/components/share-button"
import { ToolbarControls } from "@/app/(create)/components/toolbar-controls" import { ToolbarControls } from "@/app/(create)/components/toolbar-controls"
import { V0Button } from "@/app/(create)/components/v0-button" import { V0Button } from "@/app/(create)/components/v0-button"
@@ -63,6 +68,7 @@ export default async function CreatePage({
const params = await loadDesignSystemSearchParams(searchParams) const params = await loadDesignSystemSearchParams(searchParams)
const base = BASES.find((b) => b.name === params.base) ?? BASES[0] const base = BASES.find((b) => b.name === params.base) ?? BASES[0]
const pageTree = source.pageTree
const items = await getItemsForBase(base.name) const items = await getItemsForBase(base.name)
const filteredItems = items const filteredItems = items
@@ -81,35 +87,34 @@ export default async function CreatePage({
<header className="sticky top-0 z-50 w-full"> <header className="sticky top-0 z-50 w-full">
<div className="container-wrapper 3xl:fixed:px-0 px-6"> <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="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">
<MobileNav
tree={pageTree}
items={siteConfig.navItems}
className="flex lg:hidden"
/>
<Button <Button
asChild asChild
variant="outline" variant="ghost"
size="sm" size="icon"
className="rounded-lg shadow-none" className="hidden size-8 lg:flex"
> >
<Link href="/"> <Link href="/">
<ArrowLeftIcon /> <Icons.logo className="size-5" />
Back <span className="sr-only">{siteConfig.name}</span>
</Link> </Link>
</Button> </Button>
<Separator <MainNav items={siteConfig.navItems} className="hidden lg:flex" />
orientation="vertical"
className="mx-2 hidden sm:mx-4 lg:flex"
/>
<div className="text-muted-foreground hidden text-sm font-medium lg:flex">
New Project
</div>
</div> </div>
<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} /> <ItemPicker items={filteredItems} />
<CustomizerControls className="sm:hidden" /> <div className="items-center gap-0 sm:hidden">
<Separator <RandomButton />
orientation="vertical" <ResetButton />
className="mr-2 hidden sm:flex xl:hidden" </div>
/> <Separator orientation="vertical" className="mr-2 flex" />
</div> </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" /> <SiteConfig className="3xl:flex hidden" />
<Separator orientation="vertical" className="3xl:flex hidden" /> <Separator orientation="vertical" className="3xl:flex hidden" />
<ModeSwitcher /> <ModeSwitcher />

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,10 +7,10 @@ import { absoluteUrl } from "@/lib/utils"
import { DarkModeScript } from "@/components/mode-switcher" import { DarkModeScript } from "@/components/mode-switcher"
import { TailwindIndicator } from "@/components/tailwind-indicator" import { TailwindIndicator } from "@/components/tailwind-indicator"
import { BASES, type Base } from "@/registry/config" import { BASES, type Base } from "@/registry/config"
import { RandomizeScript } from "@/app/(create)/components/customizer-controls"
import { DesignSystemProvider } from "@/app/(create)/components/design-system-provider" import { DesignSystemProvider } from "@/app/(create)/components/design-system-provider"
import { ItemPickerScript } from "@/app/(create)/components/item-picker" import { ItemPickerScript } from "@/app/(create)/components/item-picker"
import { PreviewStyle } from "@/app/(create)/components/preview-style" import { PreviewStyle } from "@/app/(create)/components/preview-style"
import { RandomizeScript } from "@/app/(create)/components/random-button"
import { getBaseComponent, getBaseItem } from "@/app/(create)/lib/api" import { getBaseComponent, getBaseItem } from "@/app/(create)/lib/api"
import { ALLOWED_ITEM_TYPES } from "@/app/(create)/lib/constants" import { ALLOWED_ITEM_TYPES } from "@/app/(create)/lib/constants"

View File

@@ -1,3 +1,5 @@
import { TrashIcon } from "lucide-react"
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -6,6 +8,7 @@ import {
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/registry/new-york-v4/ui/alert-dialog" } from "@/registry/new-york-v4/ui/alert-dialog"
@@ -13,23 +16,66 @@ import { Button } from "@/registry/new-york-v4/ui/button"
export function AlertDialogDemo() { export function AlertDialogDemo() {
return ( return (
<AlertDialog> <div className="flex flex-wrap gap-4">
<AlertDialogTrigger asChild> <AlertDialog>
<Button variant="outline">Show Dialog</Button> <AlertDialogTrigger asChild>
</AlertDialogTrigger> <Button variant="outline">Default</Button>
<AlertDialogContent> </AlertDialogTrigger>
<AlertDialogHeader> <AlertDialogContent>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogHeader>
<AlertDialogDescription> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
This action cannot be undone. This will permanently delete your <AlertDialogDescription>
account and remove your data from our servers. This action cannot be undone. This will permanently delete your
</AlertDialogDescription> account and remove your data from our servers.
</AlertDialogHeader> </AlertDialogDescription>
<AlertDialogFooter> </AlertDialogHeader>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogFooter>
<AlertDialogAction>Continue</AlertDialogAction> <AlertDialogCancel>Cancel</AlertDialogCancel>
</AlertDialogFooter> <AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogContent> </AlertDialogFooter>
</AlertDialog> </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 { import {
Avatar, Avatar,
AvatarBadge,
AvatarFallback, AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage, AvatarImage,
} from "@/registry/new-york-v4/ui/avatar" } from "@/registry/new-york-v4/ui/avatar"
export function AvatarDemo() { export function AvatarDemo() {
return ( return (
<div className="flex flex-row flex-wrap items-center gap-4"> <div className="flex flex-col gap-6">
<Avatar> {/* Sizes. */}
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <div className="flex flex-row flex-wrap items-center gap-4">
<AvatarFallback>CN</AvatarFallback> <Avatar size="sm">
</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>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback> <AvatarFallback>CN</AvatarFallback>
</Avatar> </Avatar>
<Avatar> <Avatar>
<AvatarImage <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
src="https://github.com/maxleiter.png" <AvatarFallback>CN</AvatarFallback>
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar> </Avatar>
<Avatar> <Avatar size="lg">
<AvatarImage <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
src="https://github.com/evilrabbit.png" <AvatarFallback>CN</AvatarFallback>
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar> </Avatar>
</div> </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"> {/* Fallback. */}
<Avatar> <div className="flex flex-row flex-wrap items-center gap-4">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <Avatar size="sm">
<AvatarFallback>CN</AvatarFallback> <AvatarFallback>CN</AvatarFallback>
</Avatar> </Avatar>
<Avatar> <Avatar>
<AvatarImage <AvatarFallback>CN</AvatarFallback>
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar> </Avatar>
<Avatar> <Avatar size="lg">
<AvatarImage <AvatarFallback>CN</AvatarFallback>
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar> </Avatar>
</div> </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> <Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" /> <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback> <AvatarFallback>CN</AvatarFallback>
<AvatarBadge />
</Avatar> </Avatar>
<Avatar> <Avatar size="lg">
<AvatarImage <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
src="https://github.com/maxleiter.png" <AvatarFallback>CN</AvatarFallback>
alt="@maxleiter" <AvatarBadge>
/> <PlusIcon />
<AvatarFallback>LR</AvatarFallback> </AvatarBadge>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar> </Avatar>
</div> </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> </div>
) )
} }

View File

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

View File

@@ -1,405 +1,183 @@
"use client" "use client"
import * as React from "react" 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 { Button } from "@/registry/new-york-v4/ui/button"
import { import {
Command, Combobox,
CommandEmpty, ComboboxChip,
CommandGroup, ComboboxChips,
CommandInput, ComboboxChipsInput,
CommandItem, ComboboxCollection,
CommandList, ComboboxContent,
CommandSeparator, ComboboxEmpty,
} from "@/registry/new-york-v4/ui/command" ComboboxGroup,
import { ComboboxInput,
Popover, ComboboxItem,
PopoverContent, ComboboxLabel,
PopoverTrigger, ComboboxList,
} from "@/registry/new-york-v4/ui/popover" ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
} from "@/registry/new-york-v4/ui/combobox"
const frameworks = [ const frameworks = [
{ "Next.js",
value: "next.js", "SvelteKit",
label: "Next.js", "Nuxt.js",
}, "Remix",
{ "Astro",
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",
},
] as const ] as const
type User = (typeof users)[number]
const timezones = [ const timezones = [
{ {
label: "Americas", value: "Americas",
timezones: [ items: ["(GMT-5) New York", "(GMT-8) Los Angeles", "(GMT-6) Chicago"],
{ 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" },
],
}, },
{ {
label: "Europe", value: "Europe",
timezones: [ items: ["(GMT+0) London", "(GMT+1) Paris", "(GMT+1) Berlin"],
{ 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" },
],
}, },
{ {
label: "Asia/Pacific", value: "Asia/Pacific",
timezones: [ items: ["(GMT+9) Tokyo", "(GMT+8) Shanghai", "(GMT+8) Singapore"],
{ 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" },
],
}, },
] as const ] 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() { export function ComboboxDemo() {
return ( return (
<div className="flex w-full flex-wrap items-start gap-4"> <div className="flex w-full flex-col gap-6">
<FrameworkCombobox frameworks={[...frameworks]} /> {/* Basic combobox. */}
<UserCombobox users={[...users]} selectedUserId={users[0].id} /> <div className="flex flex-wrap items-start gap-4">
<TimezoneCombobox <Combobox items={frameworks}>
timezones={[...timezones]} <ComboboxInput placeholder="Select a framework" />
selectedTimezone={timezones[0].timezones[0]} <ComboboxContent>
/> <ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxWithCheckbox frameworks={[...frameworks]} /> <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> </div>
) )
} }
function FrameworkCombobox({ frameworks }: { frameworks: Framework[] }) { function ComboboxMultiple() {
const [open, setOpen] = React.useState(false) const anchor = useComboboxAnchor()
const [value, setValue] = React.useState("")
return ( return (
<Popover open={open} onOpenChange={setOpen}> <div className="flex flex-wrap items-start gap-4">
<PopoverTrigger asChild> <Combobox
<Button multiple
variant="outline" autoHighlight
role="combobox" items={frameworks}
aria-expanded={open} defaultValue={[frameworks[0]]}
className="w-full justify-between md:max-w-[200px]" >
> <ComboboxChips ref={anchor}>
{value <ComboboxValue>
? frameworks.find((framework) => framework.value === value)?.label {(values) => (
: "Select framework..."} <React.Fragment>
<ChevronsUpDown className="text-muted-foreground" /> {values.map((value: string) => (
</Button> <ComboboxChip key={value}>{value}</ComboboxChip>
</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>
))} ))}
</CommandGroup> <ComboboxChipsInput placeholder="Add framework..." />
))} </React.Fragment>
<CommandSeparator className="sticky bottom-10" /> )}
<CommandGroup className="bg-popover sticky bottom-0"> </ComboboxValue>
<CommandItem> </ComboboxChips>
<PlusCircleIcon /> <ComboboxContent anchor={anchor}>
Create timezone <ComboboxEmpty>No items found.</ComboboxEmpty>
</CommandItem> <ComboboxList>
</CommandGroup> {(item) => (
</CommandList> <ComboboxItem key={item} value={item}>
</Command> {item}
</PopoverContent> </ComboboxItem>
</Popover> )}
) </ComboboxList>
} </ComboboxContent>
</Combobox>
function ComboboxWithCheckbox({ frameworks }: { frameworks: Framework[] }) { </div>
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>
) )
} }

View File

@@ -118,6 +118,7 @@ function DropdownMenuCheckboxes() {
tabler="IconUser" tabler="IconUser"
hugeicons="UserIcon" hugeicons="UserIcon"
phosphor="UserIcon" phosphor="UserIcon"
remixicon="RiUserLine"
/> />
Profile Profile
</DropdownMenuItem> </DropdownMenuItem>
@@ -127,6 +128,7 @@ function DropdownMenuCheckboxes() {
tabler="IconCreditCard" tabler="IconCreditCard"
hugeicons="CreditCardIcon" hugeicons="CreditCardIcon"
phosphor="CreditCardIcon" phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
/> />
Billing Billing
</DropdownMenuItem> </DropdownMenuItem>
@@ -136,6 +138,7 @@ function DropdownMenuCheckboxes() {
tabler="IconSettings" tabler="IconSettings"
hugeicons="SettingsIcon" hugeicons="SettingsIcon"
phosphor="GearIcon" phosphor="GearIcon"
remixicon="RiSettings3Line"
/> />
Settings Settings
</DropdownMenuItem> </DropdownMenuItem>
@@ -171,6 +174,7 @@ function DropdownMenuCheckboxes() {
tabler="IconLogout" tabler="IconLogout"
hugeicons="LogoutIcon" hugeicons="LogoutIcon"
phosphor="SignOutIcon" phosphor="SignOutIcon"
remixicon="RiLogoutBoxLine"
/> />
Sign Out Sign Out
</DropdownMenuItem> </DropdownMenuItem>
@@ -227,6 +231,7 @@ function DropdownMenuWithAvatar() {
tabler="IconChevronsUpDown" tabler="IconChevronsUpDown"
hugeicons="ChevronUpDownIcon" hugeicons="ChevronUpDownIcon"
phosphor="CaretUpDownIcon" phosphor="CaretUpDownIcon"
remixicon="RiExpandUpDownLine"
className="text-muted-foreground ml-auto" className="text-muted-foreground ml-auto"
/> />
</Button> </Button>
@@ -257,6 +262,7 @@ function DropdownMenuWithAvatar() {
tabler="IconSparkles" tabler="IconSparkles"
hugeicons="SparklesIcon" hugeicons="SparklesIcon"
phosphor="SparklesIcon" phosphor="SparklesIcon"
remixicon="RiSparklingLine"
/> />
Upgrade to Pro Upgrade to Pro
</DropdownMenuItem> </DropdownMenuItem>
@@ -269,6 +275,7 @@ function DropdownMenuWithAvatar() {
tabler="IconBadgeCheck" tabler="IconBadgeCheck"
hugeicons="BadgeCheckIcon" hugeicons="BadgeCheckIcon"
phosphor="CheckCircleIcon" phosphor="CheckCircleIcon"
remixicon="RiVerifiedBadgeLine"
/> />
Account Account
</DropdownMenuItem> </DropdownMenuItem>
@@ -278,6 +285,7 @@ function DropdownMenuWithAvatar() {
tabler="IconCreditCard" tabler="IconCreditCard"
hugeicons="CreditCardIcon" hugeicons="CreditCardIcon"
phosphor="CreditCardIcon" phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
/> />
Billing Billing
</DropdownMenuItem> </DropdownMenuItem>
@@ -287,6 +295,7 @@ function DropdownMenuWithAvatar() {
tabler="IconBell" tabler="IconBell"
hugeicons="BellIcon" hugeicons="BellIcon"
phosphor="BellIcon" phosphor="BellIcon"
remixicon="RiNotification3Line"
/> />
Notifications Notifications
</DropdownMenuItem> </DropdownMenuItem>
@@ -298,6 +307,7 @@ function DropdownMenuWithAvatar() {
tabler="IconLogout" tabler="IconLogout"
hugeicons="LogoutIcon" hugeicons="LogoutIcon"
phosphor="SignOutIcon" phosphor="SignOutIcon"
remixicon="RiLogoutBoxLine"
/> />
Sign Out Sign Out
</DropdownMenuItem> </DropdownMenuItem>
@@ -352,6 +362,7 @@ function DropdownMenuAvatarOnly() {
tabler="IconSparkles" tabler="IconSparkles"
hugeicons="SparklesIcon" hugeicons="SparklesIcon"
phosphor="SparklesIcon" phosphor="SparklesIcon"
remixicon="RiSparklingLine"
/> />
Upgrade to Pro Upgrade to Pro
</DropdownMenuItem> </DropdownMenuItem>
@@ -364,6 +375,7 @@ function DropdownMenuAvatarOnly() {
tabler="IconBadgeCheck" tabler="IconBadgeCheck"
hugeicons="BadgeCheckIcon" hugeicons="BadgeCheckIcon"
phosphor="CheckCircleIcon" phosphor="CheckCircleIcon"
remixicon="RiVerifiedBadgeLine"
/> />
Account Account
</DropdownMenuItem> </DropdownMenuItem>
@@ -373,6 +385,7 @@ function DropdownMenuAvatarOnly() {
tabler="IconCreditCard" tabler="IconCreditCard"
hugeicons="CreditCardIcon" hugeicons="CreditCardIcon"
phosphor="CreditCardIcon" phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
/> />
Billing Billing
</DropdownMenuItem> </DropdownMenuItem>
@@ -382,6 +395,7 @@ function DropdownMenuAvatarOnly() {
tabler="IconBell" tabler="IconBell"
hugeicons="BellIcon" hugeicons="BellIcon"
phosphor="BellIcon" phosphor="BellIcon"
remixicon="RiNotification3Line"
/> />
Notifications Notifications
</DropdownMenuItem> </DropdownMenuItem>
@@ -393,6 +407,7 @@ function DropdownMenuAvatarOnly() {
tabler="IconLogout" tabler="IconLogout"
hugeicons="LogoutIcon" hugeicons="LogoutIcon"
phosphor="SignOutIcon" phosphor="SignOutIcon"
remixicon="RiLogoutBoxLine"
/> />
Sign Out Sign Out
</DropdownMenuItem> </DropdownMenuItem>
@@ -411,6 +426,7 @@ function DropdownMenuIconColor() {
tabler="IconDots" tabler="IconDots"
hugeicons="MoreHorizontalCircle01Icon" hugeicons="MoreHorizontalCircle01Icon"
phosphor="DotsThreeOutlineIcon" phosphor="DotsThreeOutlineIcon"
remixicon="RiMoreLine"
/> />
<span className="sr-only">Toggle menu</span> <span className="sr-only">Toggle menu</span>
</Button> </Button>
@@ -423,6 +439,7 @@ function DropdownMenuIconColor() {
tabler="IconPencil" tabler="IconPencil"
hugeicons="EditIcon" hugeicons="EditIcon"
phosphor="PencilIcon" phosphor="PencilIcon"
remixicon="RiPencilLine"
/> />
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
@@ -432,6 +449,7 @@ function DropdownMenuIconColor() {
tabler="IconShare" tabler="IconShare"
hugeicons="ShareIcon" hugeicons="ShareIcon"
phosphor="ShareIcon" phosphor="ShareIcon"
remixicon="RiShareLine"
/> />
Share Share
</DropdownMenuItem> </DropdownMenuItem>
@@ -442,6 +460,7 @@ function DropdownMenuIconColor() {
tabler="IconTrash" tabler="IconTrash"
hugeicons="DeleteIcon" hugeicons="DeleteIcon"
phosphor="TrashIcon" phosphor="TrashIcon"
remixicon="RiDeleteBinLine"
/> />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>

View File

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

View File

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

View File

@@ -47,6 +47,27 @@ export function SheetDemo() {
</SheetFooter> </SheetFooter>
</SheetContent> </SheetContent>
</Sheet> </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"> <div className="flex gap-2">
{SHEET_SIDES.map((side) => ( {SHEET_SIDES.map((side) => (
<Sheet key={side}> <Sheet key={side}>

View File

@@ -4,6 +4,17 @@ import { Switch } from "@/registry/new-york-v4/ui/switch"
export function SwitchDemo() { export function SwitchDemo() {
return ( return (
<div className="flex flex-col gap-6"> <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"> <div className="flex items-center gap-2">
<Switch id="switch-demo-airplane-mode" /> <Switch id="switch-demo-airplane-mode" />
<Label htmlFor="switch-demo-airplane-mode">Airplane Mode</Label> <Label htmlFor="switch-demo-airplane-mode">Airplane Mode</Label>

View File

@@ -101,6 +101,45 @@ export function TabsDemo() {
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</Tabs> </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> </div>
) )
} }

View File

@@ -1,5 +1,13 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export function ComponentPreview({ children }: { children: React.ReactNode }) { export function ComponentPreview({ children }: { children: React.ReactNode }) {
return <div className={cn("bg-background")}>{children}</div> return (
<div
className={cn(
"bg-background *:data-[slot=card]:has-[[data-slot=chart]]:shadow-none"
)}
>
{children}
</div>
)
} }

View File

@@ -4,10 +4,16 @@ import { type Metadata } from "next"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { siteConfig } from "@/lib/config" import { siteConfig } from "@/lib/config"
import { getRegistryComponent, getRegistryItem } from "@/lib/registry" import {
getDemoItem,
getRegistryComponent,
getRegistryItem,
} from "@/lib/registry"
import { absoluteUrl } from "@/lib/utils" import { absoluteUrl } from "@/lib/utils"
import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles" import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles"
import "@/styles/legacy-themes.css"
import { ComponentPreview } from "./component-preview" import { ComponentPreview } from "./component-preview"
export const revalidate = false export const revalidate = false
@@ -16,7 +22,12 @@ export const dynamicParams = false
const getCachedRegistryItem = React.cache( const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => { 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)
} }
) )
@@ -73,9 +84,54 @@ export async function generateMetadata({
export async function generateStaticParams() { export async function generateStaticParams() {
const { Index } = await import("@/registry/__index__") 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 }> = [] const params: Array<{ style: string; name: string }> = []
for (const style of legacyStyles) { 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]) { if (!Index[style.name]) {
continue continue
} }

View File

@@ -9,7 +9,9 @@ import { ActiveThemeProvider } from "@/components/active-theme"
import { Analytics } from "@/components/analytics" import { Analytics } from "@/components/analytics"
import { TailwindIndicator } from "@/components/tailwind-indicator" import { TailwindIndicator } from "@/components/tailwind-indicator"
import { ThemeProvider } from "@/components/theme-provider" 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 { Toaster } from "@/registry/bases/radix/ui/sonner"
import { TooltipProvider as RadixTooltipProvider } from "@/registry/bases/radix/ui/tooltip"
import "@/styles/globals.css" import "@/styles/globals.css"
@@ -57,6 +59,11 @@ export const metadata: Metadata = {
apple: "/apple-touch-icon.png", apple: "/apple-touch-icon.png",
}, },
manifest: `${siteConfig.url}/site.webmanifest`, manifest: `${siteConfig.url}/site.webmanifest`,
alternates: {
types: {
"application/rss+xml": `${siteConfig.url}/rss.xml`,
},
},
} }
export default function RootLayout({ export default function RootLayout({
@@ -92,8 +99,12 @@ export default function RootLayout({
<LayoutProvider> <LayoutProvider>
<ActiveThemeProvider> <ActiveThemeProvider>
<NuqsAdapter> <NuqsAdapter>
{children} <BaseTooltipProvider delay={0}>
<Toaster position="top-center" /> <RadixTooltipProvider delayDuration={0}>
{children}
<Toaster position="top-center" />
</RadixTooltipProvider>
</BaseTooltipProvider>
</NuqsAdapter> </NuqsAdapter>
<TailwindIndicator /> <TailwindIndicator />
<Analytics /> <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" 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() { export function Announcement() {
return ( return (
<Badge asChild variant="secondary" className="bg-transparent"> <Badge asChild variant="secondary" className="bg-muted">
<Link href="/docs/changelog"> <Link href="/docs/changelog/2026-01-rtl">
<span className="flex size-2 rounded-full bg-blue-500" title="New" /> RTL Support <ArrowRightIcon />
npx shadcn create <ArrowRightIcon />
</Link> </Link>
</Badge> </Badge>
) )

View File

@@ -16,7 +16,7 @@ import {
Tablet, Tablet,
Terminal, Terminal,
} from "lucide-react" } from "lucide-react"
import { type ImperativePanelHandle } from "react-resizable-panels" import { type PanelImperativeHandle } from "react-resizable-panels"
import { import {
type registryItemFileSchema, type registryItemFileSchema,
type registryItemSchema, type registryItemSchema,
@@ -68,7 +68,7 @@ type BlockViewerContext = {
setView: (view: "code" | "preview") => void setView: (view: "code" | "preview") => void
activeFile: string | null activeFile: string | null
setActiveFile: (file: string) => void setActiveFile: (file: string) => void
resizablePanelRef: React.RefObject<ImperativePanelHandle | null> | null resizablePanelRef: React.RefObject<PanelImperativeHandle | null> | null
tree: ReturnType<typeof createFileTreeForRegistryItemFiles> | null tree: ReturnType<typeof createFileTreeForRegistryItemFiles> | null
highlightedFiles: highlightedFiles:
| (z.infer<typeof registryItemFileSchema> & { | (z.infer<typeof registryItemFileSchema> & {
@@ -101,7 +101,7 @@ function BlockViewerProvider({
const [activeFile, setActiveFile] = React.useState< const [activeFile, setActiveFile] = React.useState<
BlockViewerContext["activeFile"] BlockViewerContext["activeFile"]
>(highlightedFiles?.[0].target ?? null) >(highlightedFiles?.[0].target ?? null)
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null) const resizablePanelRef = React.useRef<PanelImperativeHandle>(null)
const [iframeKey, setIframeKey] = React.useState(0) const [iframeKey, setIframeKey] = React.useState(0)
return ( return (
@@ -268,19 +268,19 @@ function BlockViewerView({ styleName }: { styleName: Style["name"] }) {
<div className="relative grid w-full gap-4"> <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> <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 <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" className="after:bg-surface/50 relative z-10 after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-xl"
> >
<ResizablePanel <ResizablePanel
ref={resizablePanelRef} panelRef={resizablePanelRef}
className="bg-background relative aspect-[4/2.5] overflow-hidden rounded-lg border md:aspect-auto md:rounded-xl" className="bg-background relative aspect-[4/2.5] overflow-hidden rounded-lg border md:aspect-auto md:rounded-xl"
defaultSize={100} defaultSize="100%"
minSize={30} minSize="30%"
> >
<BlockViewerIframe styleName={styleName} /> <BlockViewerIframe styleName={styleName} />
</ResizablePanel> </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" /> <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> </ResizablePanelGroup>
</div> </div>
</div> </div>

View File

@@ -20,7 +20,7 @@ export function Callout({
<Alert <Alert
data-variant={variant} data-variant={variant}
className={cn( 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 className
)} )}
{...props} {...props}

View File

@@ -5,6 +5,7 @@ import { type z } from "zod"
import { highlightCode } from "@/lib/highlight-code" import { highlightCode } from "@/lib/highlight-code"
import { getRegistryItem } from "@/lib/registry" import { getRegistryItem } from "@/lib/registry"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChartIframe } from "@/components/chart-iframe"
import { ChartToolbar } from "@/components/chart-toolbar" import { ChartToolbar } from "@/components/chart-toolbar"
import { type Style } from "@/registry/_legacy-styles" import { type Style } from "@/registry/_legacy-styles"
@@ -12,50 +13,43 @@ export type Chart = z.infer<typeof registryItemSchema> & {
highlightedCode: string highlightedCode: string
} }
export async function ChartDisplay({ export function ChartDisplay({
name, chart,
styleName, style,
children,
className, className,
}: { }: {
name: string chart: Chart
styleName: Style["name"] style: string
} & React.ComponentProps<"div">) { } & React.ComponentProps<"div">) {
const chart = await getCachedRegistryItem(name, styleName)
const highlightedCode = await getChartHighlightedCode(
chart?.files?.[0]?.content ?? ""
)
if (!chart || !highlightedCode) {
return null
}
return ( return (
<div <div
className={cn( className={cn(
"themes-wrapper group relative flex flex-col overflow-hidden rounded-xl border transition-all duration-200 ease-in-out hover:z-30", "themes-wrapper group relative flex flex-col overflow-hidden rounded-xl transition-all duration-200 ease-in-out hover:z-30",
className className
)} )}
> >
<ChartToolbar <ChartToolbar
chart={{ ...chart, highlightedCode }} chart={chart}
className="bg-card text-card-foreground relative z-20 flex justify-end border-b px-3 py-2.5" className="relative z-20 flex justify-end px-3 py-2.5"
> />
{children} <div className="bg-background relative z-10 overflow-hidden rounded-xl">
</ChartToolbar> <ChartIframe
<div className="relative z-10 [&>div]:rounded-none [&>div]:border-none [&>div]:shadow-none"> src={`/view/${style}/${chart.name}`}
{children} height={460}
title={chart.name}
/>
</div> </div>
</div> </div>
) )
} }
const getCachedRegistryItem = React.cache( // Exported for parallel prefetching in page components.
export const getCachedRegistryItem = React.cache(
async (name: string, styleName: Style["name"]) => { async (name: string, styleName: Style["name"]) => {
return await getRegistryItem(name, styleName) return await getRegistryItem(name, styleName)
} }
) )
const getChartHighlightedCode = React.cache(async (content: string) => { export const getChartHighlightedCode = React.cache(async (content: string) => {
return await highlightCode(content) return await highlightCode(content)
}) })

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
export function ChartIframe({
src,
height,
title,
}: {
src: string
height: number
title: string
}) {
const [loaded, setLoaded] = React.useState(false)
return (
<iframe
src={src}
className={cn(
"w-full border-none transition-opacity duration-300",
loaded ? "opacity-100" : "opacity-0"
)}
height={height}
loading="lazy"
title={title}
onLoad={() => setLoaded(true)}
/>
)
}

View File

@@ -46,7 +46,7 @@ export function ChartToolbar({
} }
function ChartTitle({ chart }: { chart: Chart }) { function ChartTitle({ chart }: { chart: Chart }) {
if (chart.name.includes("charts-line")) { if (chart.name.includes("chart-line")) {
return ( return (
<> <>
<LineChartIcon /> Line Chart <LineChartIcon /> Line Chart

View File

@@ -88,7 +88,7 @@ export function CodeBlockCommand({
<TabsTrigger <TabsTrigger
key={key} key={key}
value={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} {key}
</TabsTrigger> </TabsTrigger>
@@ -113,23 +113,16 @@ export function CodeBlockCommand({
})} })}
</div> </div>
</Tabs> </Tabs>
<Tooltip> <Button
<TooltipTrigger asChild> data-slot="copy-button"
<Button size="icon"
data-slot="copy-button" variant="ghost"
size="icon" className="absolute top-2 right-2 z-10 size-7 opacity-70 hover:opacity-100 focus-visible:opacity-100"
variant="ghost" onClick={copyCommand}
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 />}
<span className="sr-only">Copy</span> </Button>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied" : "Copy to Clipboard"}
</TooltipContent>
</Tooltip>
</div> </div>
) )
} }

View File

@@ -39,7 +39,7 @@ export function CodeCollapsibleWrapper({
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent <CollapsibleContent
forceMount 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} {children}
</CollapsibleContent> </CollapsibleContent>

View File

@@ -18,7 +18,7 @@ export function CodeTabs({ children }: React.ComponentProps<typeof Tabs>) {
onValueChange={(value) => onValueChange={(value) =>
setConfig({ ...config, installationType: value as "cli" | "manual" }) 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} {children}
</Tabs> </Tabs>

View File

@@ -1,15 +1,16 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { useRouter } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import { type DialogProps } from "@radix-ui/react-dialog"
import { IconArrowRight } from "@tabler/icons-react" import { IconArrowRight } from "@tabler/icons-react"
import { useDocsSearch } from "fumadocs-core/search/client" import { useDocsSearch } from "fumadocs-core/search/client"
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react" import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { type Color, type ColorPalette } from "@/lib/colors" import { type Color, type ColorPalette } from "@/lib/colors"
import { trackEvent } from "@/lib/events" import { trackEvent } from "@/lib/events"
import { showMcpDocs } from "@/lib/flags" import { showMcpDocs } from "@/lib/flags"
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
import { type source } from "@/lib/source" import { type source } from "@/lib/source"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config" import { useConfig } from "@/hooks/use-config"
@@ -26,13 +27,13 @@ import {
} from "@/registry/new-york-v4/ui/command" } from "@/registry/new-york-v4/ui/command"
import { import {
Dialog, Dialog,
DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/registry/new-york-v4/ui/dialog" } 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 { Separator } from "@/registry/new-york-v4/ui/separator"
import { Spinner } from "@/registry/new-york-v4/ui/spinner" import { Spinner } from "@/registry/new-york-v4/ui/spinner"
@@ -42,15 +43,18 @@ export function CommandMenu({
blocks, blocks,
navItems, navItems,
...props ...props
}: DialogProps & { }: React.ComponentProps<typeof Dialog> & {
tree: typeof source.pageTree tree: typeof source.pageTree
colors: ColorPalette[] colors: ColorPalette[]
blocks?: { name: string; description: string; categories: string[] }[] blocks?: { name: string; description: string; categories: string[] }[]
navItems?: { href: string; label: string }[] navItems?: { href: string; label: string }[]
}) { }) {
const router = useRouter() const router = useRouter()
const pathname = usePathname()
const [config] = useConfig() const [config] = useConfig()
const currentBase = getCurrentBase(pathname)
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const [renderDelayedGroups, setRenderDelayedGroups] = React.useState(false)
const [selectedType, setSelectedType] = React.useState< const [selectedType, setSelectedType] = React.useState<
"color" | "page" | "component" | "block" | null "color" | "page" | "component" | "block" | null
>(null) >(null)
@@ -90,14 +94,30 @@ export function CommandMenu({
// Set new timeout to debounce both search and tracking. // Set new timeout to debounce both search and tracking.
searchTimeoutRef.current = setTimeout(() => { searchTimeoutRef.current = setTimeout(() => {
setSearch(value) React.startTransition(() => {
trackSearchQuery(value) setSearch(value)
trackSearchQuery(value)
})
}, 500) }, 500)
}, },
[setSearch, trackSearchQuery] [setSearch, trackSearchQuery]
) )
// Cleanup timeout on unmount. // Cleanup timeout on unmount.
React.useEffect(() => {
if (open) {
const frame = requestAnimationFrame(() => {
setRenderDelayedGroups(true)
})
return () => {
cancelAnimationFrame(frame)
}
}
setRenderDelayedGroups(false)
}, [open])
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
if (searchTimeoutRef.current) { 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( const handlePageHighlight = React.useCallback(
(isComponent: boolean, item: { url: string; name?: React.ReactNode }) => { (isComponent: boolean, item: { url: string; name?: React.ReactNode }) => {
if (isComponent) { if (isComponent) {
@@ -138,10 +169,175 @@ export function CommandMenu({
[setSelectedType, setCopyPayload, packageManager] [setSelectedType, setCopyPayload, packageManager]
) )
const runCommand = React.useCallback((command: () => unknown) => { const runCommand = React.useCallback(
setOpen(false) (command: () => unknown) => {
command() 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(() => { React.useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
@@ -195,39 +391,29 @@ export function CommandMenu({
<Button <Button
variant="outline" variant="outline"
className={cn( 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)} onClick={() => setOpen(true)}
{...props} {...props}
> >
<span className="hidden lg:inline-flex">Search documentation...</span> <span className="hidden lg:inline-flex">Search documentation...</span>
<span className="inline-flex lg:hidden">Search...</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> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent <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">
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"
>
<DialogHeader className="sr-only"> <DialogHeader className="sr-only">
<DialogTitle>Search documentation...</DialogTitle> <DialogTitle>Search documentation...</DialogTitle>
<DialogDescription>Search for a command to run...</DialogDescription> <DialogDescription>Search for a command to run...</DialogDescription>
</DialogHeader> </DialogHeader>
<Command <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" 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) => { filter={commandFilter}
handleSearchChange(search)
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
return 1
}
return 0
}}
> >
<div className="relative"> <div className="relative">
<CommandInput placeholder="Search documentation..." /> <CommandInput
placeholder="Search documentation..."
onValueChange={handleSearchChange}
/>
{query.isLoading && ( {query.isLoading && (
<div className="pointer-events-none absolute top-1/2 right-3 z-10 flex -translate-y-1/2 items-center justify-center"> <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" /> <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"> <CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
{query.isLoading ? "Searching..." : "No results found."} {query.isLoading ? "Searching..." : "No results found."}
</CommandEmpty> </CommandEmpty>
{navItems && navItems.length > 0 && ( {navItemsSection}
<CommandGroup {renderDelayedGroups ? (
heading="Pages" <>
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1" {pageGroupsSection}
> {colorGroupsSection}
{navItems.map((item) => ( {blocksSection}
<CommandMenuItem <SearchResults
key={item.href} setOpen={setOpen}
value={`Navigation ${item.label}`} query={query}
keywords={["nav", "navigation", item.label.toLowerCase()]} search={search}
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>
) : null} ) : null}
<SearchResults
open={open}
setOpen={setOpen}
query={query}
search={search}
/>
</CommandList> </CommandList>
</Command> </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"> <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, query,
search, search,
}: { }: {
open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
query: Query query: Query
search: string search: string
}) { }) {
const router = useRouter() const router = useRouter()
const uniqueResults = const uniqueResults = React.useMemo(() => {
query.data && Array.isArray(query.data) if (!query.data || !Array.isArray(query.data)) {
? query.data.filter( return []
(item, index, self) => }
!(
item.type === "text" && return query.data.filter(
item.content.trim().split(/\s+/).length <= 1 (item, index, self) =>
) && index === self.findIndex((t) => t.content === item.content) !(
) item.type === "text" && item.content.trim().split(/\s+/).length <= 1
: [] ) && index === self.findIndex((t) => t.content === item.content)
)
}, [query.data])
if (!search.trim()) { if (!search.trim()) {
return null return null
@@ -523,3 +578,27 @@ function SearchResults({
</CommandGroup> </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-1/3 left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-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" "use client"
import * as React from "react" 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 { 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({ export function ComponentPreviewTabs({
className, className,
previewClassName,
align = "center", align = "center",
hideCode = false, hideCode = false,
chromeLessOnMobile = false, chromeLessOnMobile = false,
component, component,
source, source,
sourcePreview,
direction = "ltr",
styleName,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
previewClassName?: string
align?: "center" | "start" | "end" align?: "center" | "start" | "end"
hideCode?: boolean hideCode?: boolean
chromeLessOnMobile?: boolean chromeLessOnMobile?: boolean
component: React.ReactNode component: React.ReactNode
source: 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 ( return (
<div <div
data-slot="component-preview"
className={cn( 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 className
)} )}
{...props} {...props}
> >
<div data-slot="preview"> {direction === "rtl" ? (
<div <LanguageProvider defaultLanguage="ar">
data-align={align} <div className="flex h-16 items-center border-b px-4">
className={cn( <RtlLanguageSelector />
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start", <Popover>
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10" <PopoverTrigger
)} render={
> <Button
{component} variant="ghost"
</div> size="icon-sm"
{!hideCode && ( className="ml-auto size-7"
<div >
data-slot="code" <IconAlertCircle />
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]" <span className="sr-only">Toggle</span>
> </Button>
{source} }
></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> </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>
</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 Image from "next/image"
import { getRegistryComponent } from "@/lib/registry"
import { ComponentPreviewTabs } from "@/components/component-preview-tabs" import { ComponentPreviewTabs } from "@/components/component-preview-tabs"
import { ComponentSource } from "@/components/component-source" import { ComponentSource } from "@/components/component-source"
import { Index } from "@/registry/__index__"
import { type Style } from "@/registry/_legacy-styles"
export function ComponentPreview({ export function ComponentPreview({
name, name,
styleName = "new-york-v4",
type, type,
className, className,
previewClassName,
align = "center", align = "center",
hideCode = false, hideCode = false,
chromeLessOnMobile = false, chromeLessOnMobile = false,
styleName = "new-york-v4",
direction = "ltr",
caption,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
name: string name: string
styleName?: Style["name"] styleName?: string
align?: "center" | "start" | "end" align?: "center" | "start" | "end"
description?: string description?: string
hideCode?: boolean hideCode?: boolean
type?: "block" | "component" | "example" type?: "block" | "component" | "example"
chromeLessOnMobile?: boolean 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") { if (type === "block") {
return ( const content = (
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1"> <div className="relative mt-6 aspect-[4/2.5] w-full overflow-hidden rounded-xl border md:-mx-1">
<Image <Image
src={`/r/styles/new-york-v4/${name}-light.png`} src={`/r/styles/new-york-v4/${name}-light.png`}
alt={name} alt={name}
@@ -59,14 +51,42 @@ export function ComponentPreview({
</div> </div>
</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 <ComponentPreviewTabs
className={className} className={className}
previewClassName={previewClassName}
align={align} align={align}
hideCode={hideCode} hideCode={hideCode}
component={<Component />} component={React.createElement(Component)}
source={ source={
<ComponentSource <ComponentSource
name={name} name={name}
@@ -74,8 +94,34 @@ export function ComponentPreview({
styleName={styleName} styleName={styleName}
/> />
} }
sourcePreview={
<ComponentSource
name={name}
collapsible={false}
styleName={styleName}
maxLines={3}
/>
}
chromeLessOnMobile={chromeLessOnMobile} chromeLessOnMobile={chromeLessOnMobile}
direction={direction}
styleName={styleName}
{...props} {...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 * as React from "react"
import { highlightCode } from "@/lib/highlight-code" 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 { cn } from "@/lib/utils"
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper" import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
import { CopyButton } from "@/components/copy-button" import { CopyButton } from "@/components/copy-button"
import { getIconForLanguageExtension } from "@/components/icons" import { getIconForLanguageExtension } from "@/components/icons"
import { type Style } from "@/registry/_legacy-styles"
export async function ComponentSource({ export async function ComponentSource({
name, name,
@@ -18,13 +18,15 @@ export async function ComponentSource({
collapsible = true, collapsible = true,
className, className,
styleName = "new-york-v4", styleName = "new-york-v4",
maxLines,
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
name?: string name?: string
src?: string src?: string
title?: string title?: string
language?: string language?: string
collapsible?: boolean collapsible?: boolean
styleName?: Style["name"] styleName?: string
maxLines?: number
}) { }) {
if (!name && !src) { if (!name && !src) {
return null return null
@@ -33,7 +35,9 @@ export async function ComponentSource({
let code: string | undefined let code: string | undefined
if (name) { if (name) {
const item = await getRegistryItem(name, styleName) const item =
(await getDemoItem(name, styleName)) ??
(await getRegistryItem(name, styleName))
code = item?.files?.[0]?.content code = item?.files?.[0]?.content
} }
@@ -46,14 +50,14 @@ export async function ComponentSource({
return null return null
} }
// Fix imports. code = await formatCode(code, styleName)
// Replace @/registry/${style}/ with @/components/.
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
// Replace export default with export.
code = code.replaceAll("export default", "export")
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "") 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 lang = language ?? title?.split(".").pop() ?? "tsx"
const highlightedCode = await highlightCode(code, lang) const highlightedCode = await highlightCode(code, lang)

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