Compare commits

...

98 Commits

Author SHA1 Message Date
shadcn
ff1586b733 perf: optimize build-registry script for ~3.4x faster builds
- Add parallel style building with concurrency limiter (6 concurrent)
- Cache registry imports to avoid duplicate validations
- Pre-load all style maps in parallel
- Process files within each style concurrently
- Batch prettier formatting into single call at end
- Parallelize cleanup of temp files and directories
- Parallelize RTL example generation
- Add build timing output

Reduces build time from ~33s to ~10s.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 10:59:36 +04:00
shadcn
858388d64a fix 2026-01-25 10:34:00 +04:00
shadcn
e27b6838ee fix 2026-01-23 22:12:33 +04:00
shadcn
4c58d7701a Merge branch 'main' into shadcn/rtl
# Conflicts:
#	apps/v4/examples/base/ui/combobox.tsx
#	apps/v4/examples/base/ui/context-menu.tsx
#	apps/v4/examples/base/ui/dropdown-menu.tsx
#	apps/v4/examples/base/ui/hover-card.tsx
#	apps/v4/examples/base/ui/menubar.tsx
#	apps/v4/examples/base/ui/popover.tsx
#	apps/v4/examples/base/ui/select.tsx
#	apps/v4/examples/base/ui/tooltip.tsx
#	apps/v4/examples/radix/ui/menubar.tsx
#	apps/v4/examples/radix/ui/select.tsx
#	apps/v4/public/r/styles/base-lyra/context-menu.json
#	apps/v4/public/r/styles/base-lyra/dropdown-menu.json
#	apps/v4/public/r/styles/base-lyra/hover-card.json
#	apps/v4/public/r/styles/base-lyra/popover.json
#	apps/v4/public/r/styles/base-lyra/tooltip-example.json
#	apps/v4/public/r/styles/base-lyra/tooltip.json
#	apps/v4/public/r/styles/base-maia/context-menu.json
#	apps/v4/public/r/styles/base-maia/dropdown-menu.json
#	apps/v4/public/r/styles/base-maia/hover-card.json
#	apps/v4/public/r/styles/base-maia/popover.json
#	apps/v4/public/r/styles/base-maia/tooltip-example.json
#	apps/v4/public/r/styles/base-maia/tooltip.json
#	apps/v4/public/r/styles/base-mira/context-menu.json
#	apps/v4/public/r/styles/base-mira/dropdown-menu.json
#	apps/v4/public/r/styles/base-mira/hover-card.json
#	apps/v4/public/r/styles/base-mira/popover.json
#	apps/v4/public/r/styles/base-mira/tooltip-example.json
#	apps/v4/public/r/styles/base-mira/tooltip.json
#	apps/v4/public/r/styles/base-nova/context-menu.json
#	apps/v4/public/r/styles/base-nova/dropdown-menu.json
#	apps/v4/public/r/styles/base-nova/hover-card.json
#	apps/v4/public/r/styles/base-nova/popover.json
#	apps/v4/public/r/styles/base-nova/tooltip-example.json
#	apps/v4/public/r/styles/base-nova/tooltip.json
#	apps/v4/public/r/styles/base-vega/context-menu.json
#	apps/v4/public/r/styles/base-vega/dropdown-menu.json
#	apps/v4/public/r/styles/base-vega/hover-card.json
#	apps/v4/public/r/styles/base-vega/popover.json
#	apps/v4/public/r/styles/base-vega/tooltip-example.json
#	apps/v4/public/r/styles/base-vega/tooltip.json
#	apps/v4/public/r/styles/radix-mira/menubar.json
#	apps/v4/public/r/styles/radix-nova/menubar.json
#	apps/v4/registry/bases/base/examples/tooltip-example.tsx
#	apps/v4/registry/bases/base/ui/hover-card.tsx
#	apps/v4/registry/bases/base/ui/popover.tsx
#	apps/v4/registry/bases/base/ui/tooltip.tsx
2026-01-23 21:15:31 +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
shadcn
265953771f fix 2026-01-23 11:13:54 +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
6f0abe445a Merge branch 'main' into shadcn/rtl
# Conflicts:
#	apps/v4/examples/base/ui/sidebar.tsx
#	apps/v4/examples/radix/ui/sidebar.tsx
#	apps/v4/public/r/styles/base-lyra/sidebar.json
#	apps/v4/public/r/styles/base-maia/sidebar.json
#	apps/v4/public/r/styles/base-mira/sidebar.json
#	apps/v4/public/r/styles/base-nova/sidebar.json
#	apps/v4/public/r/styles/base-vega/sidebar.json
#	apps/v4/public/r/styles/radix-lyra/sidebar.json
#	apps/v4/public/r/styles/radix-maia/sidebar.json
#	apps/v4/public/r/styles/radix-mira/sidebar.json
#	apps/v4/public/r/styles/radix-nova/sidebar.json
#	apps/v4/public/r/styles/radix-vega/sidebar.json
2026-01-22 23:58:56 +04:00
shadcn
6cb2a1fd65 fix: sidebar 2026-01-22 23:55:06 +04:00
shadcn
5cd0fab0f3 fix 2026-01-22 23:52:02 +04:00
shadcn
ee7c7f4558 Merge branch 'main' into shadcn/rtl
# Conflicts:
#	apps/v4/components/component-preview-tabs.tsx
2026-01-22 22:08:02 +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
aff6e1ab47 fix 2026-01-22 09:58:18 +04:00
shadcn
5317ae8a70 feat 2026-01-21 13:24:09 +04:00
shadcn
7513f0f894 feat: rtl 2026-01-21 12:30:45 +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
Devon Govett
ccafdaf7c6 Add React Aria registry (#9121) 2025-12-19 02:22:27 +04:00
github-actions[bot]
f0d147d581 chore(release): version packages (#9125)
* chore(release): version packages

* chore: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-12-17 22:07:42 +04:00
shadcn
df67e49aac chore: add create command to readme (#9126) 2025-12-17 22:02:28 +04:00
shadcn
c0de90e1a1 ci: update permissions 2025-12-17 21:57:07 +04:00
shadcn
0447708efa Merge branch 'main' of github.com:shadcn-ui/ui 2025-12-17 21:52:50 +04:00
shadcn
4a470fc617 ci: update release 2025-12-17 21:52:29 +04:00
Dominik K.
137b1c12b7 feat(ui): add support for phosphor icons (#9044)
* feat: add phosphor icons to base ui

* feat_ add phosphor to blocks

* feat: add phosphor to radix blocks

* feat: add phosphor to radix ui

* feat: add phosphor to radix example

* feat: add missing phosphor icons

* fix: rename broken icons

* chore: format files

* fix: add missing phosphor icons

* chore: build registry

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-17 21:36:46 +04:00
shadcn
73296e79c0 ci: switch to oidc 2025-12-17 21:31:02 +04:00
Hamed.dev
78e5fa2a39 fix(mira): combobox popup background in dark mode (#9039)
* fix(mira): combobox popup background in dark mode

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-17 21:10:40 +04:00
François Best
9cf47dd4a3 fix: preview iframe URL state sync (#9095)
* fix: preview iframe URL state sync

* fix: reset iframe on base change
2025-12-17 21:09:14 +04:00
shadcn
f53400f934 chore: add livekit to directory (#9109) 2025-12-16 19:47:47 +04:00
Brendan Dash
b3d6f872db fix: remove duplicate @ui-layouts entry from registries.json (#9106) 2025-12-16 19:44:40 +04:00
Zaid Mukaddam
2aa5e11f6f Add "Open in Scira" in Copy menu item (#8013)
* Add "Open in Scira" in Copy menu item

* Fix link
2025-12-16 19:43:37 +04:00
Luka Klacar
058ebc5acd Remove duplicate @einui entry from directory.json (#9091)
Removed entry for @einui from the directory.
2025-12-16 03:24:24 +04:00
shadcn
a60683dea5 fix 2025-12-15 15:49:35 +04:00
shadcn
1dc1b8dbfb chore: remove console 2025-12-15 15:49:02 +04:00
shadcn
c6273cca03 fix: hover bug for pickers (#9084)
* fix

* fix
2025-12-15 15:46:16 +04:00
Gravityy
b15d7e8221 feat : add animbits to trusted registries (#8910)
* Add @animbits registry URL to registries.json and directory.json

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

* fix

---------

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

* fix

---------

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

Resolves: #9048

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

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

* fix: select for base

---------

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

* chore: remove changeset

---------

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

* ref: expose useDesignSystemSearchParams hook

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

* ref: remove unused code

* fix: params.size is non-nullable

* ref: move items shallow option to the parser level

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-12-15 13:19:32 +04:00
2871 changed files with 120062 additions and 15156 deletions

View File

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

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

@@ -7,6 +7,11 @@ on:
branches:
- main
permissions:
id-token: write
contents: write
pull-requests: write
jobs:
release:
if: ${{ github.repository_owner == 'shadcn-ui' }}
@@ -24,12 +29,15 @@ jobs:
version: 9.0.6
- name: Use Node.js 20
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
version: 9.0.6
node-version: 20
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Install NPM Dependencies
run: pnpm install
@@ -49,5 +57,4 @@ jobs:
publish: npx changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
NODE_ENV: "production"

View File

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

View File

@@ -1,20 +1,8 @@
"use client"
import * as React from "react"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
import { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
@@ -27,7 +15,18 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
} from "@/examples/radix/ui/dropdown-menu"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
export function ButtonGroupDemo() {
const [label, setLabel] = React.useState("personal")

View File

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

View File

@@ -1,10 +1,9 @@
"use client"
import { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
export function ButtonGroupNested() {
return (
<ButtonGroup>

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { Button } from "@/examples/radix/ui/button"
import { Checkbox } from "@/examples/radix/ui/checkbox"
import {
Field,
FieldDescription,
@@ -8,16 +8,16 @@ import {
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/registry/new-york-v4/ui/field"
import { Input } from "@/registry/new-york-v4/ui/input"
} from "@/examples/radix/ui/field"
import { Input } from "@/examples/radix/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
} from "@/examples/radix/ui/select"
import { Textarea } from "@/examples/radix/ui/textarea"
export function FieldDemo() {
return (

View File

@@ -1,5 +1,5 @@
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
import { Card, CardContent } from "@/examples/radix/ui/card"
import { Checkbox } from "@/examples/radix/ui/checkbox"
import {
Field,
FieldDescription,
@@ -8,7 +8,7 @@ import {
FieldLegend,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
} from "@/examples/radix/ui/field"
const options = [
{

View File

@@ -1,13 +1,8 @@
"use client"
import { useState } from "react"
import {
Field,
FieldDescription,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import { Slider } from "@/registry/new-york-v4/ui/slider"
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
import { Slider } from "@/examples/radix/ui/slider"
export function FieldSlider() {
const [value, setValue] = useState([200, 800])

View File

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

View File

@@ -1,20 +1,19 @@
"use client"
import * as React from "react"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Label } from "@/registry/new-york-v4/ui/label"
} from "@/examples/radix/ui/input-group"
import { Label } from "@/examples/radix/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/registry/new-york-v4/ui/popover"
} from "@/examples/radix/ui/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
export function InputGroupButtonExample() {
const [isFavorite, setIsFavorite] = React.useState(false)

View File

@@ -1,12 +1,9 @@
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
} from "@/examples/radix/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
@@ -14,13 +11,15 @@ import {
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
import { Separator } from "@/registry/new-york-v4/ui/separator"
} from "@/examples/radix/ui/input-group"
import { Separator } from "@/examples/radix/ui/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
} from "@/examples/radix/ui/tooltip"
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
export function InputGroupDemo() {
return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,11 @@ import * as React from "react"
import { notFound } from "next/navigation"
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 { charts } from "@/app/(app)/charts/charts"
@@ -44,6 +48,26 @@ export default async function ChartPage({ params }: ChartPageProps) {
const chartList = charts[chartType]
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 (
<div className="grid flex-1 gap-12 lg:gap-24">
<h2 className="sr-only">
@@ -51,16 +75,14 @@ export default async function ChartPage({ params }: ChartPageProps) {
</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">
{Array.from({ length: 12 }).map((_, index) => {
const chart = chartList[index]
const chart = prefetchedCharts[index]
return chart ? (
<ChartDisplay
key={chart.id}
name={chart.id}
styleName={activeStyle.name}
key={chart.name}
chart={chart}
style={activeStyle.name}
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
>
<chart.component />
</ChartDisplay>
/>
) : (
<div
key={`empty-${index}`}

View File

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

View File

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

View File

@@ -0,0 +1,140 @@
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>
<ul className="flex flex-col gap-4">
{olderPages.map((page) => {
const data = page.data as ChangelogPageData
return (
<li key={page.url} className="flex items-center gap-3">
<Link
href={page.url}
className="font-medium hover:underline"
>
{data.title}
</Link>
</li>
)
})}
</ul>
</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,10 @@ export default function DocsLayout({
}) {
return (
<div className="container-wrapper flex flex-1 flex-col px-2">
<SidebarProvider className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--sidebar-width:220px] [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--sidebar-width:240px] lg:[--top-spacing:calc(var(--spacing)*4)]">
<SidebarProvider
className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--top-spacing:calc(var(--spacing)*4)]"
style={{ "--sidebar-width": "220px" } as React.CSSProperties}
>
<DocsSidebar tree={source.pageTree} />
<div className="h-full w-full">{children}</div>
</SidebarProvider>

View File

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

View File

@@ -1,7 +1,5 @@
"use client"
import { useQueryStates } from "nuqs"
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
@@ -12,7 +10,7 @@ import {
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function MenuAccentPicker({
isMobile,
@@ -21,18 +19,15 @@ export function MenuAccentPicker({
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const currentAccent = MENU_ACCENTS.find(
(accent) => accent.value === params.menuAccent
)
return (
<Picker>
<div className="group/picker relative">
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Menu Accent</div>
@@ -40,7 +35,7 @@ export function MenuAccentPicker({
{currentAccent?.label}
</div>
</div>
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
@@ -70,31 +65,31 @@ export function MenuAccentPicker({
</svg>
</div>
</PickerTrigger>
<LockButton
param="menuAccent"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentAccent?.value}
onValueChange={(value) => {
setParams({ menuAccent: value as MenuAccentValue })
}}
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerGroup>
{MENU_ACCENTS.map((accent) => (
<PickerRadioItem key={accent.value} value={accent.value}>
{accent.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<PickerRadioGroup
value={currentAccent?.value}
onValueChange={(value) => {
setParams({ menuAccent: value as MenuAccentValue })
}}
>
<PickerGroup>
{MENU_ACCENTS.map((accent) => (
<PickerRadioItem key={accent.value} value={accent.value}>
{accent.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton
param="menuAccent"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
)
}

View File

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

View File

@@ -1,7 +1,6 @@
"use client"
import * as React from "react"
import { useQueryStates } from "nuqs"
import { BASES } from "@/registry/config"
import {
@@ -12,7 +11,7 @@ import {
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function BasePicker({
isMobile,
@@ -21,10 +20,7 @@ export function BasePicker({
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const currentBase = React.useMemo(
() => BASES.find((base) => base.name === params.base),
@@ -54,7 +50,7 @@ export function BasePicker({
</div>
{currentBase?.meta?.logo && (
<div
className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 size-4 -translate-y-1/2 *:[svg]:size-4"
className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 select-none *:[svg]:size-4"
dangerouslySetInnerHTML={{
__html: currentBase.meta.logo,
}}

View File

@@ -3,27 +3,29 @@
import * as React from "react"
import { Settings05Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useQueryStates } from "nuqs"
import { useIsMobile } from "@/hooks/use-mobile"
import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config"
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 { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
import { BasePicker } from "@/app/(create)/components/base-picker"
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
import { DirectionPicker } from "@/app/(create)/components/direction-picker"
import { FontPicker } from "@/app/(create)/components/font-picker"
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
import { PresetPicker } from "@/app/(create)/components/preset-picker"
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
import { RandomButton } from "@/app/(create)/components/random-button"
import { ResetButton } from "@/app/(create)/components/reset-button"
import { StylePicker } from "@/app/(create)/components/style-picker"
import { ThemePicker } from "@/app/(create)/components/theme-picker"
import { FONTS } from "@/app/(create)/lib/fonts"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function Customizer() {
const [params] = useQueryStates(designSystemSearchParams)
const [params] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const anchorRef = React.useRef<HTMLDivElement | null>(null)
@@ -37,7 +39,7 @@ export function Customizer() {
className="no-scrollbar -mx-2.5 flex flex-col overflow-y-auto p-1 md:mx-0 md:h-[calc(100svh-var(--header-height)-2rem)] md:w-48 md:gap-0 md:py-0"
ref={anchorRef}
>
<div className="hidden items-center gap-2 px-[calc(--spacing(2.5))] pb-1 md:flex md:flex-col md:items-start">
<div className="--md:flex hidden items-center gap-2 px-[calc(--spacing(2.5))] pb-1 md:flex-col md:items-start">
<HugeiconsIcon
icon={Settings05Icon}
className="size-4"
@@ -74,9 +76,13 @@ export function Customizer() {
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
<DirectionPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
<CustomizerControls className="mt-auto hidden w-full flex-col md:flex" />
<div className="mt-auto hidden w-full flex-col items-center gap-0 md:flex">
<RandomButton />
<ResetButton />
</div>
</FieldGroup>
</div>
</div>

View File

@@ -7,21 +7,23 @@ import {
DEFAULT_CONFIG,
type DesignSystemConfig,
} from "@/registry/config"
import { useDesignSystemParam } from "@/app/(create)/hooks/use-design-system"
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
import { FONTS } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function DesignSystemProvider({
children,
}: {
children: React.ReactNode
}) {
const style = useDesignSystemParam("style")
const theme = useDesignSystemParam("theme")
const font = useDesignSystemParam("font")
const baseColor = useDesignSystemParam("baseColor")
const menuAccent = useDesignSystemParam("menuAccent")
const menuColor = useDesignSystemParam("menuColor")
const radius = useDesignSystemParam("radius")
const [
{ style, theme, font, baseColor, direction, menuAccent, menuColor, radius },
setSearchParams,
] = useDesignSystemSearchParams({
shallow: true, // No need to go through the server…
history: "replace", // …or push updates into the iframe history.
})
useIframeMessageListener("design-system-params", setSearchParams)
const [isReady, setIsReady] = React.useState(false)
// Use useLayoutEffect for synchronous style updates to prevent flash.
@@ -164,6 +166,13 @@ export function DesignSystemProvider({
}
}, [menuColor])
// Set dir attribute on html element when direction changes.
React.useLayoutEffect(() => {
if (direction) {
document.documentElement.dir = direction
}
}, [direction])
if (!isReady) {
return null
}

View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import { type DirectionValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const DIRECTION_OPTIONS = [
{
value: "ltr" as const,
label: "LTR",
description: "Left-to-Right",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
role="img"
stroke="currentColor"
className="text-foreground"
>
<path
d="M3 12h18M15 6l6 6-6 6"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
{
value: "rtl" as const,
label: "RTL",
description: "Right-to-Left",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
role="img"
stroke="currentColor"
className="text-foreground"
>
<path
d="M21 12H3M9 18l-6-6 6-6"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
] as const
export function DirectionPicker({
isMobile,
anchorRef,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentDirection = DIRECTION_OPTIONS.find(
(direction) => direction.value === params.direction
)
return (
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Direction</div>
<div className="text-foreground text-sm font-medium">
{currentDirection?.label}
</div>
</div>
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
{currentDirection?.icon}
</div>
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentDirection?.value}
onValueChange={(value) => {
setParams({ direction: value as DirectionValue })
}}
>
<PickerGroup>
{DIRECTION_OPTIONS.map((direction) => (
<PickerRadioItem key={direction.value} value={direction.value}>
{direction.icon}
{direction.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton
param="direction"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -1,7 +1,6 @@
"use client"
import * as React from "react"
import { useQueryStates } from "nuqs"
import {
Item,
@@ -21,7 +20,7 @@ import {
PickerTrigger,
} from "@/app/(create)/components/picker"
import { type Font } from "@/app/(create)/lib/fonts"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function FontPicker({
fonts,
@@ -32,10 +31,7 @@ export function FontPicker({
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const currentFont = React.useMemo(
() => fonts.find((font) => font.value === params.font),
@@ -43,8 +39,8 @@ export function FontPicker({
)
return (
<Picker>
<div className="group/picker relative">
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Font</div>
@@ -53,54 +49,55 @@ export function FontPicker({
</div>
</div>
<div
className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base"
className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none"
style={{ fontFamily: currentFont?.font.style.fontFamily }}
>
Aa
</div>
</PickerTrigger>
<LockButton
param="font"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-80 md:w-72"
>
<PickerRadioGroup
value={currentFont?.value}
onValueChange={(value) => {
setParams({ font: value as FontValue })
}}
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-80 md:w-72"
>
<PickerGroup>
{fonts.map((font, index) => (
<React.Fragment key={font.value}>
<PickerRadioItem value={font.value}>
<Item size="xs">
<ItemContent className="gap-1">
<ItemTitle className="text-muted-foreground text-xs font-medium">
{font.name}
</ItemTitle>
<ItemDescription
style={{ fontFamily: font.font.style.fontFamily }}
>
Designers love packing quirky glyphs into test phrases.
</ItemDescription>
</ItemContent>
</Item>
</PickerRadioItem>
{index < fonts.length - 1 && (
<PickerSeparator className="opacity-50" />
)}
</React.Fragment>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<PickerRadioGroup
value={currentFont?.value}
onValueChange={(value) => {
setParams({ font: value as FontValue })
}}
>
<PickerGroup>
{fonts.map((font, index) => (
<React.Fragment key={font.value}>
<PickerRadioItem value={font.value}>
<Item size="xs">
<ItemContent className="gap-1">
<ItemTitle className="text-muted-foreground text-xs font-medium">
{font.name}
</ItemTitle>
<ItemDescription
style={{ fontFamily: font.font.style.fontFamily }}
>
Designers love packing quirky glyphs into test
phrases.
</ItemDescription>
</ItemContent>
</Item>
</PickerRadioItem>
{index < fonts.length - 1 && (
<PickerSeparator className="opacity-50" />
)}
</React.Fragment>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton
param="font"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -2,7 +2,6 @@
import * as React from "react"
import { lazy, memo, Suspense } from "react"
import { useQueryStates } from "nuqs"
import { Item, ItemContent, ItemTitle } from "@/registry/bases/radix/ui/item"
import {
@@ -20,7 +19,7 @@ import {
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const IconLucide = lazy(() =>
import("@/registry/icons/icon-lucide").then((mod) => ({
@@ -40,6 +39,18 @@ const IconHugeicons = lazy(() =>
}))
)
const IconPhosphor = lazy(() =>
import("@/registry/icons/icon-phosphor").then((mod) => ({
default: mod.IconPhosphor,
}))
)
const IconRemixicon = lazy(() =>
import("@/registry/icons/icon-remixicon").then((mod) => ({
default: mod.IconRemixicon,
}))
)
const PREVIEW_ICONS = {
lucide: [
"CopyIcon",
@@ -79,7 +90,7 @@ const PREVIEW_ICONS = {
"Delete02Icon",
"Share03Icon",
"ShoppingBag01Icon",
"MoreHorizontalIcon",
"MoreHorizontalCircle01Icon",
"Loading03Icon",
"PlusSignIcon",
"MinusSignIcon",
@@ -89,6 +100,38 @@ const PREVIEW_ICONS = {
"ArrowDown01Icon",
"ArrowRight01Icon",
],
phosphor: [
"CopyIcon",
"WarningCircleIcon",
"TrashIcon",
"ShareIcon",
"BagIcon",
"DotsThreeIcon",
"SpinnerIcon",
"PlusIcon",
"MinusIcon",
"ArrowLeftIcon",
"ArrowRightIcon",
"CheckIcon",
"CaretDownIcon",
"CaretRightIcon",
],
remixicon: [
"RiFileCopyLine",
"RiErrorWarningLine",
"RiDeleteBinLine",
"RiShareLine",
"RiShoppingBagLine",
"RiMoreLine",
"RiLoaderLine",
"RiAddLine",
"RiSubtractLine",
"RiArrowLeftLine",
"RiArrowRightLine",
"RiCheckLine",
"RiArrowDownSLine",
"RiArrowRightSLine",
],
}
const logos = {
@@ -155,6 +198,35 @@ const logos = {
></path>
</svg>
),
phosphor: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
width="32"
height="32"
>
<path fill="none" d="M0 0h32v32H0z" />
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5h9v16H9zm9 16v9a9 9 0 0 1-9-9M9 5l9 16m0 0h1a8 8 0 0 0 0-16h-1"
/>
</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({
@@ -164,10 +236,7 @@ export function IconLibraryPicker({
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const currentIconLibrary = React.useMemo(
() => iconLibraries[params.iconLibrary as keyof typeof iconLibraries],
@@ -175,8 +244,8 @@ export function IconLibraryPicker({
)
return (
<Picker>
<div className="group/picker relative">
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Icon Library</div>
@@ -184,42 +253,42 @@ export function IconLibraryPicker({
{currentIconLibrary?.title}
</div>
</div>
<div className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
<div className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
{logos[currentIconLibrary?.name as keyof typeof logos]}
</div>
</PickerTrigger>
<LockButton
param="iconLibrary"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentIconLibrary?.name}
onValueChange={(value) => {
setParams({ iconLibrary: value as IconLibraryName })
}}
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerGroup>
{Object.values(iconLibraries).map((iconLibrary, index) => (
<React.Fragment key={iconLibrary.name}>
<IconLibraryPickerItem
iconLibrary={iconLibrary}
value={iconLibrary.name}
/>
{index < Object.values(iconLibraries).length - 1 && (
<PickerSeparator className="opacity-50" />
)}
</React.Fragment>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<PickerRadioGroup
value={currentIconLibrary?.name}
onValueChange={(value) => {
setParams({ iconLibrary: value as IconLibraryName })
}}
>
<PickerGroup>
{Object.values(iconLibraries).map((iconLibrary, index) => (
<React.Fragment key={iconLibrary.name}>
<IconLibraryPickerItem
iconLibrary={iconLibrary}
value={iconLibrary.name}
/>
{index < Object.values(iconLibraries).length - 1 && (
<PickerSeparator className="opacity-50" />
)}
</React.Fragment>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton
param="iconLibrary"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
)
}
@@ -263,7 +332,11 @@ const IconLibraryPreview = memo(function IconLibraryPreview({
? IconLucide
: iconLibrary === "tabler"
? IconTabler
: IconHugeicons
: iconLibrary === "hugeicons"
? IconHugeicons
: iconLibrary === "phosphor"
? IconPhosphor
: IconRemixicon
return (
<Suspense

View File

@@ -4,7 +4,7 @@ import { lazy, Suspense } from "react"
import { SquareIcon } from "lucide-react"
import type { IconLibraryName } from "shadcn/icons"
import { useDesignSystemParam } from "@/app/(create)/hooks/use-design-system"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const IconLucide = lazy(() =>
import("@/registry/icons/icon-lucide").then((mod) => ({
@@ -24,12 +24,24 @@ const IconHugeicons = lazy(() =>
}))
)
const IconPhosphor = lazy(() =>
import("@/registry/icons/icon-phosphor").then((mod) => ({
default: mod.IconPhosphor,
}))
)
const IconRemixicon = lazy(() =>
import("@/registry/icons/icon-remixicon").then((mod) => ({
default: mod.IconRemixicon,
}))
)
export function IconPlaceholder({
...props
}: {
[K in IconLibraryName]: string
} & React.ComponentProps<"svg">) {
const iconLibrary = useDesignSystemParam("iconLibrary")
const [{ iconLibrary }] = useDesignSystemSearchParams()
const iconName = props[iconLibrary]
if (!iconName) {
@@ -43,6 +55,12 @@ export function IconPlaceholder({
{iconLibrary === "hugeicons" && (
<IconHugeicons name={iconName} {...props} />
)}
{iconLibrary === "phosphor" && (
<IconPhosphor name={iconName} {...props} />
)}
{iconLibrary === "remixicon" && (
<IconRemixicon name={iconName} {...props} />
)}
</Suspense>
)
}

View File

@@ -3,7 +3,6 @@
import * as React from "react"
import Link from "next/link"
import { ChevronRightIcon } from "lucide-react"
import { useQueryStates } from "nuqs"
import { type RegistryItem } from "shadcn/schema"
import { cn } from "@/lib/utils"
@@ -22,7 +21,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(create)/lib/utils"
const cachedGroupedItems = React.cache(
@@ -38,10 +37,7 @@ export function ItemExplorer({
base: Base["name"]
items: Pick<RegistryItem, "name" | "title" | "type">[]
}) {
const [params, setParams] = useQueryStates(designSystemSearchParams, {
history: "push",
shallow: true,
})
const [params, setParams] = useDesignSystemSearchParams()
const groupedItems = React.useMemo(() => cachedGroupedItems(items), [items])

View File

@@ -4,7 +4,6 @@ import * as React from "react"
import Script from "next/script"
import { Search01Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useQueryStates } from "nuqs"
import { type RegistryItem } from "shadcn/schema"
import { Button } from "@/registry/new-york-v4/ui/button"
@@ -21,7 +20,7 @@ import {
ComboboxTrigger,
ComboboxValue,
} from "@/registry/new-york-v4/ui/combobox"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(create)/lib/utils"
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
@@ -38,10 +37,7 @@ export function ItemPicker({
items: Pick<RegistryItem, "name" | "title" | "type">[]
}) {
const [open, setOpen] = React.useState(false)
const [params, setParams] = useQueryStates(designSystemSearchParams, {
history: "push",
shallow: true,
})
const [params, setParams] = useDesignSystemSearchParams()
const groupedItems = React.useMemo(() => cachedGroupedItems(items), [items])
@@ -103,7 +99,7 @@ export function ItemPicker({
variant="outline"
aria-label="Select item"
size="sm"
className="data-popup-open:bg-muted dark:data-popup-open:bg-muted/50 bg-muted/50 sm:bg-background md:dark:bg-background border-foreground/10 dark:bg-muted/50 h-[calc(--spacing(13.5))] flex-1 touch-manipulation justify-between gap-2 rounded-xl pr-4! pl-2.5 text-left shadow-none select-none *:data-[slot=combobox-trigger-icon]:hidden sm:h-8 sm:max-w-56 sm:rounded-lg sm:pr-2! xl:max-w-md"
className="data-popup-open:bg-muted dark:data-popup-open:bg-muted/50 bg-muted/50 sm:bg-background md:dark:bg-background border-foreground/10 dark:bg-muted/50 h-[calc(--spacing(13.5))] flex-1 touch-manipulation justify-between gap-2 rounded-xl pr-4! pl-2.5 text-left shadow-none select-none *:data-[slot=combobox-trigger-icon]:hidden sm:h-8 sm:max-w-56 sm:rounded-lg sm:pr-2! xl:max-w-64"
/>
}
>
@@ -127,9 +123,9 @@ export function ItemPicker({
<HugeiconsIcon icon={Search01Icon} />
</ComboboxTrigger>
<ComboboxContent
className="ring-foreground/10 min-w-[calc(var(--available-width)---spacing(4))] translate-x-2 animate-none rounded-xl border-0 ring-1 data-open:animate-none sm:min-w-[calc(var(--anchor-width)+--spacing(7))] sm:translate-x-0"
className="ring-foreground/10 min-w-[calc(var(--available-width)---spacing(4))] translate-x-2 animate-none rounded-xl border-0 ring-1 data-open:animate-none sm:min-w-[calc(var(--anchor-width)+--spacing(7))] sm:translate-x-0 xl:w-96"
side="bottom"
align="center"
align="end"
>
<ComboboxInput
showTrigger={false}

View File

@@ -2,7 +2,6 @@
import * as React from "react"
import { useTheme } from "next-themes"
import { useQueryStates } from "nuqs"
import { useMounted } from "@/hooks/use-mounted"
import { type MenuColorValue } from "@/registry/config"
@@ -15,7 +14,7 @@ import {
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const MENU_OPTIONS = [
{
@@ -114,17 +113,14 @@ export function MenuColorPicker({
}) {
const { resolvedTheme } = useTheme()
const mounted = useMounted()
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const currentMenu = MENU_OPTIONS.find(
(menu) => menu.value === params.menuColor
)
return (
<Picker>
<div className="group/picker relative">
<div className="group/picker relative">
<Picker>
<PickerTrigger disabled={mounted && resolvedTheme === "dark"}>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Menu Color</div>
@@ -132,36 +128,36 @@ export function MenuColorPicker({
{currentMenu?.label}
</div>
</div>
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
{currentMenu?.icon}
</div>
</PickerTrigger>
<LockButton
param="menuColor"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentMenu?.value}
onValueChange={(value) => {
setParams({ menuColor: value as MenuColorValue })
}}
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerGroup>
{MENU_OPTIONS.map((menu) => (
<PickerRadioItem key={menu.value} value={menu.value}>
{menu.icon}
{menu.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<PickerRadioGroup
value={currentMenu?.value}
onValueChange={(value) => {
setParams({ menuColor: value as MenuColorValue })
}}
>
<PickerGroup>
{MENU_OPTIONS.map((menu) => (
<PickerRadioItem key={menu.value} value={menu.value}>
{menu.icon}
{menu.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton
param="menuColor"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -138,6 +138,8 @@ function PickerSubTrigger({
lucide="ChevronRightIcon"
tabler="IconChevronRight"
hugeicons="ArrowRight01Icon"
phosphor="CaretRightIcon"
remixicon="RiArrowRightSLine"
className="ml-auto"
/>
</MenuPrimitive.SubmenuTrigger>
@@ -190,6 +192,8 @@ function PickerCheckboxItem({
lucide="CheckIcon"
tabler="IconCheck"
hugeicons="Tick02Icon"
phosphor="CheckIcon"
remixicon="RiCheckLine"
/>
</MenuPrimitive.CheckboxItemIndicator>
</span>
@@ -230,6 +234,8 @@ function PickerRadioItem({
lucide="CheckIcon"
tabler="IconCheck"
hugeicons="Tick02Icon"
phosphor="CheckIcon"
remixicon="RiCheckLine"
className="size-4 pointer-coarse:size-5"
/>
</MenuPrimitive.RadioItemIndicator>

View File

@@ -1,9 +1,8 @@
"use client"
import * as React from "react"
import { useQueryStates } from "nuqs"
import { BASES, STYLES, type Preset } from "@/registry/config"
import { STYLES, type Preset } from "@/registry/config"
import {
Picker,
PickerContent,
@@ -12,7 +11,7 @@ import {
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function PresetPicker({
presets,
@@ -23,10 +22,7 @@ export function PresetPicker({
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const currentPreset = React.useMemo(() => {
return presets.find(

View File

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

View File

@@ -5,18 +5,18 @@ import { type ImperativePanelHandle } from "react-resizable-panels"
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/customizer-controls"
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker"
import { useDesignSystemSync } from "@/app/(create)/hooks/use-design-system"
const MESSAGE_TYPE = "design-system-params"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
import {
serializeDesignSystemSearchParams,
useDesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
export function Preview() {
const params = useDesignSystemSync()
const [params] = useDesignSystemSearchParams()
const iframeRef = React.useRef<HTMLIFrameElement>(null)
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
const [initialParams] = React.useState(params)
const [iframeKey, setIframeKey] = React.useState(0)
// Sync resizable panel with URL param changes.
React.useEffect(() => {
@@ -32,13 +32,7 @@ export function Preview() {
}
const sendParams = () => {
iframe.contentWindow?.postMessage(
{
type: MESSAGE_TYPE,
params,
},
"*"
)
sendToIframe(iframe, "design-system-params", params)
}
if (iframe.contentWindow) {
@@ -96,18 +90,25 @@ export function Preview() {
}
}, [])
if (!params.item || !params.base) {
return null
}
const iframeSrc = `/preview/${params.base}/${params.item}?theme=${initialParams.theme ?? "neutral"}&iconLibrary=${initialParams.iconLibrary ?? "lucide"}&style=${initialParams.style ?? "vega"}&font=${initialParams.font ?? "inter"}&baseColor=${initialParams.baseColor ?? "neutral"}`
const iframeSrc = React.useMemo(() => {
// The iframe src needs to include the serialized design system params
// for the initial load, but not be reactive to them as it would cause
// full-iframe reloads on every param change (flashes & loss of state).
// Further updates of the search params will be sent to the iframe
// via a postMessage channel, for it to sync its own history onto the host's.
return serializeDesignSystemSearchParams(
`/preview/${params.base}/${params.item}`,
params
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params.base, params.item])
return (
<div className="relative -mx-1 flex flex-1 flex-col justify-center sm:mx-0">
<div className="ring-foreground/15 3xl:max-h-[1200px] 3xl:max-w-[1800px] relative -z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden rounded-2xl ring-1">
<div className="bg-muted dark:bg-muted/30 absolute inset-0 rounded-2xl" />
<iframe
key={`${params.item}-${iframeKey}`}
key={params.base + params.item}
ref={iframeRef}
src={iframeSrc}
className="z-10 size-full flex-1"

View File

@@ -1,7 +1,5 @@
"use client"
import { useQueryStates } from "nuqs"
import { RADII, type RadiusValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
@@ -13,7 +11,7 @@ import {
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function RadiusPicker({
isMobile,
@@ -22,18 +20,15 @@ export function RadiusPicker({
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const currentRadius = RADII.find((radius) => radius.name === params.radius)
const defaultRadius = RADII.find((radius) => radius.name === "default")
const otherRadii = RADII.filter((radius) => radius.name !== "default")
return (
<Picker>
<div className="group/picker relative">
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Radius</div>
@@ -41,7 +36,7 @@ export function RadiusPicker({
{currentRadius?.label}
</div>
</div>
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base">
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base select-none">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@@ -60,47 +55,47 @@ export function RadiusPicker({
</svg>
</div>
</PickerTrigger>
<LockButton
param="radius"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentRadius?.name}
onValueChange={(value) => {
setParams({ radius: value as RadiusValue })
}}
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerGroup>
{defaultRadius && (
<PickerRadioItem
key={defaultRadius.name}
value={defaultRadius.name}
>
<div className="flex flex-col justify-start pointer-coarse:gap-1">
<div>{defaultRadius.label}</div>
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
Use radius from style
<PickerRadioGroup
value={currentRadius?.name}
onValueChange={(value) => {
setParams({ radius: value as RadiusValue })
}}
>
<PickerGroup>
{defaultRadius && (
<PickerRadioItem
key={defaultRadius.name}
value={defaultRadius.name}
>
<div className="flex flex-col justify-start pointer-coarse:gap-1">
<div>{defaultRadius.label}</div>
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
Use radius from style
</div>
</div>
</div>
</PickerRadioItem>
)}
</PickerGroup>
<PickerSeparator />
<PickerGroup>
{otherRadii.map((radius) => (
<PickerRadioItem key={radius.name} value={radius.name}>
{radius.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
</PickerRadioItem>
)}
</PickerGroup>
<PickerSeparator />
<PickerGroup>
{otherRadii.map((radius) => (
<PickerRadioItem key={radius.name} value={radius.name}>
{radius.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton
param="radius"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -1,16 +1,12 @@
"use client"
import * as React from "react"
import { useRouter } from "next/navigation"
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 { useQueryStates } from "nuqs"
import { cn } from "@/lib/utils"
import {
BASE_COLORS,
DEFAULT_CONFIG,
getThemesForBaseColor,
iconLibraries,
MENU_ACCENTS,
@@ -32,7 +28,7 @@ import {
RANDOMIZE_BIASES,
type RandomizeContext,
} from "@/app/(create)/lib/randomize-biases"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
@@ -40,29 +36,9 @@ function randomItem<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)]
}
export function CustomizerControls({ className }: { className?: string }) {
const router = useRouter()
export function RandomButton() {
const { locks } = useLocks()
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
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 [params, setParams] = useDesignSystemSearchParams()
const handleRandomize = React.useCallback(() => {
// Use current value if locked, otherwise randomize.
@@ -141,33 +117,30 @@ export function CustomizerControls({ className }: { className?: string }) {
}, [handleRandomize])
return (
<div className={cn("items-center gap-0", className)}>
<Button
variant="ghost"
size="sm"
onClick={handleRandomize}
className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Shuffle</div>
<div className="text-foreground text-sm font-medium">Try Random</div>
</div>
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
<Kbd className="bg-foreground/10 text-foreground hidden md:flex">R</Kbd>
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleReset}
className="border-foreground/10 bg-muted/50 hidden h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:flex md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Reset</div>
<div className="text-foreground text-sm font-medium">Start Over</div>
</div>
<HugeiconsIcon icon={Undo02Icon} className="-translate-x-0.5" />
</Button>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={handleRandomize}
className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Shuffle</div>
<div className="text-foreground text-sm font-medium">
Try Random
</div>
</div>
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
<Kbd className="bg-foreground/10 text-foreground hidden md:flex">
R
</Kbd>
</Button>
</TooltipTrigger>
<TooltipContent side="left">
Use browser back/forward to navigate history
</TooltipContent>
</Tooltip>
)
}

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

@@ -3,7 +3,6 @@
import * as React from "react"
import { Share03Icon, Tick02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useQueryStates } from "nuqs"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/registry/new-york-v4/ui/button"
@@ -12,12 +11,10 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function ShareButton() {
const [params] = useQueryStates(designSystemSearchParams, {
shallow: false,
})
const [params] = useDesignSystemSearchParams()
const [hasCopied, setHasCopied] = React.useState(false)
const shareUrl = React.useMemo(() => {
@@ -59,7 +56,7 @@ export function ShareButton() {
<Button
size="sm"
variant="outline"
className="rounded-lg shadow-none"
className="rounded-lg shadow-none lg:w-8 xl:w-fit"
onClick={handleCopy}
>
{hasCopied ? (
@@ -67,7 +64,7 @@ export function ShareButton() {
) : (
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
)}
Share
<span className="lg:hidden xl:block">Share</span>
</Button>
</TooltipTrigger>
<TooltipContent>Copy Link</TooltipContent>

View File

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

View File

@@ -1,7 +1,5 @@
"use client"
import { useQueryStates } from "nuqs"
import {
Picker,
PickerContent,
@@ -10,7 +8,7 @@ import {
PickerRadioItem,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const TEMPLATES = [
{
@@ -37,10 +35,7 @@ export function TemplatePicker({
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const currentTemplate = TEMPLATES.find(
(template) => template.value === params.template
@@ -57,7 +52,7 @@ export function TemplatePicker({
</div>
{currentTemplate?.logo && (
<div
className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 size-4 -translate-y-1/2 *:[svg]:size-4"
className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 select-none *:[svg]:size-4"
dangerouslySetInnerHTML={{
__html: currentTemplate.logo,
}}

View File

@@ -2,7 +2,6 @@
import * as React from "react"
import { useTheme } from "next-themes"
import { useQueryStates } from "nuqs"
import { useMounted } from "@/hooks/use-mounted"
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
@@ -11,13 +10,12 @@ import {
Picker,
PickerContent,
PickerGroup,
PickerLabel,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function ThemePicker({
themes,
@@ -30,10 +28,7 @@ export function ThemePicker({
}) {
const { resolvedTheme } = useTheme()
const mounted = useMounted()
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const currentTheme = React.useMemo(
() => themes.find((theme) => theme.name === params.theme),
@@ -52,8 +47,8 @@ export function ThemePicker({
}, [currentTheme, themes, setParams])
return (
<Picker>
<div className="group/picker relative">
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Theme</div>
@@ -73,99 +68,99 @@ export function ThemePicker({
],
} as React.CSSProperties
}
className="absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color)"
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none"
/>
)}
</PickerTrigger>
<LockButton
param="theme"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-96"
>
<PickerRadioGroup
value={currentTheme?.name}
onValueChange={(value) => {
setParams({ theme: value as ThemeName })
}}
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-96"
>
<PickerGroup>
{themes
.filter((theme) =>
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
)
.map((theme) => {
const isBaseColor = BASE_COLORS.find(
(baseColor) => baseColor.name === theme.name
<PickerRadioGroup
value={currentTheme?.name}
onValueChange={(value) => {
setParams({ theme: value as ThemeName })
}}
>
<PickerGroup>
{themes
.filter((theme) =>
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
)
return (
<PickerRadioItem key={theme.name} value={theme.name}>
<div className="flex items-start gap-2">
{mounted && resolvedTheme && (
<div
style={
{
"--color":
theme.cssVars?.[
resolvedTheme as "light" | "dark"
]?.[
isBaseColor ? "muted-foreground" : "primary"
],
} as React.CSSProperties
}
className="size-4 translate-y-1 rounded-full bg-(--color)"
/>
)}
<div className="flex flex-col justify-start pointer-coarse:gap-1">
<div>{theme.title}</div>
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
Match base color
</div>
</div>
</div>
</PickerRadioItem>
)
})}
</PickerGroup>
<PickerSeparator />
<PickerGroup>
{themes
.filter(
(theme) =>
!BASE_COLORS.find(
.map((theme) => {
const isBaseColor = BASE_COLORS.find(
(baseColor) => baseColor.name === theme.name
)
)
.map((theme) => {
return (
<PickerRadioItem key={theme.name} value={theme.name}>
<div className="flex items-center gap-2">
{mounted && resolvedTheme && (
<div
style={
{
"--color":
theme.cssVars?.[
resolvedTheme as "light" | "dark"
]?.["primary"],
} as React.CSSProperties
}
className="size-4 rounded-full bg-(--color)"
/>
)}
{theme.title}
</div>
</PickerRadioItem>
return (
<PickerRadioItem key={theme.name} value={theme.name}>
<div className="flex items-start gap-2">
{mounted && resolvedTheme && (
<div
style={
{
"--color":
theme.cssVars?.[
resolvedTheme as "light" | "dark"
]?.[
isBaseColor ? "muted-foreground" : "primary"
],
} as React.CSSProperties
}
className="size-4 translate-y-1 rounded-full bg-(--color)"
/>
)}
<div className="flex flex-col justify-start pointer-coarse:gap-1">
<div>{theme.title}</div>
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
Match base color
</div>
</div>
</div>
</PickerRadioItem>
)
})}
</PickerGroup>
<PickerSeparator />
<PickerGroup>
{themes
.filter(
(theme) =>
!BASE_COLORS.find(
(baseColor) => baseColor.name === theme.name
)
)
})}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
.map((theme) => {
return (
<PickerRadioItem key={theme.name} value={theme.name}>
<div className="flex items-center gap-2">
{mounted && resolvedTheme && (
<div
style={
{
"--color":
theme.cssVars?.[
resolvedTheme as "light" | "dark"
]?.["primary"],
} as React.CSSProperties
}
className="size-4 rounded-full bg-(--color)"
/>
)}
{theme.title}
</div>
</PickerRadioItem>
)
})}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton
param="theme"
className="absolute top-1/2 right-10 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -7,7 +7,6 @@ import {
Tick02Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useQueryStates } from "nuqs"
import { toast } from "sonner"
import { useConfig } from "@/hooks/use-config"
@@ -43,7 +42,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const TEMPLATES = [
{
@@ -65,10 +64,7 @@ const TEMPLATES = [
export function ToolbarControls() {
const [open, setOpen] = React.useState(false)
const [params, setParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params, setParams] = useDesignSystemSearchParams()
const [config, setConfig] = useConfig()
const [hasCopied, setHasCopied] = React.useState(false)

View File

@@ -1,7 +1,5 @@
"use client"
import { useQueryStates } from "nuqs"
import { cn } from "@/lib/utils"
import { useIsMobile } from "@/hooks/use-mobile"
import { useMounted } from "@/hooks/use-mounted"
@@ -13,20 +11,15 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function V0Button({ className }: { className?: string }) {
const [params] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
})
const [params] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const isMounted = useMounted()
const url = `${process.env.NEXT_PUBLIC_APP_URL}/create/v0?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&item=${params.item}`
console.log(url)
if (!isMounted) {
return <Skeleton className="h-8 w-24 rounded-lg" />
}
@@ -39,7 +32,7 @@ export function V0Button({ className }: { className?: string }) {
size="sm"
variant={isMobile ? "default" : "outline"}
className={cn(
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px]",
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px] lg:w-8 xl:w-24",
className
)}
asChild
@@ -48,7 +41,8 @@ export function V0Button({ className }: { className?: string }) {
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`}
target="_blank"
>
Open in <Icons.v0 className="size-5" />
<span className="lg:hidden xl:block">Open in</span>
<Icons.v0 className="size-5" />
</a>
</Button>
</TooltipTrigger>

View File

@@ -4,7 +4,11 @@ import { ArrowLeftIcon } from "lucide-react"
import type { SearchParams } from "nuqs/server"
import { siteConfig } from "@/lib/config"
import { source } from "@/lib/source"
import { absoluteUrl } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { MainNav } from "@/components/main-nav"
import { MobileNav } from "@/components/mobile-nav"
import { ModeSwitcher } from "@/components/mode-switcher"
import { SiteConfig } from "@/components/site-config"
import { BASES } from "@/registry/config"
@@ -12,16 +16,17 @@ import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar"
import { Customizer } from "@/app/(create)/components/customizer"
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
import { ItemExplorer } from "@/app/(create)/components/item-explorer"
import { ItemPicker } from "@/app/(create)/components/item-picker"
import { Preview } from "@/app/(create)/components/preview"
import { RandomButton } from "@/app/(create)/components/random-button"
import { ResetButton } from "@/app/(create)/components/reset-button"
import { ShareButton } from "@/app/(create)/components/share-button"
import { ToolbarControls } from "@/app/(create)/components/toolbar-controls"
import { V0Button } from "@/app/(create)/components/v0-button"
import { WelcomeDialog } from "@/app/(create)/components/welcome-dialog"
import { getItemsForBase } from "@/app/(create)/lib/api"
import { designSystemSearchParamsCache } from "@/app/(create)/lib/search-params"
import { loadDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export const revalidate = false
export const dynamic = "force-static"
@@ -60,9 +65,10 @@ export default async function CreatePage({
}: {
searchParams: Promise<SearchParams>
}) {
const params = await designSystemSearchParamsCache.parse(searchParams)
const params = await loadDesignSystemSearchParams(searchParams)
const base = BASES.find((b) => b.name === params.base) ?? BASES[0]
const pageTree = source.pageTree
const items = await getItemsForBase(base.name)
const filteredItems = items
@@ -81,35 +87,34 @@ export default async function CreatePage({
<header className="sticky top-0 z-50 w-full">
<div className="container-wrapper 3xl:fixed:px-0 px-6">
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
<div className="flex items-center xl:w-1/3">
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
<MobileNav
tree={pageTree}
items={siteConfig.navItems}
className="flex lg:hidden"
/>
<Button
asChild
variant="outline"
size="sm"
className="rounded-lg shadow-none"
variant="ghost"
size="icon"
className="hidden size-8 lg:flex"
>
<Link href="/">
<ArrowLeftIcon />
Back
<Icons.logo className="size-5" />
<span className="sr-only">{siteConfig.name}</span>
</Link>
</Button>
<Separator
orientation="vertical"
className="mx-2 hidden sm:mx-4 lg:flex"
/>
<div className="text-muted-foreground hidden text-sm font-medium lg:flex">
New Project
</div>
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
</div>
<div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center gap-2 px-4.5 pb-4 sm:static sm:justify-end sm:p-0 lg:ml-0 xl:justify-center">
<div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center justify-end gap-2 px-4.5 pb-4 sm:static sm:p-0 lg:ml-0">
<ItemPicker items={filteredItems} />
<CustomizerControls className="sm:hidden" />
<Separator
orientation="vertical"
className="mr-2 hidden sm:flex xl:hidden"
/>
<div className="items-center gap-0 sm:hidden">
<RandomButton />
<ResetButton />
</div>
<Separator orientation="vertical" className="mr-2 flex" />
</div>
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end xl:ml-auto xl:w-1/3">
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end">
<SiteConfig className="3xl:flex hidden" />
<Separator orientation="vertical" className="3xl:flex hidden" />
<ModeSwitcher />

View File

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

View File

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

View File

@@ -1,89 +0,0 @@
"use client"
import { useQueryStates } from "nuqs"
import {
createIframeSyncStore,
useIframeSyncAll,
useIframeSyncValue,
} from "@/app/(create)/hooks/use-iframe-sync"
import {
designSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
const MESSAGE_TYPE = "design-system-params"
const getInitialValues = (): DesignSystemSearchParams => {
if (typeof window === "undefined") {
return {
base: "radix",
iconLibrary: "lucide",
theme: "neutral",
style: "vega",
font: "inter",
item: "cover-example",
baseColor: "neutral",
menuAccent: "subtle",
menuColor: "default",
radius: "default",
size: 100,
custom: false,
template: "next",
}
}
const searchParams = new URLSearchParams(window.location.search)
return {
base: (searchParams.get("base") ||
"radix") as DesignSystemSearchParams["base"],
iconLibrary: (searchParams.get("iconLibrary") ||
"lucide") as DesignSystemSearchParams["iconLibrary"],
theme: (searchParams.get("theme") ||
"neutral") as DesignSystemSearchParams["theme"],
style: (searchParams.get("style") ||
"vega") as DesignSystemSearchParams["style"],
font: (searchParams.get("font") ||
"inter") as DesignSystemSearchParams["font"],
item: searchParams.get("item") || "cover-example",
baseColor: (searchParams.get("baseColor") ||
"neutral") as DesignSystemSearchParams["baseColor"],
menuAccent: (searchParams.get("menuAccent") ||
"subtle") as DesignSystemSearchParams["menuAccent"],
menuColor: (searchParams.get("menuColor") ||
"default") as DesignSystemSearchParams["menuColor"],
radius: (searchParams.get("radius") ||
"default") as DesignSystemSearchParams["radius"],
size: parseInt(searchParams.get("size") || "100"),
custom: (searchParams.get("custom") || "false") === "true",
template: (searchParams.get("template") ||
"next") as DesignSystemSearchParams["template"],
}
}
const designSystemStore = createIframeSyncStore(
MESSAGE_TYPE,
getInitialValues()
)
export function useDesignSystemSync() {
const [urlParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
})
const keys = Object.keys(
designSystemSearchParams
) as (keyof DesignSystemSearchParams)[]
return useIframeSyncAll(designSystemStore, keys, urlParams)
}
export function useDesignSystemParam<K extends keyof DesignSystemSearchParams>(
key: K
) {
const [urlParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
})
return useIframeSyncValue(designSystemStore, key, urlParams[key])
}

View File

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

View File

@@ -11,6 +11,7 @@ export type LockableParam =
| "menuAccent"
| "menuColor"
| "radius"
| "direction"
type LocksContextValue = {
locks: Set<LockableParam>

View File

@@ -1,15 +1,20 @@
import { useQueryStates } from "nuqs"
import {
createSearchParamsCache,
createLoader,
createSerializer,
parseAsBoolean,
parseAsInteger,
parseAsString,
parseAsStringLiteral,
type inferParserType,
type Options,
} from "nuqs/server"
import {
BASE_COLORS,
BASES,
DEFAULT_CONFIG,
DIRECTIONS,
iconLibraries,
MENU_ACCENTS,
MENU_COLORS,
@@ -18,6 +23,7 @@ import {
THEMES,
type BaseColorName,
type BaseName,
type DirectionValue,
type FontValue,
type IconLibraryName,
type MenuAccentValue,
@@ -28,11 +34,11 @@ import {
} from "@/registry/config"
import { FONTS } from "@/app/(create)/lib/fonts"
export const designSystemSearchParams = {
const designSystemSearchParams = {
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
DEFAULT_CONFIG.base
),
item: parseAsString.withDefault("preview"),
item: parseAsString.withDefault("preview").withOptions({ shallow: true }),
iconLibrary: parseAsStringLiteral<IconLibraryName>(
Object.values(iconLibraries).map((i) => i.name)
).withDefault(DEFAULT_CONFIG.iconLibrary),
@@ -48,6 +54,9 @@ export const designSystemSearchParams = {
baseColor: parseAsStringLiteral<BaseColorName>(
BASE_COLORS.map((b) => b.name)
).withDefault(DEFAULT_CONFIG.baseColor),
direction: parseAsStringLiteral<DirectionValue>(
DIRECTIONS.map((d) => d.value)
).withDefault(DEFAULT_CONFIG.direction),
menuAccent: parseAsStringLiteral<MenuAccentValue>(
MENU_ACCENTS.map((a) => a.value)
).withDefault(DEFAULT_CONFIG.menuAccent),
@@ -57,19 +66,30 @@ export const designSystemSearchParams = {
radius: parseAsStringLiteral<RadiusValue>(
RADII.map((r) => r.name)
).withDefault("default"),
template: parseAsStringLiteral<"next" | "start" | "vite">([
template: parseAsStringLiteral([
"next",
"start",
"vite",
]).withDefault("next"),
] as const).withDefault("next"),
size: parseAsInteger.withDefault(100),
custom: parseAsBoolean.withDefault(false),
}
export const designSystemSearchParamsCache = createSearchParamsCache(
export const loadDesignSystemSearchParams = createLoader(
designSystemSearchParams
)
export type DesignSystemSearchParams = Awaited<
ReturnType<typeof designSystemSearchParamsCache.parse>
export const serializeDesignSystemSearchParams = createSerializer(
designSystemSearchParams
)
export const useDesignSystemSearchParams = (options: Options = {}) =>
useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
...options,
})
export type DesignSystemSearchParams = inferParserType<
typeof designSystemSearchParams
>

View File

@@ -7,10 +7,10 @@ import { absoluteUrl } from "@/lib/utils"
import { DarkModeScript } from "@/components/mode-switcher"
import { TailwindIndicator } from "@/components/tailwind-indicator"
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 { ItemPickerScript } from "@/app/(create)/components/item-picker"
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 { ALLOWED_ITEM_TYPES } from "@/app/(create)/lib/constants"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -117,6 +117,8 @@ function DropdownMenuCheckboxes() {
lucide="UserIcon"
tabler="IconUser"
hugeicons="UserIcon"
phosphor="UserIcon"
remixicon="RiUserLine"
/>
Profile
</DropdownMenuItem>
@@ -125,6 +127,8 @@ function DropdownMenuCheckboxes() {
lucide="CreditCardIcon"
tabler="IconCreditCard"
hugeicons="CreditCardIcon"
phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
/>
Billing
</DropdownMenuItem>
@@ -133,6 +137,8 @@ function DropdownMenuCheckboxes() {
lucide="SettingsIcon"
tabler="IconSettings"
hugeicons="SettingsIcon"
phosphor="GearIcon"
remixicon="RiSettings3Line"
/>
Settings
</DropdownMenuItem>
@@ -167,6 +173,8 @@ function DropdownMenuCheckboxes() {
lucide="LogOutIcon"
tabler="IconLogout"
hugeicons="LogoutIcon"
phosphor="SignOutIcon"
remixicon="RiLogoutBoxLine"
/>
Sign Out
</DropdownMenuItem>
@@ -222,6 +230,8 @@ function DropdownMenuWithAvatar() {
lucide="ChevronsUpDownIcon"
tabler="IconChevronsUpDown"
hugeicons="ChevronUpDownIcon"
phosphor="CaretUpDownIcon"
remixicon="RiExpandUpDownLine"
className="text-muted-foreground ml-auto"
/>
</Button>
@@ -251,6 +261,8 @@ function DropdownMenuWithAvatar() {
lucide="SparklesIcon"
tabler="IconSparkles"
hugeicons="SparklesIcon"
phosphor="SparklesIcon"
remixicon="RiSparklingLine"
/>
Upgrade to Pro
</DropdownMenuItem>
@@ -262,6 +274,8 @@ function DropdownMenuWithAvatar() {
lucide="BadgeCheckIcon"
tabler="IconBadgeCheck"
hugeicons="BadgeCheckIcon"
phosphor="CheckCircleIcon"
remixicon="RiVerifiedBadgeLine"
/>
Account
</DropdownMenuItem>
@@ -270,6 +284,8 @@ function DropdownMenuWithAvatar() {
lucide="CreditCardIcon"
tabler="IconCreditCard"
hugeicons="CreditCardIcon"
phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
/>
Billing
</DropdownMenuItem>
@@ -278,6 +294,8 @@ function DropdownMenuWithAvatar() {
lucide="BellIcon"
tabler="IconBell"
hugeicons="BellIcon"
phosphor="BellIcon"
remixicon="RiNotification3Line"
/>
Notifications
</DropdownMenuItem>
@@ -288,6 +306,8 @@ function DropdownMenuWithAvatar() {
lucide="LogOutIcon"
tabler="IconLogout"
hugeicons="LogoutIcon"
phosphor="SignOutIcon"
remixicon="RiLogoutBoxLine"
/>
Sign Out
</DropdownMenuItem>
@@ -341,6 +361,8 @@ function DropdownMenuAvatarOnly() {
lucide="SparklesIcon"
tabler="IconSparkles"
hugeicons="SparklesIcon"
phosphor="SparklesIcon"
remixicon="RiSparklingLine"
/>
Upgrade to Pro
</DropdownMenuItem>
@@ -352,6 +374,8 @@ function DropdownMenuAvatarOnly() {
lucide="BadgeCheckIcon"
tabler="IconBadgeCheck"
hugeicons="BadgeCheckIcon"
phosphor="CheckCircleIcon"
remixicon="RiVerifiedBadgeLine"
/>
Account
</DropdownMenuItem>
@@ -360,6 +384,8 @@ function DropdownMenuAvatarOnly() {
lucide="CreditCardIcon"
tabler="IconCreditCard"
hugeicons="CreditCardIcon"
phosphor="CreditCardIcon"
remixicon="RiBankCardLine"
/>
Billing
</DropdownMenuItem>
@@ -368,6 +394,8 @@ function DropdownMenuAvatarOnly() {
lucide="BellIcon"
tabler="IconBell"
hugeicons="BellIcon"
phosphor="BellIcon"
remixicon="RiNotification3Line"
/>
Notifications
</DropdownMenuItem>
@@ -378,6 +406,8 @@ function DropdownMenuAvatarOnly() {
lucide="LogOutIcon"
tabler="IconLogout"
hugeicons="LogoutIcon"
phosphor="SignOutIcon"
remixicon="RiLogoutBoxLine"
/>
Sign Out
</DropdownMenuItem>
@@ -395,6 +425,8 @@ function DropdownMenuIconColor() {
lucide="MoreHorizontalIcon"
tabler="IconDots"
hugeicons="MoreHorizontalCircle01Icon"
phosphor="DotsThreeOutlineIcon"
remixicon="RiMoreLine"
/>
<span className="sr-only">Toggle menu</span>
</Button>
@@ -406,6 +438,8 @@ function DropdownMenuIconColor() {
lucide="PencilIcon"
tabler="IconPencil"
hugeicons="EditIcon"
phosphor="PencilIcon"
remixicon="RiPencilLine"
/>
Edit
</DropdownMenuItem>
@@ -414,6 +448,8 @@ function DropdownMenuIconColor() {
lucide="ShareIcon"
tabler="IconShare"
hugeicons="ShareIcon"
phosphor="ShareIcon"
remixicon="RiShareLine"
/>
Share
</DropdownMenuItem>
@@ -423,6 +459,8 @@ function DropdownMenuIconColor() {
lucide="TrashIcon"
tabler="IconTrash"
hugeicons="DeleteIcon"
phosphor="TrashIcon"
remixicon="RiDeleteBinLine"
/>
Delete
</DropdownMenuItem>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -57,6 +57,11 @@ export const metadata: Metadata = {
apple: "/apple-touch-icon.png",
},
manifest: `${siteConfig.url}/site.webmanifest`,
alternates: {
types: {
"application/rss+xml": `${siteConfig.url}/rss.xml`,
},
},
}
export default function RootLayout({
@@ -91,9 +96,11 @@ export default function RootLayout({
<ThemeProvider>
<LayoutProvider>
<ActiveThemeProvider>
<NuqsAdapter>{children}</NuqsAdapter>
<NuqsAdapter>
{children}
<Toaster position="top-center" />
</NuqsAdapter>
<TailwindIndicator />
<Toaster position="top-center" />
<Analytics />
</ActiveThemeProvider>
</LayoutProvider>

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ export function ColorFormatSelector({
<span className="font-medium">Format: </span>
<span className="text-muted-foreground font-mono">{format}</span>
</SelectTrigger>
<SelectContent align="end" className="rounded-xl">
<SelectContent align="end" position="popper" className="rounded-xl">
{Object.entries(formats).map(([format, value]) => (
<SelectItem
key={format}

View File

@@ -1,15 +1,17 @@
"use client"
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 * as DialogPrimitive from "@radix-ui/react-dialog"
import { IconArrowRight } from "@tabler/icons-react"
import { useDocsSearch } from "fumadocs-core/search/client"
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
import { CornerDownLeftIcon, SquareDashedIcon, XIcon } from "lucide-react"
import { type Color, type ColorPalette } from "@/lib/colors"
import { trackEvent } from "@/lib/events"
import { showMcpDocs } from "@/lib/flags"
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
import { type source } from "@/lib/source"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
@@ -26,13 +28,14 @@ import {
} from "@/registry/new-york-v4/ui/command"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "@/registry/new-york-v4/ui/dialog"
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
@@ -49,7 +52,9 @@ export function CommandMenu({
navItems?: { href: string; label: string }[]
}) {
const router = useRouter()
const pathname = usePathname()
const [config] = useConfig()
const currentBase = getCurrentBase(pathname)
const [open, setOpen] = React.useState(false)
const [selectedType, setSelectedType] = React.useState<
"color" | "page" | "component" | "block" | null
@@ -138,10 +143,13 @@ export function CommandMenu({
[setSelectedType, setCopyPayload, packageManager]
)
const runCommand = React.useCallback((command: () => unknown) => {
setOpen(false)
command()
}, [])
const runCommand = React.useCallback(
(command: () => unknown) => {
setOpen(false)
command()
},
[setOpen]
)
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
@@ -207,10 +215,7 @@ export function CommandMenu({
</div>
</Button>
</DialogTrigger>
<DialogContent
showCloseButton={false}
className="rounded-xl border-none bg-clip-padding p-2 pb-11 shadow-2xl ring-4 ring-neutral-200/80 dark:bg-neutral-900 dark:ring-neutral-800"
>
<DialogContent className="rounded-xl border-none bg-clip-padding p-2 pb-11 shadow-2xl ring-4 ring-neutral-200/80 dark:bg-neutral-900 dark:ring-neutral-800">
<DialogHeader className="sr-only">
<DialogTitle>Search documentation...</DialogTitle>
<DialogDescription>Search for a command to run...</DialogDescription>
@@ -269,40 +274,37 @@ export function CommandMenu({
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/")
getPagesFromFolder(group, currentBase).map((item) => {
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>
)
if (!showMcpDocs && item.url.includes("/mcp")) {
return null
}
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>
)
})}
</CommandGroup>
))}
@@ -523,3 +525,27 @@ function SearchResults({
</CommandGroup>
)
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background fixed top-[50%] 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

@@ -3,49 +3,199 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import {
LanguageProvider,
LanguageSelector,
useLanguageContext,
useTranslation,
type Translations,
} from "@/components/language-selector"
import { DirectionProvider as BaseDirectionProvider } from "@/registry/bases/base/ui/direction"
import { DirectionProvider as RadixDirectionProvider } from "@/registry/bases/radix/ui/direction"
import { Button } from "@/registry/new-york-v4/ui/button"
export function ComponentPreviewTabs({
className,
previewClassName,
align = "center",
hideCode = false,
chromeLessOnMobile = false,
component,
source,
sourcePreview,
direction = "ltr",
styleName,
...props
}: React.ComponentProps<"div"> & {
previewClassName?: string
align?: "center" | "start" | "end"
hideCode?: boolean
chromeLessOnMobile?: boolean
component: React.ReactNode
source: React.ReactNode
sourcePreview?: React.ReactNode
direction?: "ltr" | "rtl"
styleName?: string
}) {
const [isMobileCodeVisible, setIsMobileCodeVisible] = React.useState(false)
const base = styleName?.split("-")[0]
return (
<div
data-slot="component-preview"
className={cn(
"group relative mt-4 mb-12 flex flex-col gap-2 rounded-lg border",
"group relative mt-4 mb-12 flex flex-col gap-2 overflow-hidden rounded-xl border",
className
)}
{...props}
>
<div data-slot="preview">
<div
data-align={align}
className={cn(
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
)}
>
{component}
</div>
{!hideCode && (
<div
data-slot="code"
className="overflow-hidden [&_[data-rehype-pretty-code-figure]]:!m-0 [&_[data-rehype-pretty-code-figure]]:rounded-t-none [&_[data-rehype-pretty-code-figure]]:border-t [&_pre]:max-h-[400px]"
{direction === "rtl" ? (
<LanguageProvider defaultLanguage="ar">
<RtlLanguageSelector />
<PreviewWrapper
align={align}
chromeLessOnMobile={chromeLessOnMobile}
previewClassName={previewClassName}
>
{source}
</div>
<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-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 ? (
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"
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() {
const context = useLanguageContext()
// This component is always rendered inside LanguageProvider when direction === "rtl".
// so context should always be available.
if (!context) {
return null
}
return (
<LanguageSelector
value={context.language}
onValueChange={context.setLanguage}
/>
)
}
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}>
<div
data-align={align}
data-chromeless={chromeLessOnMobile}
className={cn(
"preview relative flex h-72 w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start data-[chromeless=true]:h-auto data-[chromeless=true]:p-0",
previewClassName
)}
>
{children}
</div>
</div>
)
}
function DirectionProviderWrapper({
base,
dir: explicitDir,
children,
}: {
base?: string
dir?: "ltr" | "rtl"
children: React.ReactNode
}) {
// useTranslation handles the case when there's no LanguageProvider context.
// It will fall back to local state with defaultLanguage.
const translation = useTranslation(directionTranslations, "ar")
const dir = explicitDir ?? translation.dir
if (base === "base") {
return (
<BaseDirectionProvider direction={dir}>{children}</BaseDirectionProvider>
)
}
return <RadixDirectionProvider dir={dir}>{children}</RadixDirectionProvider>
}

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
"use client"
import * as React from "react"
import Link from "next/link"
import { IconCheck } from "@tabler/icons-react"
import { IconCheck, IconCopy, IconPlus } from "@tabler/icons-react"
import { cn } from "@/lib/utils"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { useConfig } from "@/hooks/use-config"
import { useIsMobile } from "@/hooks/use-mobile"
import { CopyButton } from "@/components/copy-button"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Dialog,
@@ -29,77 +27,114 @@ import {
DrawerTitle,
DrawerTrigger,
} from "@/registry/new-york-v4/ui/drawer"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/new-york-v4/ui/tabs"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function DirectoryAddButton({
registry,
}: {
registry: {
name: string
url: string
}
registry: { name: string }
}) {
const { copyToClipboard, isCopied } = useCopyToClipboard()
const isMobile = useIsMobile()
const [config, setConfig] = useConfig()
const [hasCopied, setHasCopied] = React.useState(false)
const [open, setOpen] = React.useState(false)
const isMobile = useIsMobile()
const jsonValue = `{
"registries": {
"${registry.name}": "${registry.url}"
}
}`
const packageManager = config.packageManager || "pnpm"
const commands = React.useMemo(() => {
return {
pnpm: `pnpm dlx shadcn@latest registry add ${registry.name}`,
npm: `npx shadcn@latest registry add ${registry.name}`,
yarn: `yarn dlx shadcn@latest registry add ${registry.name}`,
bun: `bunx --bun shadcn@latest registry add ${registry.name}`,
}
}, [registry.name])
const command = commands[packageManager]
React.useEffect(() => {
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
const handleCopy = React.useCallback(() => {
copyToClipboardWithMeta(command, {
name: "copy_registry_add_command",
properties: {
command,
registry: registry.name,
},
})
setHasCopied(true)
}, [command, registry.name])
const Trigger = (
<Button
size="sm"
variant="outline"
className="relative z-10"
onClick={() => setOpen(true)}
>
{isCopied ? (
<IconCheck />
) : (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Model Context Protocol</title>
<path
d="M13.85 0a4.16 4.16 0 0 0-2.95 1.217L1.456 10.66a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l9.442-9.442a2.49 2.49 0 0 1 3.541 0 2.49 2.49 0 0 1 0 3.541L8.59 12.97l-.1.1a.835.835 0 0 0 0 1.18.835.835 0 0 0 1.18 0l.1-.098 7.03-7.034a2.49 2.49 0 0 1 3.542 0l.049.05a2.49 2.49 0 0 1 0 3.54l-8.54 8.54a1.96 1.96 0 0 0 0 2.755l1.753 1.753a.835.835 0 0 0 1.18 0 .835.835 0 0 0 0-1.18l-1.753-1.753a.266.266 0 0 1 0-.394l8.54-8.54a4.185 4.185 0 0 0 0-5.9l-.05-.05a4.16 4.16 0 0 0-2.95-1.218c-.2 0-.401.02-.6.048a4.17 4.17 0 0 0-1.17-3.552A4.16 4.16 0 0 0 13.85 0m0 3.333a.84.84 0 0 0-.59.245L6.275 10.56a4.186 4.186 0 0 0 0 5.902 4.186 4.186 0 0 0 5.902 0L19.16 9.48a.835.835 0 0 0 0-1.18.835.835 0 0 0-1.18 0l-6.985 6.984a2.49 2.49 0 0 1-3.54 0 2.49 2.49 0 0 1 0-3.54l6.983-6.985a.835.835 0 0 0 0-1.18.84.84 0 0 0-.59-.245"
className="fill-foreground"
/>
</svg>
)}
MCP
<Button size="sm" variant="outline" className="relative z-10">
Add <IconPlus />
</Button>
)
const Content = (
<>
<figure
data-rehype-pretty-code-figure
className={cn(
"group relative mt-0",
!isMobile &&
"dark:bg-background dark:[&_[data-line]:not([data-highlighted-line]):before]:bg-background!"
)}
>
<CopyButton
value={jsonValue}
className="top-3 right-2"
tooltip="Copy Code"
/>
<div data-rehype-pretty-code-title>components.json</div>
<pre className="no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0">
<code data-line-numbers data-language="json">
<span data-line>{"{"}</span>
<span data-line>{' "registries": {'}</span>
<span
data-line
data-highlighted-line
>{` "${registry.name}": "${registry.url}"`}</span>
<span data-line>{" }"}</span>
<span data-line>{"}"}</span>
</code>
</pre>
</figure>
</>
<Tabs
value={packageManager}
onValueChange={(value) => {
setConfig({
...config,
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
})
}}
className="gap-0 overflow-hidden rounded-lg border"
>
<div className="flex items-center gap-2 border-b p-2">
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
<TabsTrigger value="npm">npm</TabsTrigger>
<TabsTrigger value="yarn">yarn</TabsTrigger>
<TabsTrigger value="bun">bun</TabsTrigger>
</TabsList>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto size-7 rounded-lg"
onClick={handleCopy}
>
{hasCopied ? (
<IconCheck className="size-4" />
) : (
<IconCopy 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]) => (
<TabsContent key={key} value={key} className="mt-0">
<div className="bg-surface text-surface-foreground px-3 py-3">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">{cmd}</code>
</div>
</div>
</TabsContent>
))}
</Tabs>
)
if (isMobile) {
@@ -108,20 +143,16 @@ export function DirectoryAddButton({
<DrawerTrigger asChild>{Trigger}</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Configure MCP</DrawerTitle>
<DrawerTitle>Add Registry</DrawerTitle>
<DrawerDescription>
Copy and paste the following code into your project&apos;s
components.json.
Run this command to add {registry.name} to your project.
</DrawerDescription>
</DrawerHeader>
<div className="px-6">{Content}</div>
<div className="px-4">{Content}</div>
<DrawerFooter>
<DrawerClose asChild>
<Button size="sm">Close</Button>
<Button size="sm">Done</Button>
</DrawerClose>
<Button size="sm" asChild variant="outline">
<Link href="/docs/mcp">Read the docs</Link>
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
@@ -131,22 +162,15 @@ export function DirectoryAddButton({
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{Trigger}</DialogTrigger>
<DialogContent
className="rounded-xl border-none bg-clip-padding shadow-2xl ring-4 ring-neutral-200/80 sm:max-w-[600px] dark:bg-neutral-900 dark:ring-neutral-800"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<DialogContent className="dialog-ring animate-none! rounded-xl sm:max-w-md">
<DialogHeader>
<DialogTitle>Configure MCP</DialogTitle>
<DialogTitle>Add Registry</DialogTitle>
<DialogDescription>
Copy and paste the following code into your project&apos;s
components.json.
Run this command to add {registry.name} to your project.
</DialogDescription>
</DialogHeader>
{Content}
<DialogFooter className="justify-between!">
<Button size="sm" asChild variant="ghost">
<Link href="/docs/mcp">Read the docs</Link>
</Button>
<DialogFooter>
<DialogClose asChild>
<Button size="sm">Done</Button>
</DialogClose>

View File

@@ -50,8 +50,10 @@ export function DirectoryList() {
href={getHomepageUrl(registry.homepage)}
target="_blank"
rel="noopener noreferrer external"
className="group flex items-center gap-1"
>
{registry.name}
{registry.name}{" "}
<IconArrowUpRight className="size-4 opacity-0 group-hover:opacity-100" />
</a>
</ItemTitle>
{registry.description && (
@@ -61,15 +63,6 @@ export function DirectoryList() {
)}
</ItemContent>
<ItemActions className="relative z-10 hidden self-start sm:flex">
<Button size="sm" variant="outline" asChild>
<a
href={getHomepageUrl(registry.homepage)}
target="_blank"
rel="noopener noreferrer external"
>
View <IconArrowUpRight />
</a>
</Button>
<DirectoryAddButton registry={registry} />
</ItemActions>
<ItemFooter className="justify-start pl-16 sm:hidden">

View File

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

View File

@@ -87,6 +87,70 @@ const menuItems = {
Open in Claude
</a>
),
scira: (url: string) => (
<a
href={getPromptUrl("https://scira.ai/", url)}
target="_blank"
className="m-0 p-0"
>
<svg
width="910"
height="934"
viewBox="0 0 910 934"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="8"
strokeLinejoin="round"
/>
<path
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="8"
strokeLinejoin="round"
/>
<path
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
stroke="currentColor"
strokeWidth="20"
strokeLinejoin="round"
/>
<path
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
stroke="currentColor"
strokeWidth="20"
strokeLinejoin="round"
/>
<path
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="8"
strokeLinejoin="round"
/>
<path
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="8"
strokeLinejoin="round"
/>
<path
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
stroke="currentColor"
strokeWidth="30"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Open in Scira
</a>
),
}
export function DocsCopyPage({ page, url }: { page: string; url: string }) {
@@ -119,7 +183,10 @@ export function DocsCopyPage({ page, url }: { page: string; url: string }) {
<DropdownMenuTrigger asChild className="hidden sm:flex">
{trigger}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="shadow-none">
<DropdownMenuContent
align="end"
className="animate-none! rounded-lg shadow-none"
>
{Object.entries(menuItems).map(([key, value]) => (
<DropdownMenuItem key={key} asChild>
{value(url)}
@@ -129,13 +196,13 @@ export function DocsCopyPage({ page, url }: { page: string; url: string }) {
</DropdownMenu>
<Separator
orientation="vertical"
className="!bg-foreground/10 absolute top-0 right-8 z-0 !h-8 peer-focus-visible:opacity-0 sm:right-7 sm:!h-7"
className="!bg-foreground/5 absolute top-1 right-8 z-0 !h-6 peer-focus-visible:opacity-0 sm:right-7 sm:!h-5"
/>
<PopoverTrigger asChild className="flex sm:hidden">
{trigger}
</PopoverTrigger>
<PopoverContent
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-sm backdrop-blur-sm"
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-none backdrop-blur-sm"
align="start"
>
{Object.entries(menuItems).map(([key, value]) => (

View File

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

View File

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

View File

@@ -107,7 +107,7 @@ export function DocsTableOfContents({
return (
<div className={cn("flex flex-col gap-2 p-4 pt-0 text-sm", className)}>
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs">
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
On This Page
</p>
{toc.map((item) => (

View File

@@ -0,0 +1,119 @@
"use client"
import * as React from "react"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/examples/base/ui/select"
import { cn } from "@/lib/utils"
export type Language = "en" | "ar" | "he"
export type Direction = "ltr" | "rtl"
export type Translations<
T extends Record<string, string> = Record<string, string>,
> = Record<
Language,
{
dir: Direction
values: T
}
>
export const languageOptions = [
{ value: "en", label: "English" },
{ value: "ar", label: "Arabic (العربية)" },
{ value: "he", label: "Hebrew (עברית)" },
] as const
type LanguageContextType = {
language: Language
setLanguage: (language: Language) => void
}
const LanguageContext = React.createContext<LanguageContextType | undefined>(
undefined
)
export function LanguageProvider({
children,
defaultLanguage = "ar",
}: {
children: React.ReactNode
defaultLanguage?: Language
}) {
const [language, setLanguage] = React.useState<Language>(defaultLanguage)
return (
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
)
}
export function useLanguageContext() {
const context = React.useContext(LanguageContext)
return context
}
export function useTranslation<T extends Record<string, string>>(
translations: Translations<T>,
defaultLanguage: Language = "ar"
) {
const context = useLanguageContext()
const [localLanguage, setLocalLanguage] =
React.useState<Language>(defaultLanguage)
const language = context?.language ?? localLanguage
const setLanguage = context?.setLanguage ?? setLocalLanguage
const { dir, values: t } = translations[language]
return { language, setLanguage, dir, t }
}
export interface LanguageSelectorProps {
value: Language
onValueChange: (value: Language) => void
}
export function LanguageSelector({
value,
onValueChange,
className,
}: LanguageSelectorProps & {
className?: string
}) {
return (
<Select
items={languageOptions}
value={value}
onValueChange={(value) => onValueChange(value as Language)}
>
<SelectTrigger
size="sm"
className={cn("absolute top-4 right-4 z-50 w-36", className)}
dir="ltr"
>
<SelectValue />
</SelectTrigger>
<SelectContent
dir="ltr"
className="data-closed:animate-none data-open:animate-none"
>
<SelectGroup>
{languageOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)
}

View File

@@ -2,10 +2,11 @@
import * as React from "react"
import Link, { type LinkProps } from "next/link"
import { useRouter } from "next/navigation"
import { usePathname, useRouter } from "next/navigation"
import { PAGES_NEW } from "@/lib/docs"
import { showMcpDocs } from "@/lib/flags"
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
import { type source } from "@/lib/source"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york-v4/ui/button"
@@ -16,19 +17,31 @@ import {
} from "@/registry/new-york-v4/ui/popover"
const TOP_LEVEL_SECTIONS = [
{ name: "Get Started", href: "/docs" },
{ name: "Introduction", href: "/docs" },
{
name: "Components",
href: "/docs/components",
},
{
name: "Installation",
href: "/docs/installation",
},
{
name: "Directory",
href: "/docs/directory",
},
{
name: "RTL",
href: "/docs/rtl",
},
{
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Registry",
href: "/docs/registry",
},
{
name: "Forms",
href: "/docs/forms",
@@ -49,6 +62,8 @@ export function MobileNav({
className?: string
}) {
const [open, setOpen] = React.useState(false)
const pathname = usePathname()
const currentBase = getCurrentBase(pathname)
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -125,31 +140,30 @@ export function MobileNav({
<div className="flex flex-col gap-8">
{tree?.children?.map((group, index) => {
if (group.type === "folder") {
const pages = getPagesFromFolder(group, currentBase)
return (
<div key={index} className="flex flex-col gap-4">
<div className="text-muted-foreground text-sm font-medium">
{group.name}
</div>
<div className="flex flex-col gap-3">
{group.children.map((item) => {
if (item.type === "page") {
if (!showMcpDocs && item.url.includes("/mcp")) {
return null
}
return (
<MobileLink
key={`${item.url}-${index}`}
href={item.url}
onOpenChange={setOpen}
className="flex items-center gap-2"
>
{item.name}{" "}
{PAGES_NEW.includes(item.url) && (
<span className="flex size-2 rounded-full bg-blue-500" />
)}
</MobileLink>
)
{pages.map((item) => {
if (!showMcpDocs && item.url.includes("/mcp")) {
return null
}
return (
<MobileLink
key={`${item.url}-${index}`}
href={item.url}
onOpenChange={setOpen}
className="flex items-center gap-2"
>
{item.name}{" "}
{PAGES_NEW.includes(item.url) && (
<span className="flex size-2 rounded-full bg-blue-500" />
)}
</MobileLink>
)
})}
</div>
</div>

View File

@@ -23,7 +23,7 @@ function PageHeaderHeading({
return (
<h1
className={cn(
"text-primary leading-tighter max-w-2xl text-4xl font-semibold tracking-tight text-balance lg:leading-[1.1] lg:font-semibold xl:text-5xl xl:tracking-tight",
"text-primary leading-tighter max-w-2xl text-4xl font-semibold tracking-tight text-balance lg:leading-[1.1] lg:font-semibold xl:text-5xl xl:tracking-tighter",
className
)}
{...props}

View File

@@ -11,7 +11,7 @@ import {
} from "@/registry/new-york-v4/ui/input-group"
export const SearchDirectory = () => {
const { query, setQuery } = useSearchRegistry()
const { query, registries, setQuery } = useSearchRegistry()
const onQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
@@ -29,6 +29,12 @@ export const SearchDirectory = () => {
value={query}
onChange={onQueryChange}
/>
<InputGroupAddon align="inline-end">
<span className="text-muted-foreground tabular-nums sm:text-xs">
{registries.length}{" "}
{registries.length === 1 ? "registry" : "registries"}
</span>
</InputGroupAddon>
<InputGroupAddon
align="inline-end"
data-disabled={!query.length}

View File

@@ -2,7 +2,7 @@ import { siteConfig } from "@/lib/config"
export function SiteFooter() {
return (
<footer className="group-has-[.section-soft]/body:bg-surface/40 3xl:fixed:bg-transparent group-has-[.docs-nav]/body:pb-20 group-has-[[data-slot=designer]]/body:hidden group-has-[.docs-nav]/body:sm:pb-0 dark:bg-transparent">
<footer className="group-has-[.section-soft]/body:bg-surface/40 3xl:fixed:bg-transparent dark:group-has-[.section-soft]/body:bg-surface/40 group-has-[.docs-nav]/body:pb-20 group-has-[[data-slot=designer]]/body:hidden group-has-[[data-slot=docs]]/body:hidden group-has-[.docs-nav]/body:sm:pb-0 dark:bg-transparent">
<div className="container-wrapper px-4 xl:px-6">
<div className="flex h-(--footer-height) items-center justify-between">
<div className="text-muted-foreground w-full px-1 text-center text-xs leading-loose sm:text-sm">

View File

@@ -1,4 +1,4 @@
const SHOW = false
const SHOW = true
export function TailwindIndicator({
forceMount = false,

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,22 @@ title: Registry Directory
description: Discover community registries for shadcn/ui components and blocks.
---
import { TriangleAlertIcon } from "lucide-react"
These registries are built into the CLI with no additional configuration required. To add a component, run: `npx shadcn add @<registry>/<component>`.
<DirectoryList />
<Callout
type="warning"
className="border-amber-200 bg-amber-50 font-semibold dark:border-amber-900 dark:bg-amber-950"
>
Community registries are maintained by third-party developers. Always review
code on installation to ensure it meets your security and quality standards.
</Callout>
Don't see a registry? Learn how to [add it here](/docs/registry/registry-index).
<DirectoryList />
## Documentation
You can use the `shadcn` CLI to run your own code registry. Running your own registry allows you to distribute your custom components, hooks, pages, config, rules and other files to any project.

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