Compare commits

...

611 Commits

Author SHA1 Message Date
Claude
95e8ef9939 Fix tracking empty preset code in init routes
Guard against tracking an empty preset value in both /init and /init/v0
routes. When no valid preset code is in the URL, getPresetCode encodes
one from the parsed config params. The conditional spread ensures an
empty string never reaches analytics.

https://claude.ai/code/session_018Mm7WJAaSUGSqamSm3RiyC
2026-03-21 10:15:16 +00:00
Claude
900f099583 Fix tracking empty preset code in init route
When no valid preset code is provided in the URL, encode one from the
parsed config params instead of tracking an empty value.

https://claude.ai/code/session_018Mm7WJAaSUGSqamSm3RiyC
2026-03-21 10:05:48 +00:00
shadcn
5b40b9de5a Revert "feat: update customizer (#10129)" (#10130)
This reverts commit e327cef2c1.
2026-03-20 16:56:57 +04:00
shadcn
e327cef2c1 feat: update customizer (#10129) 2026-03-20 16:46:40 +04:00
github-actions[bot]
563d572ba0 chore(release): version packages (#10120)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-19 23:36:28 +04:00
shadcn
687f09817b feat: chartColor and fontHeading (#10115)
* feat: chart color

* fix

* fix

* fix: chart color

* chore: changeset

* chore: restore directory registry formatting

* feat: add fontHeading

* feat: rebuild registry

* fix: v0

* refactor

* fix

* fix

* fix

* fix

* fix

* fix: refactor preset handling

* fix

* fix

* fix
2026-03-19 23:35:01 +04:00
shadcn
31dbc6fc91 Merge branch 'main' of github.com:shadcn-ui/ui 2026-03-18 10:53:27 +04:00
Victor Williams
8db2be8b09 feat: add @nexus-ui registry to public directory (#10067) 2026-03-18 10:11:15 +04:00
shadcn
a8bd00466a chore(templates): bump minor dependencies (#10076)
* fix: Update import path for Button component in react-router-app template

* chore(templates): bump minor dependencies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Aboubakary Cissé <58236609+Aboubakary833@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:56:02 +04:00
shadcn
e78bb7b4f3 feat: move base picker to project form (#10077)
* feat: move base picker to project form

* fix: format
2026-03-17 09:55:37 +04:00
Aboubakary Cissé
acaa0953df fix: Update import path for Button component in react-router-app template (#10073) 2026-03-17 08:43:46 +04:00
shadcn
632e2c012e fix: update skill and add allowed-tools (#10075) 2026-03-17 08:26:13 +04:00
Danila Yudin
78f6a8b0f0 Add @sabraman ui registry (#10054)
* Add new UI component entry for @sabraman

* fix: update registries.json
2026-03-17 08:07:46 +04:00
Aboubakary Cissé
a9f997d00a fix: Update import path for Button component in react-router-app template 2026-03-17 02:25:06 +00:00
shadcn
dbe1fa76b3 fix(tests): fix e2e sleep (#10061)
* fix(tests): wait for registry readiness in global setup instead of per-test sleep

The first e2e test was flaky on CI because `start-server-and-test` only
checks that the root URL (http://localhost:4000) responds before running
tests, not the /r registry endpoint. The existing 2-second hardcoded sleep
in the first test was unreliable on slower CI runners.

Move the readiness check into the vitest globalSetup so all tests wait for
the registry /r endpoint to actually be reachable before any test starts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): fix race condition in global setup - poll correct URL and CLI binary

Two issues caused the previous fix to fail:

1. Was polling `http://localhost:4000/r` which is a directory → always 404.
   Now polls `{REGISTRY_URL}/index.json`, a real static file that returns 200.

2. The v4 dev script (`pnpm --filter=shadcn build && pnpm icons:dev & next dev`)
   runs the shadcn CLI build in the background while next dev starts immediately.
   On fast CI runs start-server-and-test can detect the server as ready before
   the CLI binary (packages/shadcn/dist/index.js) has been built, causing the
   first test to fail when it tries to invoke the CLI.
   Now explicitly waits for the binary to exist before any test runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): warm up /init route in global setup to prevent first-test timeout

The CLI's first request during `shadcn init` hits the dynamic Next.js /init
route. On a cold dev server this route takes ~1.8s to compile. Combined with
the rest of what init does (pnpm install, file writes), this pushes the first
test over the 30s CLI timeout on CI. Subsequent tests pass because the route
is already warm.

Polling /init in global setup ensures the route is compiled before any test
runs, making the first test's CLI invocation as fast as all subsequent ones.

Also replaced the /r/index.json poll (a static file that responds immediately
and doesn't reflect real route readiness) with the actual /init route poll,
which also naturally verifies the registry server is up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): warm up 404 route and increase default CLI timeout

Two more issues found in CI logs:

1. The CLI requests font files that don't exist (e.g. /r/styles/new-york-v4/
   font-geist.json), causing Next.js to compile /_not-found/page on the first
   404 response. That compilation takes ~4-5s on a cold dev server and is
   another hidden cost on the first test. Now triggering a 404 in global setup
   so the not-found page is compiled before any test runs.

2. The default CLI timeout of 30s is too tight for CI. Even with the /init and
   404 routes pre-warmed, pnpm install inside the fixture takes ~25s, leaving
   only ~5s of headroom. Increasing the default from 30s to 60s gives a
   comfortable buffer without masking real hangs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 16:16:16 +04:00
shadcn
74c4c7508b docs: review all docs (#10058)
* docs: review all docs

* fix

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

* docs(spinner): reintroduce data-icon attribute guidance in radix spinner docs (#10059)

* Initial plan

* docs: add data-icon attribute instructions to radix/spinner.mdx button and badge sections

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-03-16 13:29:23 +04:00
atyb a.
4809da6f9c feat(colors): add mauve, olive, mist, and taupe color palettes (#10049)
* fix:Nuqs adapter scope, select color-format component and color copy-to-clipboard

* chore: remove changeset

* feat(colors): add mauve, olive, mist, and taupe color palettes

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-03-16 08:36:19 +04:00
léo
7ffefce9e0 feat: add @0unlumen-ui registry (#9970)
* feat: add @0unlumen-ui registry

* fix: consistent registry name from @0unlumen-ui to @unlumen-ui
2026-03-15 15:21:09 +04:00
shadcn
6cad522930 chore: rebuild registry 2026-03-15 13:02:35 +04:00
shadcn
d683b05d7f chore: remove tooltip from mode switcher (#10047)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:58:00 +04:00
Frank
e000e17856 fix(registry): register next form examples (#10021) 2026-03-15 11:50:47 +04:00
shadcn
1be8f98c46 feat: add namespace validation for registries (#10033) 2026-03-15 11:07:13 +04:00
Copilot
6e476e4756 Rename @hooks registry to @shadcnhooks (#10036)
* Initial plan

* Rename @hooks registry to @shadcnhooks

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>
2026-03-15 11:06:56 +04:00
github-actions[bot]
e2d36a3a7d chore(release): version packages (#10046)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-15 10:07:48 +04:00
shadcn
a97ebe54f1 fix: bundle @antfu/ni to resolve tinyexec missing module error (#10041)
* fix: bundle @antfu/ni to resolve tinyexec missing module error

Fixes #10028.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: changeset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 10:01:15 +04:00
Ahmed Zougari
b2cc0dfe59 fix(docs): update tanstack commands (#10045) 2026-03-15 10:00:58 +04:00
github-actions[bot]
af99d4ebd3 chore(release): version packages (#10037) 2026-03-14 18:50:45 +04:00
shadcn
a0a072dcdd Merge pull request #9929 from kapishdima/fix/registry-font
Add fontsource and support override for registry:font install
2026-03-14 18:30:41 +04:00
KapishDima
447c7aac06 Merge branch 'main' into fix/registry-font 2026-03-14 16:29:44 +02:00
shadcn
752615f231 Merge pull request #10032 from shadcn-ui/codex/rename-blocks-so-registry
chore: rename @blocks registry to @blocks-so
2026-03-14 18:21:18 +04:00
shadcn
f9b365bc7f chore: changeset 2026-03-14 16:13:05 +04:00
shadcn
17a1a9093a Merge branch 'fix/registry-font' of github.com:kapishdima/ui into fix/registry-font 2026-03-14 16:12:34 +04:00
shadcn
8159e98075 feat: update schema 2026-03-14 16:12:27 +04:00
shadcn
6a527b3e75 chore: rebuild registry 2026-03-14 16:12:18 +04:00
shadcn
ebe689e85c docs: update font examples 2026-03-14 16:12:07 +04:00
shadcn
8b683b44e6 Merge branch 'main' into fix/registry-font 2026-03-14 15:56:54 +04:00
shadcn
725ca574f6 Rename test to clarify non-variable font coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:37:06 +04:00
shadcn
1dc39e2484 Add dependency field to font schema for explicit fontsource package control.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:34:33 +04:00
shadcn
090556691c Revert "fix: added fountsource, @support ovveride when registry:font install, monorepo init"
This reverts commit ad99fc9a73.
2026-03-14 15:33:15 +04:00
shadcn
8e9f781cdb Merge pull request #9911 from lior-pesoa/main
Add @paletteui entry to directory.json
2026-03-14 14:13:34 +04:00
shadcn
9d7c205442 Merge branch 'main' into pr-9911 and resolve conflicts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:56:31 +04:00
shadcn
902379fa3e Merge pull request #9932 from felipemenezes098/feat/registry-add-flx
feat(registry): add @flx
2026-03-14 13:53:57 +04:00
shadcn
94dcf37add Rename @blocks registry to @blocks‑s 2026-03-14 12:39:37 +04:00
felipemenezes098
843a5e2334 feat(registry): add @flx 2026-03-13 16:57:36 -03:00
shadcn
cdaad392ae Merge pull request #10025 from Ziane-Badreddine/main
fix: swap homepage and url fields for @waves-cn registry
2026-03-13 20:53:35 +04:00
shadcn
49abe0d594 Merge branch 'main' into main 2026-03-13 20:52:16 +04:00
shadcn
eeb33ae9c9 fix: homepage and url order in registries.json 2026-03-13 20:47:35 +04:00
Ziane-Badreddine
55fa1bb7cc fix: correct homepage url in waves-cn registry entry 2026-03-13 12:45:26 +00:00
KapishDima
90bbbb7993 Merge branch 'main' into fix/registry-font 2026-03-13 09:19:02 +02:00
shadcn
fc9705665c Merge pull request #9976 from withden/patch-4
Add @pacekit-gsap registry information
2026-03-13 09:49:34 +04:00
Denish Navadiya
52c477e118 Merge branch 'main' into patch-4 2026-03-13 10:59:00 +05:30
withden
41a4573002 Add @pacekit-gsap to registry directory 2026-03-13 10:58:23 +05:30
shadcn
f813fb5884 Merge pull request #10024 from shadcn-ui/chore/remove-deprecated
chore: remove deprecated/ directory and related workflow
2026-03-13 09:24:37 +04:00
shadcn
429c001412 chore: remove deprecated/ directory and related workflow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:19:17 +04:00
shadcn
cd7743cbc1 Merge pull request #9923 from tsubasakong/lucas/fix-shadcn-ui-9914-preserve-base-on-preset-switch
docs: preserve current base when switching preset codes
2026-03-13 09:00:27 +04:00
shadcn
cadc3f96de chore: small nit in phrasing 2026-03-13 08:59:07 +04:00
shadcn
a74515d6e1 Merge pull request #9973 from jal-co/add-jalco-registry
feat(registry): add @jalco registry
2026-03-13 08:56:28 +04:00
shadcn
0df9af0d75 Merge pull request #9935 from Ziane-Badreddine/main
feat: add @waves-cn registry
2026-03-13 08:56:04 +04:00
shadcn
e653c1a833 Merge pull request #10023 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-03-13 08:40:36 +04:00
github-actions[bot]
8b819e1db4 chore(release): version packages 2026-03-13 04:35:24 +00:00
shadcn
5236bfdf07 Merge pull request #10022 from shadcn-ui/chore/monorepo-pm-changeset
chore: add changeset for monorepo package manager fix
2026-03-13 08:34:26 +04:00
shadcn
7e93eb81ea chore: changeset 2026-03-13 08:33:28 +04:00
shadcn
abf1555a65 Merge pull request #9962 from devinscodebase/fix/monorepo-package-manager-detection
fix: monorepo templates ignore the user's package manager
2026-03-13 08:25:53 +04:00
shadcn
584db77fee Merge branch 'main' into fix/monorepo-package-manager-detection 2026-03-13 08:17:12 +04:00
shadcn
3faa91d670 tests: add more tests coverage 2026-03-13 08:15:56 +04:00
Justin Levine
2bf8ef86b9 fix: revert unintended changes and run registry:build 2026-03-12 21:12:44 -07:00
shadcn
624a4fe320 Merge branch 'main' into patch-4 2026-03-13 08:03:23 +04:00
shadcn
5508b5e4ec Merge branch 'main' into add-jalco-registry 2026-03-13 08:01:39 +04:00
shadcn
3af2ba80e8 Merge pull request #9967 from thomasyuill-livekit/update-agents-ui-registry-domain-to-dot-com
update agents-ui registry from livekit.io to livekit.com
2026-03-13 08:01:27 +04:00
Justin Levine
40a00278ab Merge branch 'main' into add-jalco-registry 2026-03-12 14:08:03 -07:00
Denish Navadiya
5ab89f3ae3 Add @pacekit-gsap registry information 2026-03-12 17:10:30 +05:30
KapishDima
40dc195fad Merge branch 'main' into fix/registry-font 2026-03-12 09:19:14 +02:00
Devin Alexander
ca374ad0a0 Merge branch 'main' into fix/monorepo-package-manager-detection 2026-03-12 08:49:11 +02:00
shadcn
bc9f556c38 docs: fix headings in changelog 2026-03-12 08:00:37 +04:00
Justin Levine
d06e54d2bb feat(registry): add @jalco registry 2026-03-11 18:53:23 -07:00
Ziane-Badreddine
65ddce2886 chore: rebase on main 2026-03-11 19:45:55 +00:00
Thomas Yuill
f413598ba3 update agents-ui registry from livekit.io to livekit.com 2026-03-11 14:17:06 -04:00
Devin Alexander
34c04d5f01 style: fix prettier formatting in create-template.ts 2026-03-11 16:27:30 +02:00
Devin Alexander
0029b3b6f7 fix: respect detected package manager for monorepo templates
All monorepo templates hardcoded `packageManager: "pnpm"` which
meant running `bunx --bun shadcn@latest init --monorepo` would
still shell out to `pnpm install`, triggering Corepack and crashing
under Bun because `process.mainModule` is readonly there.

This removes the hardcoded pnpm override from every monorepo template
config so `getPackageManager()` can actually detect what the user is
running. The scaffold step now adapts the cloned template on the fly:

- strips the `packageManager` field from package.json (avoids Corepack)
- converts pnpm-workspace.yaml to a `"workspaces"` array in package.json
- removes pnpm-lock.yaml
- rewrites `workspace:*` refs to `"*"` when the detected PM is npm
  (npm doesn't support the workspace: protocol)
- picks the right install flags per PM (`--no-frozen-lockfile` for pnpm,
  nothing extra for bun/npm/yarn)

pnpm behavior is completely unchanged — `adaptWorkspaceConfig` early-
returns when the detected PM is pnpm.
2026-03-11 14:52:39 +02:00
KapishDima
75cc35272a Merge branch 'main' into fix/registry-font 2026-03-11 10:32:27 +02:00
shadcn
821ac7ee4d Merge pull request #9961 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-03-11 12:15:13 +04:00
github-actions[bot]
8df46c4ded chore(release): version packages 2026-03-11 08:14:30 +00:00
shadcn
2303ce2372 Merge pull request #9960 from shadcn-ui/shadcn/update-track
feat: update handling of init urls
2026-03-11 12:13:34 +04:00
shadcn
cf672a9575 fix 2026-03-11 12:10:25 +04:00
shadcn
5ee4567353 feat: update handling of init urls 2026-03-11 12:07:18 +04:00
shadcn
6f72dba9c4 Merge pull request #9449 from mazyar-kawa02/main
feat: add @gammaui registry with homepage, URL, description, and logo
2026-03-11 11:56:45 +04:00
shadcn
cd717896fa Merge branch 'main' into main 2026-03-11 11:56:34 +04:00
shadcn
d47562cc08 Merge pull request #9910 from LGLabGreg/feat/@shadcnmaps
feat(registry): add @shadcnmaps
2026-03-11 11:56:01 +04:00
mazyar-kawa02
aff5d7f0c1 feat: add @gammaui registry with homepage, URL, description, and logo 2026-03-11 09:58:32 +03:00
LGLabGreg
4c0be13dcc feat(registry): add @shadcnmaps 2026-03-11 06:56:21 +00:00
shadcn
afa410e47f Merge pull request #9958 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-03-11 10:51:11 +04:00
github-actions[bot]
7ee55e8bd3 chore(release): version packages 2026-03-11 06:50:39 +00:00
shadcn
e3c9a3f9dc Merge pull request #9957 from shadcn-ui/shadcn/fix-resolve-init-url
fix: cache in resolveRegistryBaseConfig
2026-03-11 10:49:43 +04:00
shadcn
aa841e35cf fix: cache in resolveRegistryBaseConfig 2026-03-11 10:48:08 +04:00
shadcn
598fb2f55a feat: track preset code 2026-03-11 10:35:46 +04:00
KapishDima
aa786280a3 Merge branch 'main' into fix/registry-font 2026-03-11 08:29:16 +02:00
shadcn
07fd9d0ea4 Merge pull request #9956 from shadcn-ui/shadcn/button-active
feat: add button active state
2026-03-11 10:28:01 +04:00
KapishDima
6ad0590d87 Merge branch 'main' into fix/registry-font 2026-03-11 08:26:52 +02:00
shadcn
ff51e9ca3c feat: add button active state 2026-03-11 10:18:00 +04:00
shadcn
7958cc6a33 Merge pull request #9688 from niculistana/add-kapwa-registry-to-directory
feat: add @kapwa to registry
2026-03-11 08:20:08 +04:00
shadcn
2871e15418 Merge pull request #9465 from codewithmehmet/add-nessra-registry
feat(registry): add @nessra-ui registry
2026-03-11 08:19:02 +04:00
shadcn
c7d57548e5 Merge pull request #9438 from monab/main
feat(registry): added new @shadcnstore registry
2026-03-11 08:18:41 +04:00
KapishDima
dac13c90f2 Merge branch 'main' into fix/registry-font 2026-03-10 20:39:50 +02:00
shadcn
50d8b764a9 Merge pull request #9951 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-03-10 22:08:45 +04:00
github-actions[bot]
90fc0b2dff chore(release): version packages 2026-03-10 18:08:00 +00:00
shadcn
d9d43d5b3b Merge pull request #9950 from shadcn-ui/shadcn/translucent-preset
chore: changeset
2026-03-10 22:07:02 +04:00
shadcn
ce2c3ca688 chore: changeset 2026-03-10 22:05:58 +04:00
shadcn
3bd1bbe858 Merge pull request #9941 from shadcn-ui/shadcn/translucent
feat: menu appearance
2026-03-10 22:01:58 +04:00
shadcn
448fb0bc06 fix 2026-03-10 21:05:02 +04:00
shadcn
09a84892d9 Merge branch 'shadcn/translucent' of github.com:shadcn-ui/ui into shadcn/translucent 2026-03-10 17:06:43 +04:00
shadcn
16da5f2a56 fix 2026-03-10 17:06:29 +04:00
shadcn
f5f2a02eda Merge branch 'main' into shadcn/translucent 2026-03-10 16:58:09 +04:00
shadcn
dad006aa1e fix 2026-03-10 16:55:09 +04:00
shadcn
20a94ddb77 fix 2026-03-10 16:28:51 +04:00
shadcn
ae733168cd fix 2026-03-10 13:29:15 +04:00
shadcn
49616d2e16 fix 2026-03-10 13:25:21 +04:00
tsubasakong
7bc47bb858 Merge remote-tracking branch 'upstream/main' into sync/pr-9923 2026-03-10 00:29:48 -07:00
KapishDima
e149aac756 Merge branch 'main' into fix/registry-font 2026-03-10 08:54:20 +02:00
shadcn
62abc6be99 Merge pull request #9936 from shadcn-ui/shadcn/fix-animations
fix: animations for sheets and navigation-menu
2026-03-10 10:45:13 +04:00
shadcn
0072c9801f Merge branch 'main' into shadcn/fix-animations 2026-03-10 10:28:42 +04:00
KapishDima
729708ad2e Merge branch 'main' into fix/registry-font 2026-03-10 08:26:03 +02:00
shadcn
a4c6504c96 Merge pull request #9698 from mickadesign/add-fluid-functionalism-registry
Add @fluid registry (Fluid Functionalism)
2026-03-10 10:13:26 +04:00
shadcn
1bd5f3d7c8 fix: animations for sheets and navigation-menu 2026-03-10 10:12:03 +04:00
Micka.design
3d6ea09c50 Merge upstream/main and resolve conflicts 2026-03-09 21:14:45 -07:00
shadcn
f45b8f3066 feat: init 2026-03-09 17:18:12 +04:00
kapishdima
ad99fc9a73 fix: added fountsource, @support ovveride when registry:font install, monorepo init 2026-03-09 12:39:36 +02:00
Lior Pesoa
da05ee321c fix: run registry:build and fix directory.json syntax
- Fix missing comma between entries in directory.json
- Fix multiline SVG logo string
- Regenerate registries.json with @paletteui entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-09 10:56:16 +02:00
shadcn
de497a36bb Merge branch 'main' of github.com:shadcn-ui/ui 2026-03-09 09:11:54 +04:00
shadcn
882a9cb145 fix: mode switcher toggle 2026-03-09 09:11:47 +04:00
shadcn
65cb5b49ff ci: add script to check registry 2026-03-09 09:11:34 +04:00
shadcn
ae6f2e67aa Merge pull request #9922 from shadcn-ui/fix/login-02-separator-bg
fix: separator color in blocks
2026-03-09 08:57:51 +04:00
Frank
d13d42d434 docs: preserve base when switching preset codes 2026-03-08 21:54:17 -07:00
shadcn
67c99dd33c fix: separator color in blocks 2026-03-09 08:53:25 +04:00
Nicu Listana
ce012faf1e Merge branch 'main' into add-kapwa-registry-to-directory 2026-03-08 17:50:11 -07:00
lior-pesoa
554a1a69a7 Add @paletteui entry to directory.json 2026-03-08 18:50:13 +02:00
shadcn
e489552614 Merge pull request #9907 from edwinvakayil/feat/iconiq-registry
feat: updating my existing registry
2026-03-08 16:15:04 +04:00
shadcn
8386198073 Merge pull request #9904 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-03-08 16:12:35 +04:00
Edwin Vakayil
9c570f1435 Merge branch 'main' into feat/iconiq-registry 2026-03-08 17:11:41 +05:30
edwiee
ed2d9a6728 feat: updating my existing registry 2026-03-08 17:09:58 +05:30
github-actions[bot]
f336513d18 chore(release): version packages 2026-03-08 10:36:11 +00:00
shadcn
5755d6aa1f Merge pull request #9903 from shadcn-ui/shadcn/fix-template-scaffold
feat(shadcn): scaffold projects from github remote
2026-03-08 14:35:15 +04:00
shadcn
e363e343b7 chore 2026-03-08 14:29:09 +04:00
shadcn
fe955258c3 fix 2026-03-08 14:21:58 +04:00
shadcn
f5ac4a0d2a feat(shadcn): scaffold projects from github remote 2026-03-08 14:17:30 +04:00
shadcn
97ed7eb35c Merge pull request #9864 from kapishdima/fix/laravel-init
fix: added laravel to validation schema
2026-03-08 13:11:35 +04:00
KapishDima
6909385aea Merge branch 'main' into fix/laravel-init 2026-03-08 11:01:25 +02:00
shadcn
8dabe113fa fix: registries 2026-03-08 12:54:48 +04:00
shadcn
f5556230f1 Merge pull request #9757 from harshjdhv/feat/registry-add-componentry
Feat/registry add componentry
2026-03-08 12:45:46 +04:00
shadcn
327551f8b6 Merge branch 'main' into feat/registry-add-componentry 2026-03-08 12:45:26 +04:00
shadcn
cdb4a4547f Merge pull request #9899 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-03-08 12:44:39 +04:00
Harsh Jadhav
52f72b9cf7 Merge branch 'main' into feat/registry-add-componentry 2026-03-08 14:13:54 +05:30
github-actions[bot]
048dac9359 chore(release): version packages 2026-03-08 08:41:12 +00:00
shadcn
f93d44730e Merge pull request #9897 from shadcn-ui/shadcn/fix-cli
fix(shadcn): fallback style resolving issue
2026-03-08 12:40:17 +04:00
shadcn
21c64cb561 fix 2026-03-08 12:39:47 +04:00
shadcn
7e405f1568 fix: --base in project form 2026-03-08 12:39:38 +04:00
shadcn
04248d752e Merge pull request #9883 from kapishdima/feat/fonttrio
feat: added fonttrio to directory.json
2026-03-08 12:33:44 +04:00
shadcn
15f6a0fe49 Merge branch 'main' into feat/fonttrio 2026-03-08 12:33:20 +04:00
shadcn
54d254100d fix 2026-03-08 12:32:58 +04:00
shadcn
6d7f3479d1 Merge pull request #9896 from shadcn-ui/fix/exclude-appledouble-from-template-archives
fix(shadcn): apple metadata files in tar
2026-03-08 12:29:09 +04:00
shadcn
5e1fca8b4e fix 2026-03-08 12:27:37 +04:00
shadcn
30229bfd14 Merge pull request #9888 from llanesluis/chore/update-registry-item-supported-types
chore(docs): update registry item types table
2026-03-08 12:26:16 +04:00
shadcn
1ce9c2dd6a fix(shadcn): apple metadata files in tar 2026-03-08 12:14:24 +04:00
shadcn
5edf9c95b7 fix(shadcn): fallback style resolving issue 2026-03-08 12:05:55 +04:00
Micka.design
35657b4d5f Add @fluid registry (Fluid Functionalism)
Adds Fluid Functionalism to the registry directory.

- Registry URL: https://fluid-functionalism.vercel.app/r/{name}.json
- Homepage: https://fluid-functionalism.vercel.app
- Open source: https://github.com/mickadesign/fluid-functionalism

Fluid components with proximity hover, spring animations, font-weight
transitions, and animated focus rings. Built on Radix primitives,
Framer Motion, and CVA.
2026-03-07 18:31:22 -08:00
Luis Llanes
b7786c4b42 chore(docs): update registry item types to include registry:base and registry:font 2026-03-07 13:27:55 -07:00
kapishdima
6ca3784b67 feat: added fonttrio to directory.json 2026-03-07 19:42:15 +02:00
KapishDima
c1b92c3175 Merge branch 'main' into fix/laravel-init 2026-03-07 09:12:30 +02:00
Harsh Jadhav
b7afa9ba73 Merge branch 'main' into feat/registry-add-componentry 2026-03-07 12:24:09 +05:30
shadcn
119d534e85 Merge pull request #9798 from ncdai/fix/block-viewer-toolbar-device-type-switch 2026-03-07 09:08:16 +04:00
kapishdima
4e416dea5e fix: added laravel to validation schema 2026-03-06 21:54:53 +02:00
shadcn
b600dd7091 Merge pull request #9862 from kapishdima/fix/changelog-command 2026-03-06 22:41:36 +04:00
kapishdima
d59e5be214 fix: added shadcn@latest to command 2026-03-06 20:37:13 +02:00
shadcn
cddbc1f3ff Merge pull request #9802 from edwinvakayil/feat/iconiq-registry
feat: add @iconiq registry
2026-03-06 22:14:01 +04:00
shadcn
7c0d413e3c Merge pull request #9801 from xxtomm/add-spell-registry
feat: add @spell registry
2026-03-06 22:13:28 +04:00
shadcn
00de8addfe Merge pull request #9861 from kapishdima/fix/docs-link
fix: fixed registry link in changelog page
2026-03-06 22:11:47 +04:00
kapishdima
869e7bb17f fix: fixed registry link in changelog page 2026-03-06 20:07:38 +02:00
Edwin Vakayil
8491d4207a Merge branch 'main' into feat/iconiq-registry 2026-03-06 23:07:08 +05:30
shadcn
6f31c22f11 Merge pull request #9733 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-03-06 21:28:32 +04:00
github-actions[bot]
a4c806ec26 chore(release): version packages 2026-03-06 17:25:54 +00:00
shadcn
1445fb769d Merge pull request #9804 from shadcn-ui/shadcn/v4
feat
2026-03-06 21:24:43 +04:00
shadcn
a6bdaa6776 Merge branch 'main' into shadcn/v4 2026-03-06 17:15:52 +04:00
shadcn
b44ca370f1 fix 2026-03-06 17:13:13 +04:00
shadcn
d2776903c2 fix 2026-03-06 17:04:18 +04:00
shadcn
936ee754b1 chore: update skills 2026-03-06 17:03:27 +04:00
edwiee
3a431547bb feat: add @iconiq registry 2026-03-06 18:19:28 +05:30
shadcn
066e1e9abd docs: add changelog 2026-03-06 16:05:21 +04:00
shadcn
b19fa88dec fix: update tracking 2026-03-06 16:05:11 +04:00
shadcn
3aa50ddc9d feat: update create page 2026-03-06 16:05:01 +04:00
xxtomm
f26db39334 feat: add @spell registry 2026-03-06 19:47:00 +09:00
shadcn
3003e9e67a chore: remove 2026-03-06 11:09:45 +04:00
shadcn
ee1303198a chore: add temporary skills 2026-03-06 11:06:27 +04:00
shadcn
acb92a8df9 Merge pull request #9799 from JugglerX/main
Added Shadcnblocks UI Kit to figma docs
2026-03-06 10:00:59 +04:00
Rob Austin
78410f9738 Added Shadcnblocks UI Kit to figma docs
docs: Added Shadcnblocks UI Kit to Figma paid section
2026-03-06 11:17:07 +10:00
shadcn
edf571debd fix 2026-03-05 21:49:44 +04:00
shadcn
257448bead fix 2026-03-05 16:40:21 +04:00
Nguyễn Chánh Đại
e9f4cfb010 fix(BlockViewerToolbar): fix device type switching not working 2026-03-05 19:25:48 +07:00
shadcn
3c5f594b94 chore: remove flags 2026-03-05 15:40:05 +04:00
shadcn
cf3f9f134a feat: add demo component 2026-03-05 15:25:53 +04:00
shadcn
a643dc6ab5 fix 2026-03-05 11:44:10 +04:00
shadcn
8c705f8af9 fix 2026-03-05 11:21:58 +04:00
shadcn
28104c684d fix 2026-03-05 10:11:39 +04:00
shadcn
eccf6a2522 fix 2026-03-05 10:07:51 +04:00
shadcn
8ba3d50d7d feat: add selector to fonts 2026-03-05 10:02:45 +04:00
shadcn
75031d4461 fix 2026-03-05 08:55:34 +04:00
shadcn
13e64ea341 fix: v0 tailwind import 2026-03-04 22:10:00 +04:00
shadcn
6034ffcd3c chore: update skills rc 2026-03-04 17:13:18 +04:00
shadcn
a749633d51 fix: rc 2026-03-04 17:03:42 +04:00
shadcn
dad8a74ab4 fix 2026-03-04 16:52:04 +04:00
shadcn
3f03d30ce5 fix: fonts 2026-03-04 15:52:02 +04:00
shadcn
3365f4ebb2 deps: configure dependabot for templates 2026-03-04 15:18:34 +04:00
shadcn
68b8932406 feat: update template 2026-03-04 14:21:03 +04:00
shadcn
a24351838a fix 2026-03-04 13:49:33 +04:00
shadcn
67b1083f3a fix 2026-03-04 13:08:07 +04:00
shadcn
aa4a97730a feat: use REGISTRY_URL 2026-03-04 13:01:04 +04:00
shadcn
02f34a3b31 fix 2026-03-04 12:14:28 +04:00
shadcn
7cebd74ce5 fix 2026-03-04 11:57:54 +04:00
shadcn
bd1d93bbbc fix 2026-03-04 10:55:10 +04:00
shadcn
37ff1a3d12 fix 2026-03-04 10:50:57 +04:00
shadcn
308ebdbd3b refactor: skills updates 2026-03-04 10:34:45 +04:00
shadcn
cb6e798b90 fix: rework v0 init route 2026-03-04 08:49:23 +04:00
shadcn
2224411358 feat: update layout 2026-03-03 21:48:46 +04:00
shadcn
586f09a0c0 fix: tests 2026-03-03 17:48:00 +04:00
shadcn
475ae744e6 Merge pull request #9644 from binnodon/add-gc-solid-registry
feat: add @gc-solid registry for SolidJS components
2026-03-03 17:15:14 +04:00
shadcn
553b6454f1 Merge branch 'main' into add-gc-solid-registry 2026-03-03 17:14:49 +04:00
shadcn
5805be2a2a Merge pull request #9745 from Shatlyk1011/patch-2
feat: add emerald-ui to registry
2026-03-03 17:14:30 +04:00
shadcn
c44d89a742 Merge branch 'main' into patch-2 2026-03-03 17:14:03 +04:00
shadcn
ce3fc7625a Merge pull request #9746 from SiphoChris/main
feat(registry): add @lmscn
2026-03-03 17:13:38 +04:00
shadcn
2532aeaa1d fix 2026-03-03 17:09:10 +04:00
shadcn
a4dafd1b32 fix 2026-03-03 16:38:10 +04:00
shadcn
07c87ff431 fix 2026-03-03 14:08:21 +04:00
shadcn
4a4b379f21 fix 2026-03-03 14:05:04 +04:00
shadcn
837e2bcc93 feat: rework create page 2026-03-03 13:57:18 +04:00
Binnodon
33dc7ea273 chore: add logo SVG to @gc-solid registry entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:07:05 -06:00
shadcn
b8da7ce8b8 fix 2026-03-02 22:10:52 +04:00
shadcn
da3c255575 fix 2026-03-02 21:01:52 +04:00
shadcn
5eaad6ea6c refactor: use twmerge for css 2026-03-02 20:55:46 +04:00
shadcn
f68e240293 fix 2026-03-02 20:55:27 +04:00
Binnodon
ddc68e480a Merge remote-tracking branch 'upstream/main' into add-gc-solid-registry 2026-03-02 10:51:56 -06:00
Binnodon
c31ebfaf6b chore: rebuild registries.json with @gc-solid entry
Run `pnpm registry:build` to generate the built registry JSON
as requested by maintainer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:48:33 -06:00
shadcn
e79f6e74bb feat: update --diff and --view 2026-03-02 16:15:48 +04:00
shadcn
57f9d875be fix 2026-03-02 14:22:31 +04:00
shadcn
a59144d8e1 style: format 2026-03-02 14:01:31 +04:00
shadcn
3d8837bddb Merge branch 'main' into shadcn/v4
# Conflicts:
#	apps/v4/app/(create)/components/customizer.tsx
#	apps/v4/app/(create)/components/item-picker.tsx
#	apps/v4/app/(create)/components/lock-button.tsx
#	apps/v4/app/(create)/components/picker.tsx
#	apps/v4/app/(create)/components/preview-controls.tsx
#	apps/v4/app/(create)/components/preview.tsx
#	apps/v4/app/(create)/components/random-button.tsx
#	apps/v4/app/(create)/components/reset-button.tsx
#	apps/v4/app/(create)/components/toolbar-controls.tsx
#	apps/v4/app/(create)/create/page.tsx
#	apps/v4/components/docs-sidebar.tsx
#	apps/v4/components/site-header.tsx
#	apps/v4/examples/base/ui-rtl/tooltip.tsx
#	apps/v4/examples/base/ui/tooltip.tsx
#	apps/v4/examples/radix/ui-rtl/tooltip.tsx
#	apps/v4/examples/radix/ui/tooltip.tsx
#	apps/v4/package.json
#	apps/v4/public/r/styles/base-lyra/chatgpt.json
#	apps/v4/public/r/styles/base-lyra/elevenlabs.json
#	apps/v4/public/r/styles/base-lyra/github.json
#	apps/v4/public/r/styles/base-lyra/input-group.json
#	apps/v4/public/r/styles/base-lyra/preview.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-lyra/vercel.json
#	apps/v4/public/r/styles/base-maia/chatgpt.json
#	apps/v4/public/r/styles/base-maia/elevenlabs.json
#	apps/v4/public/r/styles/base-maia/github.json
#	apps/v4/public/r/styles/base-maia/preview.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-maia/vercel.json
#	apps/v4/public/r/styles/base-mira/chatgpt.json
#	apps/v4/public/r/styles/base-mira/elevenlabs.json
#	apps/v4/public/r/styles/base-mira/github.json
#	apps/v4/public/r/styles/base-mira/input-group.json
#	apps/v4/public/r/styles/base-mira/preview.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-mira/vercel.json
#	apps/v4/public/r/styles/base-nova/chatgpt.json
#	apps/v4/public/r/styles/base-nova/elevenlabs.json
#	apps/v4/public/r/styles/base-nova/github.json
#	apps/v4/public/r/styles/base-nova/preview.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-nova/vercel.json
#	apps/v4/public/r/styles/base-vega/chatgpt.json
#	apps/v4/public/r/styles/base-vega/elevenlabs.json
#	apps/v4/public/r/styles/base-vega/github.json
#	apps/v4/public/r/styles/base-vega/preview.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/base-vega/vercel.json
#	apps/v4/public/r/styles/radix-lyra/chatgpt.json
#	apps/v4/public/r/styles/radix-lyra/elevenlabs.json
#	apps/v4/public/r/styles/radix-lyra/github.json
#	apps/v4/public/r/styles/radix-lyra/input-group.json
#	apps/v4/public/r/styles/radix-lyra/preview.json
#	apps/v4/public/r/styles/radix-lyra/tooltip-example.json
#	apps/v4/public/r/styles/radix-lyra/tooltip.json
#	apps/v4/public/r/styles/radix-lyra/vercel.json
#	apps/v4/public/r/styles/radix-maia/chatgpt.json
#	apps/v4/public/r/styles/radix-maia/elevenlabs.json
#	apps/v4/public/r/styles/radix-maia/github.json
#	apps/v4/public/r/styles/radix-maia/preview.json
#	apps/v4/public/r/styles/radix-maia/tooltip-example.json
#	apps/v4/public/r/styles/radix-maia/tooltip.json
#	apps/v4/public/r/styles/radix-maia/vercel.json
#	apps/v4/public/r/styles/radix-mira/chatgpt.json
#	apps/v4/public/r/styles/radix-mira/elevenlabs.json
#	apps/v4/public/r/styles/radix-mira/github.json
#	apps/v4/public/r/styles/radix-mira/input-group.json
#	apps/v4/public/r/styles/radix-mira/preview.json
#	apps/v4/public/r/styles/radix-mira/tooltip-example.json
#	apps/v4/public/r/styles/radix-mira/tooltip.json
#	apps/v4/public/r/styles/radix-mira/vercel.json
#	apps/v4/public/r/styles/radix-nova/chatgpt.json
#	apps/v4/public/r/styles/radix-nova/elevenlabs.json
#	apps/v4/public/r/styles/radix-nova/github.json
#	apps/v4/public/r/styles/radix-nova/preview.json
#	apps/v4/public/r/styles/radix-nova/tooltip-example.json
#	apps/v4/public/r/styles/radix-nova/tooltip.json
#	apps/v4/public/r/styles/radix-nova/vercel.json
#	apps/v4/public/r/styles/radix-vega/chatgpt.json
#	apps/v4/public/r/styles/radix-vega/elevenlabs.json
#	apps/v4/public/r/styles/radix-vega/github.json
#	apps/v4/public/r/styles/radix-vega/preview.json
#	apps/v4/public/r/styles/radix-vega/tooltip-example.json
#	apps/v4/public/r/styles/radix-vega/tooltip.json
#	apps/v4/public/r/styles/radix-vega/vercel.json
#	apps/v4/registry/bases/base/blocks/chatgpt.tsx
#	apps/v4/registry/bases/base/blocks/elevenlabs.tsx
#	apps/v4/registry/bases/base/blocks/github.tsx
#	apps/v4/registry/bases/base/blocks/preview.tsx
#	apps/v4/registry/bases/base/blocks/vercel.tsx
#	apps/v4/registry/bases/radix/blocks/chatgpt.tsx
#	apps/v4/registry/bases/radix/blocks/elevenlabs.tsx
#	apps/v4/registry/bases/radix/blocks/github.tsx
#	apps/v4/registry/bases/radix/blocks/preview.tsx
#	apps/v4/registry/bases/radix/blocks/vercel.tsx
2026-03-02 14:00:50 +04:00
shadcn
4d89b13e6f fix 2026-03-02 13:57:42 +04:00
shadcn
7d9689ba01 Merge pull request #9768 from shadcn-ui/shadcn/fix-prettier
style: run format on all components
2026-03-02 13:55:40 +04:00
shadcn
81a1dde380 fix 2026-03-02 13:47:25 +04:00
shadcn
8448acdf90 fix 2026-03-02 13:42:11 +04:00
shadcn
51b867e5dc fix 2026-03-02 13:32:12 +04:00
shadcn
c97ab6ee18 fix 2026-03-02 13:18:27 +04:00
shadcn
9584703534 fix 2026-03-02 13:08:24 +04:00
shadcn
f31ed81983 style: run format on all components 2026-03-02 12:49:00 +04:00
shadcn
e85a698821 fix: handling of fonts 2026-03-02 12:41:50 +04:00
shadcn
2bb09a50a1 refactor: theme colors 2026-03-02 12:41:23 +04:00
shadcn
17ed9baedb fix 2026-03-02 12:30:49 +04:00
shadcn
b40685050d refactor: presets 2026-03-02 11:19:59 +04:00
shadcn
0dab4f92ac fix: esling config for start-monorepo 2026-03-02 11:19:35 +04:00
shadcn
0ddc3503a5 feat: add new serif fonts 2026-03-02 11:19:19 +04:00
Shatlyk
29ea3a7d67 feat: add Emerald UI registry 2026-03-02 11:54:29 +05:00
sinkecpt025
823a1a42b4 feat: add @lmscn registry 2026-03-02 08:25:17 +02:00
shadcn
0b66b1c473 feat: new menu links 2026-03-02 10:15:13 +04:00
shadcn
934afbcf15 docs: review and update docs 2026-03-02 10:14:31 +04:00
shadcn
e0c924d2f4 Merge pull request #9705 from Debbl/feat/update-shadcn-hooks-link
feat: update shadcn-hooks link
2026-03-02 09:40:48 +04:00
shadcn
a92b56491e fix: sidebar demo 2026-03-02 09:37:28 +04:00
shadcn
6dcd9f4fef Merge pull request #9758 from nkurunziza-saddy/fix/base-sidebar-tooltip-render-link
fix(base-sidebar): fix tooltip rendering with render prop
2026-03-02 09:09:37 +04:00
shadcn
f5c36e520e Merge branch 'main' into fix/base-sidebar-tooltip-render-link 2026-03-02 08:59:05 +04:00
shadcn
fb2a3433e2 Merge pull request #9755 from Shitanshukumar607/fix-missing-items
fix the missing items in select page (base ui)
2026-03-01 11:32:42 +04:00
Shitanshu Kumar
87ddddf41e Merge branch 'main' into fix-missing-items 2026-03-01 11:09:13 +05:30
Shatlyk
45c8c1b873 Merge branch 'main' into patch-2 2026-03-01 09:01:06 +05:00
nkurunziza-saddy
68c9ada079 fix(base-sidebar): fix tooltip rendering with render prop 2026-02-28 15:23:27 +02:00
jadhavharshh
16a0473b10 feat: add @componentry registry to available registries and directory. 2026-02-28 17:18:52 +05:30
jadhavharshh
4210d1ab05 add new registry componentry 2026-02-28 17:10:11 +05:30
shadcn
bb7cf2c425 Merge pull request #9749 from alamenai/feat/add-terrae-registry
feat: add @terrae registry
2026-02-28 13:07:31 +04:00
Shitanshu Kumar
1a67379f57 fix the missing items in select page (base ui) 2026-02-28 12:55:48 +05:30
Nicu Listana
d99fcf4a1c Merge branch 'main' into add-kapwa-registry-to-directory 2026-02-27 12:56:04 -08:00
shadcn
9954e2b014 feat: update skill 2026-02-27 21:53:16 +04:00
shadcn
7d28dfdb15 fix: ensure templates are not overwritten 2026-02-27 21:53:07 +04:00
shadcn
fd9c64f416 fix: issue with fonts 2026-02-27 21:52:51 +04:00
shadcn
7e766f4714 faet: add support for laravel in init 2026-02-27 21:52:41 +04:00
shadcn
9dc307f7cc fix: new colors 2026-02-27 18:35:52 +04:00
shadcn
47c0330610 fix: issue with font updates 2026-02-27 18:35:12 +04:00
shadcn
ded8a4086f style: fixes 2026-02-27 18:34:58 +04:00
Ala Eddine
f6dc35c9a1 feat: add @terrae registry
Add Terrae - composable, animated map components for React built with
TypeScript, Tailwind CSS, Mapbox GL JS, and MapLibre GL.
2026-02-27 15:34:42 +01:00
shadcn
408d15f73f feat: add serif fonts 2026-02-27 15:58:51 +04:00
shadcn
a50f6795cc feat: update skills 2026-02-27 12:21:18 +04:00
shadcn
da10396f2b feat: add new base colors: mauve, olive, mist and taupe 2026-02-27 11:46:38 +04:00
shadcn
c2f28e3ef5 feat: update skills 2026-02-27 11:45:50 +04:00
shadcn
40ab22fded fix: bug in registries 2026-02-27 11:45:30 +04:00
sinkecpt025
db0482ed1f feat(registry): add @lmscn 2026-02-27 08:34:20 +02:00
shadcn
9f8a877e8f refactor: clean up formatter 2026-02-27 10:03:18 +04:00
Shatlyk
331fe02c2a feat: add emerald-ui to registry 2026-02-27 11:01:43 +05:00
shadcn
34ee2a17c2 format 2026-02-27 09:39:15 +04:00
shadcn
8dbb61cdd4 Merge branch 'main' into shadcn/init-preset
# Conflicts:
#	apps/v4/public/r/styles/base-lyra/input-group.json
#	apps/v4/public/r/styles/base-lyra/preview.json
#	apps/v4/public/r/styles/base-lyra/registry.json
#	apps/v4/public/r/styles/base-lyra/tooltip.json
#	apps/v4/public/r/styles/base-maia/preview.json
#	apps/v4/public/r/styles/base-maia/registry.json
#	apps/v4/public/r/styles/base-maia/tooltip.json
#	apps/v4/public/r/styles/base-mira/input-group.json
#	apps/v4/public/r/styles/base-mira/preview.json
#	apps/v4/public/r/styles/base-mira/registry.json
#	apps/v4/public/r/styles/base-mira/tooltip.json
#	apps/v4/public/r/styles/base-nova/preview.json
#	apps/v4/public/r/styles/base-nova/registry.json
#	apps/v4/public/r/styles/base-nova/tooltip.json
#	apps/v4/public/r/styles/base-vega/preview.json
#	apps/v4/public/r/styles/base-vega/registry.json
#	apps/v4/public/r/styles/base-vega/tooltip.json
#	apps/v4/public/r/styles/radix-lyra/input-group.json
#	apps/v4/public/r/styles/radix-lyra/registry.json
#	apps/v4/public/r/styles/radix-lyra/tooltip.json
#	apps/v4/public/r/styles/radix-maia/registry.json
#	apps/v4/public/r/styles/radix-maia/tooltip.json
#	apps/v4/public/r/styles/radix-mira/input-group.json
#	apps/v4/public/r/styles/radix-mira/registry.json
#	apps/v4/public/r/styles/radix-mira/tooltip.json
#	apps/v4/public/r/styles/radix-nova/registry.json
#	apps/v4/public/r/styles/radix-nova/tooltip.json
#	apps/v4/public/r/styles/radix-vega/registry.json
#	apps/v4/public/r/styles/radix-vega/tooltip.json
2026-02-27 09:31:02 +04:00
shadcn
cc86750dfb fix: bugs 2026-02-27 09:30:06 +04:00
shadcn
646f884e8f feat: implement dry-run 2026-02-27 09:30:01 +04:00
shadcn
fbdf6c02c1 Merge pull request #9737 from myusubov/fix/notion-prompt-model-dropdown-min-width
fix(notion-prompt-form): replace w-48 with min-w-48 on model dropdown
2026-02-27 09:28:43 +04:00
shadcn
8ab757be8d Merge branch 'main' into fix/notion-prompt-model-dropdown-min-width 2026-02-27 09:28:30 +04:00
shadcn
b557df5840 Merge pull request #9744 from shadcn-ui/shadcn/fix-formatting
feat: ensure bases are formatted before build
2026-02-27 09:28:06 +04:00
shadcn
8271bb7f40 style 2026-02-27 09:22:59 +04:00
shadcn
0008c487e9 feat: ensure bases are formatted before build 2026-02-27 09:18:23 +04:00
shadcn
ae68204542 Merge branch 'main' into fix/notion-prompt-model-dropdown-min-width 2026-02-27 08:13:37 +04:00
Binnodon
e68e081d7f Merge remote-tracking branch 'upstream/main' into add-gc-solid-registry 2026-02-26 14:41:12 -06:00
shadcn
006dc8f9d0 feat: add shadcnuikit to registry 2026-02-26 23:42:59 +04:00
shadcn
b9b30a23e6 feat: add shadcn skills 2026-02-26 22:17:32 +04:00
shadcn
8af3cfd031 fix 2026-02-26 17:27:02 +04:00
shadcn
fae5e78292 feat: add astro template 2026-02-26 17:11:56 +04:00
shadcn
a13adf8f3a fix 2026-02-26 16:15:51 +04:00
Murad
dc89adf190 chore: run prettier format:write 2026-02-26 15:06:18 +03:00
shadcn
3fc793287b fix 2026-02-26 16:03:56 +04:00
shadcn
7d4dd65acd fix 2026-02-26 15:27:34 +04:00
shadcn
d4a2a5fe80 chore: update next 2026-02-26 15:01:15 +04:00
shadcn
d9a01999e8 fix 2026-02-26 13:42:50 +04:00
shadcn
6bb4060686 feat: add --monorepo flag and rework monorepo 2026-02-26 13:33:36 +04:00
shadcn
605246f93b feat: update variable fonts 2026-02-26 13:18:58 +04:00
shadcn
5ef76dece1 feat: add new monorepo templates 2026-02-26 13:17:43 +04:00
shadcn
d24d2e6fd0 feat: add /new page 2026-02-26 13:12:53 +04:00
shadcn
9546f3ad1e refactor: rework create page 2026-02-26 13:12:35 +04:00
Murad
6d2c00376e fix(notion-prompt-form): replace w-48 with min-w-48 on model dropdown to prevent hover clipping 2026-02-26 11:54:42 +03:00
shadcn
117136ada3 fix 2026-02-25 21:08:44 +04:00
shadcn
f130d4d8c7 fix 2026-02-25 18:10:56 +04:00
shadcn
a46eea77a6 Merge pull request #9690 from albertasaftei/patch-pixelart
chore: modified pixelact-ui description
2026-02-25 17:46:14 +04:00
shadcn
0b42927d38 Merge pull request #9708 from shadcn-ui/fix/apply-inside-utility
fix: handling of @apply inside @utility
2026-02-25 17:45:15 +04:00
shadcn
b979ca6e79 feat: rework create 2026-02-25 16:47:15 +04:00
shadcn
91ce4cc854 feat: new create page 2026-02-24 17:26:48 +04:00
shadcn
b58195e154 Merge pull request #9717 from harishsundar-okta/feat/add-auth0-registry
[Registry Directory]: add auth0 to registry directory
2026-02-24 11:20:17 +04:00
shadcn
0d3f6a0812 chore: deprecate registry:build and registry:mcp 2026-02-23 14:55:11 +04:00
shadcn
22ce4605d8 fix: exit code 2026-02-23 13:20:21 +04:00
harish-sundar_akto
474d461b1c feat(registry): add auth0 to registry directory 2026-02-23 13:30:54 +05:30
shadcn
339de90b8a fix: refactor 2026-02-23 09:58:55 +04:00
shadcn
048313aefa Merge pull request #9710 from ncdai/docs/registry-item-json
docs: add devDependencies section to registry-item-json.mdx
2026-02-22 15:02:23 +04:00
Nguyễn Chánh Đại
805f73582f fix: update husky dependency to name@1.2.0 in registry-item-json.mdx 2026-02-22 06:44:42 +07:00
Nguyễn Chánh Đại
a6ab998e5c docs: add devDependencies section to registry-item-json.mdx 2026-02-22 06:40:48 +07:00
alburt
92075c8426 Merge branch 'main' into patch-pixelart 2026-02-22 00:03:03 +01:00
shadcn
751c520865 feat: refactor 2026-02-21 22:28:47 +04:00
shadcn
4fa2ef66ed feat: add shadcn docs command 2026-02-21 22:28:26 +04:00
shadcn
aa735ef562 feat: add links to ui primitives 2026-02-21 22:27:48 +04:00
shadcn
a927f9c458 Merge branch 'main' into fix/apply-inside-utility 2026-02-21 21:48:24 +04:00
shadcn
82f03d0f1d chore: changeset 2026-02-21 21:41:37 +04:00
shadcn
40aca13fb0 fix: handling of apply directive inside utility 2026-02-21 21:41:01 +04:00
Brendan Dash
e2832bac7c feat: update shadcn-hooks link 2026-02-21 22:31:27 +08:00
shadcn
5f96916701 feat: refactor shadcn info command to output llm-friendly output 2026-02-21 15:18:05 +04:00
shadcn
4a96d95bde chore: changeset 2026-02-21 15:17:26 +04:00
shadcn
dc3eb9081a fix: radius values 2026-02-21 15:17:15 +04:00
shadcn
2ddd920e4d feat: warn if in monorepo 2026-02-21 15:17:06 +04:00
shadcn
e1e9940a04 fix: tooltip 2026-02-21 15:15:48 +04:00
Nicu Listana
bc8626c6f8 Merge branch 'main' into add-kapwa-registry-to-directory 2026-02-20 22:36:08 -08:00
shadcn
f2817b7c49 Merge pull request #9674 from dkroderos/patch-1 2026-02-21 04:21:13 +04:00
Ronny Badilla
fc79e82108 feat(shadcn): add HTTP 410 (Gone) support to registry fetcher 2026-02-21 04:09:47 +04:00
shadcn
58052634fa fix 2026-02-20 23:20:45 +04:00
shadcn
c1374c5592 fix 2026-02-20 22:49:00 +04:00
Binnodon
3a5d636345 Merge upstream main to resolve conflicts
- Resolved conflicts in apps/v4/registry/directory.json
- Kept @gc-solid registry entry in alphabetical order
- Merged upstream changes including new registries (@abui, @arc, @uicapsule, @ui-layouts, @pureui)
- Updated registry components and styles from upstream
2026-02-20 10:01:26 -06:00
shadcn
642d802eee chore: changesets 2026-02-20 16:44:37 +04:00
shadcn
76ba624dce fix 2026-02-20 16:40:41 +04:00
Albert Asaftei
01d5f034b9 chore: modified pixelact-ui description 2026-02-20 09:58:43 +01:00
shadcn
b7ced9f289 Merge branch 'main' into shadcn/init-preset 2026-02-20 10:11:36 +04:00
Nicu Listana
1df2bf4d9b feat: add kapwa to registry 2026-02-19 21:47:23 -08:00
emir
9c39e1ddc9 feat: arc registry 2026-02-19 23:03:36 +04:00
shadcn
bbac1cb663 fix 2026-02-19 20:13:49 +04:00
shadcn
3bc23a60c7 feat: create alias 2026-02-19 15:46:48 +04:00
shadcn
c171ae4761 fix 2026-02-19 13:45:00 +04:00
shadcn
b530f4928e Merge branch 'main' into shadcn/v4-init-preset 2026-02-19 12:46:42 +04:00
shadcn
9fc6afd181 fix 2026-02-19 12:46:16 +04:00
shadcn
eb3d88afbf Merge pull request #9641 from shadcn-ui/shadcn/fix-lint
fix: eslint config
2026-02-19 12:45:57 +04:00
shadcn
8ded0658d4 fix 2026-02-19 12:44:06 +04:00
shadcn
d032f81fd6 fix 2026-02-19 11:53:47 +04:00
shadcn
75becccf78 Merge branch 'main' into shadcn/v4-init-preset
# Conflicts:
#	apps/v4/registry/styles/style-maia.css
#	apps/v4/registry/styles/style-mira.css
#	apps/v4/registry/styles/style-nova.css
#	apps/v4/registry/styles/style-vega.css
2026-02-19 11:36:18 +04:00
shadcn
bfb84e2960 Merge pull request #9676 from shadcn-ui/shadcn/radio-group-icon
feat: update style of radio groups
2026-02-19 11:35:15 +04:00
shadcn
2f64c5a407 fix 2026-02-19 11:32:29 +04:00
shadcn
9e6765f4e2 feat: update style of radio groups 2026-02-19 11:28:01 +04:00
shadcn
d77c84b7c9 Merge branch 'main' into shadcn/v4-init-preset 2026-02-19 10:52:11 +04:00
shadcn
7172f787ac feat 2026-02-19 10:51:37 +04:00
David King Roderos
77f66d5357 Fix highlighted changes
On the sorting section, I've included the closing brackets of state of useReactTable() on the highlighted changes
2026-02-19 14:08:31 +08:00
shadcn
4307815c0f chore: changeset 2026-02-19 09:10:36 +04:00
shadcn
b484f36a22 feat: add preset code 2026-02-19 09:09:59 +04:00
shadcn
360a649d2a fix: base handling 2026-02-17 23:03:19 +04:00
shadcn
4bdd23291c chore: changeset 2026-02-17 22:47:16 +04:00
shadcn
1ee480122b fix 2026-02-17 22:46:50 +04:00
shadcn
382a5220e0 feat(shadcn): implement --reinstall flag 2026-02-17 22:39:49 +04:00
shadcn
627155b13c Merge pull request #9660 from shadcn-ui/shadcn/issue-9657
docs: add shadcn dependency in manual docs
2026-02-17 22:26:25 +04:00
shadcn
0ca4dd1b32 docs: add shadcn dependency in manual docs 2026-02-17 22:21:54 +04:00
shadcn
383bcc4fc1 fix 2026-02-17 15:22:53 +04:00
shadcn
8028a0d75d feat: add react-router template 2026-02-17 15:21:13 +04:00
shadcn
31c1c5eb56 fix 2026-02-17 14:47:50 +04:00
shadcn
ca9295016a Merge branch 'shadcn/init-preset' of github.com:shadcn-ui/ui into shadcn/init-preset 2026-02-17 14:45:32 +04:00
shadcn
7b90fe9833 fix 2026-02-17 14:45:26 +04:00
shadcn
330786352c Merge branch 'main' into shadcn/init-preset 2026-02-17 14:29:34 +04:00
shadcn
da309ae929 debug 2026-02-17 14:29:20 +04:00
shadcn
3877ae5328 fix 2026-02-17 14:10:35 +04:00
shadcn
9c99070d54 Merge pull request #9645 from albertasaftei/patch-1
chore: added Pixelact UI to registry
2026-02-17 14:00:59 +04:00
shadcn
5751250a7f Merge pull request #9654 from shadcn-ui/shadcn/issue-9651
fix: aria-disabled for accordion for base-ui
2026-02-17 14:00:03 +04:00
shadcn
f97ff8124c Merge branch 'main' into shadcn/issue-9651 2026-02-17 13:56:03 +04:00
shadcn
7f37ed96d1 Merge branch 'main' of github.com:shadcn-ui/ui 2026-02-17 13:55:26 +04:00
shadcn
7ff7049018 style: format 2026-02-17 13:55:10 +04:00
shadcn
ae895787c1 fix: aria-disabled for accordion for base-ui 2026-02-17 13:54:19 +04:00
shadcn
305f5c7d47 Merge pull request #9650 from shadcn-ui/shadcn/style-fixes
fix: nova dropdown sub menu radius
2026-02-17 13:50:23 +04:00
shadcn
f0d3984376 Merge branch 'main' into shadcn/style-fixes 2026-02-17 13:46:36 +04:00
shadcn
b8f355ac4f fix 2026-02-17 13:42:15 +04:00
shadcn
29195a17a7 fix 2026-02-17 13:39:34 +04:00
shadcn
e90efd4fa9 fix: nova dropdown sub menu radius 2026-02-17 13:31:13 +04:00
shadcn
70c158990d Merge pull request #9273 from dhwani1806/fix/alt-d-theme-toggle-9198
fix: update theme toggle key detection to exclude Alt key
2026-02-17 13:22:32 +04:00
Albert Asaftei
6e2efb4b55 feat: pixelact-ui registries.json file 2026-02-17 09:59:45 +01:00
shadcn
18db1a78ab fix 2026-02-17 12:53:10 +04:00
shadcn
bd87d729fd Merge branch 'main' into shadcn/init-preset 2026-02-17 12:35:00 +04:00
shadcn
a6f3ef591f test: update to use mock server 2026-02-17 12:34:45 +04:00
shadcn
aaed0a186c docs: update manual installation 2026-02-17 11:45:33 +04:00
alburt
2b74bbca5c added Pixelact UI to registry 2026-02-17 00:11:31 +01:00
Binnodon
36758f61b4 feat: add @gc-solid registry for SolidJS components
Adds gc-solid registry providing shadcn-ui components ported to SolidJS.

Registry Details:
- 57 UI components built with Kobalte primitives
- Full TypeScript support
- Vega theme
- Deployed at https://binnodon.github.io/gc-solid-ui

Registry Validation:
 Open source (MIT license)
 Valid JSON conforming to registry schema
 Flat structure with registry.json at root
 Files array does not include content property
 All components tested with direct URL installation

Documentation: https://binnodon.github.io/gc-solid-ui
Repository: https://github.com/binnodon/gc-solid-ui
2026-02-16 16:42:25 -06:00
shadcn
f9de81f032 Merge pull request #9587 from romeu-maleiane/fix/add-type-to-SidebarProvider-style-example
add React.CSSProperties type annotation to SidebarProvider style example
2026-02-16 23:34:04 +04:00
shadcn
444aa53803 Merge pull request #9585 from pavan-sh/fix/readme-links
docs: fix README links
2026-02-16 23:32:19 +04:00
shadcn
4e9f3e6e05 Merge pull request #9642 from shadcn-ui/shadcn/issue-9595
fix: remove link to calendar blocks
2026-02-16 23:30:19 +04:00
shadcn
3fc4482d7c fix: remove link to calendar blocks 2026-02-16 23:24:33 +04:00
shadcn
ad851375dd fix 2026-02-16 23:12:04 +04:00
shadcn
dd3e942057 fix 2026-02-16 23:10:24 +04:00
shadcn
dd4439c34a fix: eslint config 2026-02-16 22:58:28 +04:00
shadcn
e81d850438 Merge pull request #9640 from shadcn-ui/shadcn/issue-9614
fix: document buttonVariants for base button
2026-02-16 22:54:58 +04:00
shadcn
779453be26 fix 2026-02-16 22:44:50 +04:00
shadcn
867d341182 fix: document buttonVariants for base button 2026-02-16 22:38:37 +04:00
shadcn
78b51f9a11 fix 2026-02-16 17:54:43 +04:00
shadcn
417772dd9c Merge pull request #9638 from shadcn-ui/shadcn/issue-9635
fix: move className in badgeVariants
2026-02-16 17:44:39 +04:00
shadcn
b86885512f fix: move className in badgeVariants 2026-02-16 16:56:24 +04:00
shadcn
65381cd614 fix 2026-02-16 16:07:21 +04:00
shadcn
e3f11d8fe1 Merge branch 'main' into shadcn/init-preset 2026-02-16 15:24:17 +04:00
shadcn
093eb419a8 fix 2026-02-16 15:23:55 +04:00
shadcn
ad25490cf9 Merge pull request #9616 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-02-16 14:45:15 +04:00
github-actions[bot]
e94d3d80fa chore(release): version packages 2026-02-16 10:40:02 +00:00
shadcn
0e6b6d90bc Merge pull request #9493 from ANIBIT14/add-boldkit-registry
feat(registry): add @boldkit neubrutalism components
2026-02-16 14:39:04 +04:00
shadcn
ce1f9259bf chore: update changesets 2026-02-16 14:37:08 +04:00
shadcn
8cec12b98b feat: deprecate --css-variables flag for add 2026-02-16 14:37:02 +04:00
shadcn
028b1b2d93 feat: refactor add command 2026-02-16 14:15:24 +04:00
shadcn
d8e5d0d4f1 Merge branch 'main' into shadcn/init-preset 2026-02-16 12:28:00 +04:00
shadcn
0da9826821 Merge pull request #9624 from hongaar/clean-up-path-collisions
chore(v4): clean up path collisions
2026-02-16 11:50:44 +04:00
shadcn
b416e09e8b Merge branch 'main' into shadcn/init-preset 2026-02-15 20:32:21 +04:00
shadcn
2ef58bd75d Merge pull request #9634 from kapishdima/feat/soundcn
feat: added soundcn to directory
2026-02-15 20:30:34 +04:00
shadcn
cac794208e Merge branch 'main' into feat/soundcn 2026-02-15 20:30:11 +04:00
shadcn
a22aec8694 chore: rebuild 2026-02-15 20:29:58 +04:00
shadcn
6f11e820b5 wip 2026-02-15 20:28:01 +04:00
kapishdima
6a75b60b4f feat: added soundcn to directory 2026-02-15 17:00:30 +02:00
Aniruddha Agarwal
c494adbd87 Merge branch 'main' into add-boldkit-registry 2026-02-15 19:31:45 +05:30
anibit14
3aa0f13869 chore: run registry:build and update BoldKit description
- Updated component count: 43 components, 42 SVG shapes
- Added Vue and Nuxt framework support to description
- Regenerated registries.json as requested
2026-02-15 19:30:14 +05:30
shadcn
e9af9efaf3 fix 2026-02-14 23:16:30 +04:00
shadcn
1ecc8066db fix 2026-02-14 21:03:06 +04:00
shadcn
525775fb36 feat 2026-02-14 18:36:56 +04:00
shadcn
3e4c608aca feat: refactor to templates 2026-02-14 10:24:34 +04:00
shadcn
bd5028e331 fix 2026-02-13 17:24:38 +04:00
shadcn
4207614600 Merge branch 'main' into shadcn/init-preset 2026-02-13 16:19:07 +04:00
shadcn
e1af950724 Merge pull request #9625 from shadcn-ui/shadcn/debug-react-hook-form
fix: checkbox demo for rhf and tanstack form
2026-02-13 16:11:03 +04:00
shadcn
e91388a010 fix 2026-02-13 16:07:57 +04:00
shadcn
8648ddb528 fix 2026-02-13 16:03:19 +04:00
shadcn
feff5b6a57 fix: checkbox example for forms 2026-02-13 15:59:10 +04:00
shadcn
32198910ce wip 2026-02-13 15:19:52 +04:00
Joram van den Boezem
07f7147ff3 chore: clean up path collisions 2026-02-13 11:09:59 +01:00
shadcn
0e8a006adc Merge pull request #9615 from pavan-sh/docs/fix-available-typo
docs: fix typo in December 2023 changelog
2026-02-13 14:05:44 +04:00
codewithmehmet
4193e3c78f Merge branch 'main' into add-nessra-registry 2026-02-13 08:53:58 +01:00
Pavan Shinde
d2f91d6f1e Merge branch 'main' into docs/fix-available-typo 2026-02-12 21:22:56 +05:30
shadcn
9ed5093474 Merge branch 'main' into shadcn/init-preset 2026-02-12 11:37:50 +04:00
shadcn
a12dd019d3 feat: wip for init 2026-02-12 11:37:35 +04:00
shadcn
e53bc92f41 Merge pull request #9599 from shadcn-ui/shadcn/fix-nested-aschild
fix: handling of nested aschild transforms
2026-02-12 11:36:49 +04:00
shadcn
597a8db2d9 Merge pull request #9592 from shadcn-ui/shadcn/fix-base-layer-handling
feat: update handling of base styles
2026-02-12 11:36:32 +04:00
shadcn
0b0f639cd0 Merge pull request #9609 from prithvi-rajan-222/add-slide-cn
chore: Adding slide-cn to the registry
2026-02-12 09:30:35 +04:00
shadcn
6b4ba6bca1 Merge pull request #9613 from petekp/codex/add-tool-ui-registry-directory
registry: add @tool-ui to registry directory
2026-02-12 09:30:08 +04:00
Pavan Shinde
3cdd67b5b4 docs: fix typo in December 2023 changelog 2026-02-12 02:14:52 +00:00
Pete Petrash
2b03bc7a53 registry: add @tool-ui to open registry directory 2026-02-11 11:19:08 -08:00
Aniruddha Agarwal
f6447b8936 Merge branch 'main' into add-boldkit-registry 2026-02-11 23:15:54 +05:30
Prithvi Rajan
4069c33671 Adding slide-cn to the registry 2026-02-10 23:16:46 -08:00
shadcn
4dbc5581a5 fix 2026-02-10 18:51:25 +04:00
shadcn
3fc5c1c995 fix 2026-02-10 17:10:37 +04:00
shadcn
f123057ae5 Merge pull request #9600 from sean0205/reui-package-update
ReUI Registry Update
2026-02-10 12:24:47 +04:00
shadcn
5025ec818f fix: failing tests 2026-02-10 12:16:38 +04:00
sean0205
65c6c8146d reui registry update 2026-02-10 13:03:08 +05:00
shadcn
b93745f24a Merge branch 'main' into shadcn/init-preset 2026-02-10 11:58:17 +04:00
shadcn
bbb59c9fe1 fix: handling of nested aschild transforms 2026-02-10 11:23:36 +04:00
shadcn
fb56f6571a Merge pull request #9598 from AnmolSaini16/AnmolSaini16/add-mapcn-registry
feature: add @mapcn registry
2026-02-10 10:56:09 +04:00
Anmoldeep Singh
082af1f82c feature: add @mapcn registry 2026-02-10 12:09:05 +05:30
shadcn
f5b3a0cbad chore: changeset 2026-02-10 10:38:20 +04:00
shadcn
d602ccc224 feat: fix preset handling and templates 2026-02-10 10:34:38 +04:00
shadcn
ab54e7b7bd feat: update prompt for preset 2026-02-10 10:15:49 +04:00
shadcn
0137b07f66 feat: init 2026-02-10 09:48:17 +04:00
shadcn
ae95fbd1be Merge pull request #9591 from Rhysjosmin/patch-1
Docs:Update Sonner API Reference link in Radix UI
2026-02-09 17:25:48 +04:00
shadcn
625bd97d8b Merge branch 'shadcn/fix-base-layer-handling' of github.com:shadcn-ui/ui into shadcn/fix-base-layer-handling 2026-02-09 17:15:31 +04:00
shadcn
603fce7cd3 Merge branch 'main' into shadcn/fix-base-layer-handling 2026-02-09 17:15:23 +04:00
shadcn
c759f460d5 feat: add docs 2026-02-09 17:13:49 +04:00
shadcn
e1c00667f7 Merge branch 'main' into shadcn/fix-base-layer-handling 2026-02-09 17:05:02 +04:00
shadcn
46631fc4d4 chore: rebuild 2026-02-09 17:04:26 +04:00
shadcn
f235a5d951 feat: update handling of base styles 2026-02-09 16:31:56 +04:00
RhysJosmin
b0b711f181 Update Sonner API Reference link
fixes the same issue solved here
https://github.com/shadcn-ui/ui/pull/9440
but for radix
2026-02-09 17:10:15 +05:30
shadcn
f1b7102583 Merge branch 'main' of github.com:shadcn-ui/ui 2026-02-09 13:19:46 +04:00
shadcn
f076420e68 fix 2026-02-09 13:19:40 +04:00
shadcn
4ce0a7eaa1 Merge pull request #9577 from rezaaa/feat/calendar_hijri_font
feat(calendar): add Vazirmatn font to Persian examples
2026-02-09 12:53:11 +04:00
Reza Mahmoudi
270b730c21 Merge branch 'main' into feat/calendar_hijri_font 2026-02-09 11:52:06 +03:30
shadcn
14a6cc5999 fix: position of command menu 2026-02-09 12:10:41 +04:00
shadcn
0067873f60 fix: block viewer 2026-02-09 12:08:10 +04:00
shadcn
fc16e1461f ci: update validate registries workflow 2026-02-09 11:41:34 +04:00
shadcn
8f01916bb2 Merge pull request #9586 from mxkaske/registries/openstatus
feat(registry): add @openstatus registry
2026-02-09 11:30:45 +04:00
Romeu Maleiane
87d522f249 add React.CSSProperties type annotation to SidebarProvider style example 2026-02-09 00:21:17 +02:00
Maximilian Kaske
ead138b4cd chore: add openstatus registry 2026-02-08 21:28:57 +01:00
Pavan Shinde
ef39979548 docs: fix README links 2026-02-08 14:45:55 +00:00
Reza Mahmoudi
ab6c8caf2f Merge branch 'main' into feat/calendar_hijri_font 2026-02-08 12:05:21 +03:30
shadcn
ba9206bded chore: rebuild registry 2026-02-08 12:08:47 +04:00
Reza Mahmoudi
c5838cf955 Merge branch 'main' into feat/calendar_hijri_font 2026-02-08 00:29:42 +03:30
shadcn
0c41fc30e4 Merge pull request #9426 from inference-sh/add-inferencesh-registry
registry: add @inferencesh
2026-02-07 13:39:54 +04:00
Aniruddha Agarwal
8270cfa39e Merge branch 'main' into add-boldkit-registry 2026-02-07 14:04:03 +05:30
Reza Mahmoudi
06e356cab9 feat(calendar): add Vazirmatn font to Persian examples 2026-02-06 22:04:50 +03:30
shadcn
f24631dc48 Merge pull request #9573 from educlopez/add-thegridcn-registry
feat: add @thegridcn registry
2026-02-06 17:21:55 +04:00
Edu Calvo
ec936bcd06 feat: add @thegridcn registry
Add The Gridcn (thegridcn.com) - a Tron-inspired shadcn/ui theme system with Greek god color schemes, glow intensity levels, and sci-fi components.
2026-02-06 10:47:37 +01:00
shadcn
6c7975e400 feat: default to nova 2026-02-06 13:19:30 +04:00
shadcn
8acef7ab66 docs: add changelog 2026-02-06 13:10:12 +04:00
shadcn
4ddfd39b0d Merge pull request #9440 from rahman-D3V/fix-sonner-doc-link
fix: correct Sonner documentation link
2026-02-06 12:50:31 +04:00
shadcn
3ba37cc24c Merge pull request #9570 from shadcn-ui/shadcn/init-new-york
feat: add shadcn as a dependency for init
2026-02-06 12:49:35 +04:00
shadcn
da080118b0 feat: add shadcn as a dependency for init 2026-02-06 12:35:51 +04:00
shadcn
e8897ea80a Merge pull request #9255 from PhilemonChiro/fix/tanstack-form-performance
fix(docs): improve TanStack Form example performance
2026-02-06 12:27:54 +04:00
shadcn
9d26f582fa Merge pull request #9569 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-02-06 11:23:29 +04:00
github-actions[bot]
0a2ad2176c chore(release): version packages 2026-02-06 07:21:44 +00:00
shadcn
7c36439836 Merge pull request #9568 from shadcn-ui/shadcn/remove-block-message
feat: remove restricted block message
2026-02-06 11:20:44 +04:00
shadcn
a1e3afed06 chore: changeset 2026-02-06 11:14:54 +04:00
shadcn
be5b1bbae3 feat: remove restricted blocks 2026-02-06 11:14:30 +04:00
shadcn
52de23bf95 Merge pull request #9567 from shadcn-ui/shadcn/blocks-for-base
feat: create blocks for new styles
2026-02-06 11:06:19 +04:00
shadcn
1d16fe46cd Merge pull request #9555 from shadcn-ui/dependabot/npm_and_yarn/modelcontextprotocol/sdk-1.26.0
chore(deps): bump @modelcontextprotocol/sdk from 1.17.2 to 1.26.0
2026-02-06 11:01:53 +04:00
shadcn
cbecda13f9 fix 2026-02-06 10:55:44 +04:00
shadcn
24649ec103 fix 2026-02-06 10:51:42 +04:00
shadcn
b9f62a8399 feat: create blocks for new styles 2026-02-06 10:47:18 +04:00
shadcn
689d45e095 Merge branch 'main' of github.com:shadcn-ui/ui 2026-02-05 12:29:31 +04:00
shadcn
33f7b3f2bb fix: minor style updates 2026-02-05 12:29:21 +04:00
shadcn
2cce072393 Merge pull request #9558 from Dinil-Thilakarathne/feat/add-sona-ui-registry
feat: Add @sona-ui to registries and directory.
2026-02-05 12:25:14 +04:00
shadcn
d64bdec2f9 Merge branch 'main' into feat/add-sona-ui-registry 2026-02-05 12:25:02 +04:00
shadcn
5adacdecad Merge pull request #9550 from rgbkrk/add-nteract-to-registry
feat(registry): added new nteract registry
2026-02-05 12:23:11 +04:00
Dinil Thilakarathne
f2552d3f3b feat: Add @sona-ui to registries and directory. 2026-02-05 04:18:30 +05:30
dependabot[bot]
b435e01199 chore(deps): bump @modelcontextprotocol/sdk from 1.17.2 to 1.26.0
Bumps [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) from 1.17.2 to 1.26.0.
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/1.17.2...v1.26.0)

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

* chore: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2026-01-30 21:13:27 +04:00
shadcn
38de7fddc2 feat: rtl (#9498)
* feat: rtl

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add sidebar

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* chore: changeset

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2026-01-30 21:08:39 +04:00
Aniruddha Agarwal
c719d24f3a Merge branch 'main' into add-boldkit-registry 2026-01-30 19:31:09 +05:30
shadcn
4479965555 fix: directory.json 2026-01-30 16:41:57 +04:00
Denish Navadiya
7ea124b25d Rename @paceui to @pacekit with updated details 2026-01-30 17:42:39 +05:30
anibit14
f746368369 chore: update component count to 45 2026-01-30 12:14:39 +05:30
anibit14
164b6ff6c1 feat(registry): add @boldkit neubrutalism components 2026-01-30 12:04:00 +05:30
Mehmet Cinar
7ae522e610 feat(registry): add @nessra-ui to registries.json 2026-01-27 22:26:05 +01:00
Mona Brahmakshatriya
e1a0ec3061 Merge branch 'shadcn-ui:main' into main 2026-01-27 16:24:22 +05:30
monab
f8222528eb fix(registry): updated @shadcnstore description for @registries.json 2026-01-27 16:23:39 +05:30
Jaem
759003c781 Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 15:11:00 +09:00
Jaem
6529256e98 Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 11:57:41 +09:00
Mehmet Cinar
b142bd2fd5 feat(registry): add @nessra-ui registry 2026-01-26 23:10:56 +01:00
Jaem
4a39de5c56 Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 03:30:02 +09:00
Jaem
d53f7489ce Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 03:19:30 +09:00
Jaem
dfe784b44a fix(resizable): upgrade to react-resizable-panels v4
- Update component API: PanelGroup → Group, PanelResizeHandle → Separator
- Update prop: direction → orientation
- Update size values: number → string with units (e.g., "50%")
- Update CSS selectors: data-[panel-group-direction] → aria-[orientation]
- Update controlled component: onLayout → onLayoutChange with Layout type

Closes #9118, #9136, #9200
2026-01-27 03:10:54 +09:00
rahman-D3V
3dbe9e6a3e fix: correct Sonner documentation link 2026-01-24 18:32:59 +05:30
Mona Brahmakshatriya
4fa8f9b4c2 Merge branch 'shadcn-ui:main' into main 2026-01-24 17:28:33 +05:30
monab
24205601e1 feat(registry): added new @shadcnstore registry 2026-01-24 17:11:16 +05:30
dhwani1806
31f8af8409 fix: update theme toggle key detection to exclude Alt key 2026-01-05 18:40:25 +05:30
Philemon Chiro
9317a93152 fix(docs): improve TanStack Form example performance
Change validation strategy from onChange to onBlur to prevent
validation on every keystroke, which causes input lag in textareas.
2026-01-02 13:41:26 +02:00
7297 changed files with 173107 additions and 282435 deletions

View File

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

View File

@@ -0,0 +1,22 @@
---
description: Keep registry base and radix trees in sync when editing shared UI
globs: apps/v4/registry/bases/**/*
alwaysApply: false
---
# Registry bases: Base UI ↔ Radix parity
`apps/v4/registry/bases/base` and `apps/v4/registry/bases/radix` are **parallel registries**. Anything that exists in both trees for the same purpose (preview blocks, mirrored examples, shared card layouts, etc.) **must stay in sync**.
## When editing
- If you change a file under **`bases/base/...`**, apply the **same behavioral and visual change** to the matching path under **`bases/radix/...`** (and the reverse).
- Only diverge where APIs differ (e.g. import paths like `@/registry/bases/base/ui/*` vs `@/registry/bases/radix/ui/*`, or Base UI vs Radix component props).
- Do **not** update only one side unless the user explicitly asks for a single-base change.
## Typical mirrored paths
- `blocks/preview/**` — preview cards and blocks
- Parallel `ui/*` components when both exist for the same component
After edits, briefly confirm both trees were updated (or state why one side is intentionally unchanged).

View File

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

View File

@@ -4,3 +4,43 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/astro-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/astro-monorepo"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/next-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/next-monorepo"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/react-router-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/react-router-monorepo"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/start-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/start-monorepo"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/vite-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/vite-monorepo"
schedule:
interval: "weekly"

View File

@@ -77,6 +77,9 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm --filter=shadcn build
- run: pnpm format:check
tsc:

View File

@@ -1,78 +0,0 @@
name: Deprecated
on:
pull_request_target:
types: [opened, synchronize]
permissions:
issues: write
contents: read
pull-requests: write
jobs:
deprecated:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
with:
files: |
apps/www/**
files_ignore: |
apps/www/public/r/**
base_sha: ${{ github.event.pull_request.base.sha }}
sha: ${{ github.event.pull_request.head.sha }}
- name: Comment on PR if www files changed
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
const wwwFiles = changedFiles.filter(file =>
file.startsWith('apps/www/') &&
!file.startsWith('apps/www/public/r/') &&
file !== 'apps/www/package.json'
);
if (wwwFiles.length > 0) {
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
Consider applying the change to \`apps/v4\` if relevant.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
// Add deprecated label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['deprecated']
});
} else {
// Remove deprecated label if no www files are changed
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'deprecated'
});
} catch (error) {
// Label doesn't exist, which is fine
console.log('Deprecated label not found, skipping removal');
}
}

View File

@@ -19,7 +19,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 22
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -42,7 +42,4 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm build --filter=shadcn
- run: pnpm test

View File

@@ -4,13 +4,53 @@ on:
pull_request:
paths:
- "apps/v4/public/r/registries.json"
- "apps/v4/registry/directory.json"
push:
branches:
- main
paths:
- "apps/v4/public/r/registries.json"
- "apps/v4/registry/directory.json"
jobs:
check-registry-sync:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
name: check-registry-sync
permissions:
contents: read
pull-requests: write
steps:
- name: Check changed files
id: changed
env:
GH_TOKEN: ${{ github.token }}
run: |
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only)
DIRECTORY_CHANGED=false
REGISTRIES_CHANGED=false
if echo "$CHANGED_FILES" | grep -q "^apps/v4/registry/directory.json$"; then
DIRECTORY_CHANGED=true
fi
if echo "$CHANGED_FILES" | grep -q "^apps/v4/public/r/registries.json$"; then
REGISTRIES_CHANGED=true
fi
echo "directory_changed=$DIRECTORY_CHANGED" >> $GITHUB_OUTPUT
echo "registries_changed=$REGISTRIES_CHANGED" >> $GITHUB_OUTPUT
- name: Flag missing registries.json update
if: steps.changed.outputs.directory_changed == 'true' && steps.changed.outputs.registries_changed == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "registries: invalid"
gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "can you run \`pnpm registry:build\` and commit the json files please?"
exit 1
validate:
runs-on: ubuntu-latest
name: pnpm validate:registries
@@ -26,6 +66,44 @@ jobs:
with:
node-version: 20
- name: Block reserved registry namespaces
env:
RESERVED_NAMESPACES: "@shadcn,@ui,@blocks,@components,@block,@component,@util,@utils,@registry,@lib,@hook,@hooks,@theme,@themes,@chart,@charts"
run: |
node <<'EOF'
const fs = require("node:fs")
const files = [
"apps/v4/public/r/registries.json",
"apps/v4/registry/directory.json",
]
const reservedNamespaces = new Set(
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
)
function readNames(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
(entry) => entry.name
)
}
const violations = files.flatMap((filePath) => {
return readNames(filePath)
.filter((name) => reservedNamespaces.has(name))
.map((name) => `${filePath}: ${name}`)
})
if (violations.length > 0) {
console.error("Reserved registry namespaces are not allowed:")
for (const violation of violations) {
console.error(`- ${violation}`)
}
process.exit(1)
}
EOF
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
@@ -47,8 +125,5 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm build --filter=shadcn
- name: Validate registries
run: pnpm --filter=v4 validate:registries

2
.gitignore vendored
View File

@@ -41,3 +41,5 @@ tsconfig.tsbuildinfo
.vscode
.notes
.playwright-mcp
shadcn-workspace

View File

@@ -5,3 +5,4 @@ build
.contentlayer
**/fixtures
deprecated
apps/v4/registry/styles/**/*.css

View File

@@ -6,7 +6,7 @@ A set of beautifully designed components that you can customize, extend, and bui
## Documentation
Visit http://ui.shadcn.com/docs to view the documentation.
Visit https://ui.shadcn.com/docs to view the documentation.
## Contributing
@@ -14,4 +14,4 @@ Please read the [contributing guide](/CONTRIBUTING.md).
## License
Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md).
Licensed under the [MIT license](./LICENSE.md).

View File

@@ -5,3 +5,4 @@ build
.contentlayer
registry/__index__.tsx
content/docs/components/calendar.mdx
registry/styles/**/*.css

View File

@@ -93,7 +93,7 @@ export function AppearanceSettings() {
value={gpuCount}
onChange={handleGpuInputChange}
size={3}
className="h-7 !w-14 font-mono"
className="h-7 w-14! font-mono"
maxLength={3}
/>
<Button

View File

@@ -56,7 +56,7 @@ export function ButtonGroupDemo() {
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 [--radius:1rem]">
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />

View File

@@ -21,7 +21,7 @@ export function ButtonGroupPopover() {
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
<PopoverContent align="end" className="gap-0 rounded-xl p-0 text-sm">
<div className="px-4 py-3">
<div className="text-sm font-medium">Agent Tasks</div>
</div>

View File

@@ -13,6 +13,7 @@ import { Input } from "@/examples/radix/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
@@ -21,7 +22,7 @@ import { Textarea } from "@/examples/radix/ui/textarea"
export function FieldDemo() {
return (
<div className="w-full max-w-md rounded-lg border p-6">
<div className="w-full max-w-md rounded-xl border p-6">
<form>
<FieldGroup>
<FieldSet>
@@ -69,18 +70,20 @@ export function FieldDemo() {
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
<SelectItem value="01">01</SelectItem>
<SelectItem value="02">02</SelectItem>
<SelectItem value="03">03</SelectItem>
<SelectItem value="04">04</SelectItem>
<SelectItem value="05">05</SelectItem>
<SelectItem value="06">06</SelectItem>
<SelectItem value="07">07</SelectItem>
<SelectItem value="08">08</SelectItem>
<SelectItem value="09">09</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
<SelectGroup>
<SelectItem value="01">01</SelectItem>
<SelectItem value="02">02</SelectItem>
<SelectItem value="03">03</SelectItem>
<SelectItem value="04">04</SelectItem>
<SelectItem value="05">05</SelectItem>
<SelectItem value="06">06</SelectItem>
<SelectItem value="07">07</SelectItem>
<SelectItem value="08">08</SelectItem>
<SelectItem value="09">09</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
@@ -93,12 +96,14 @@ export function FieldDemo() {
<SelectValue placeholder="YYYY" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
<SelectItem value="2027">2027</SelectItem>
<SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
<SelectGroup>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
<SelectItem value="2027">2027</SelectItem>
<SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>

View File

@@ -46,11 +46,11 @@ export function FieldHear() {
<FieldLabel
htmlFor={option.value}
key={option.value}
className="!w-fit"
className="w-fit!"
>
<Field
orientation="horizontal"
className="gap-1.5 overflow-hidden !px-3 !py-1.5 transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:!px-2"
className="gap-1.5 overflow-hidden px-3! py-1.5! transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:px-2!"
>
<Checkbox
value={option.value}

View File

@@ -19,7 +19,7 @@ import { SpinnerEmpty } from "./spinner-empty"
export function RootComponents() {
return (
<div className="theme-container mx-auto grid gap-8 py-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
<div className="mx-auto grid gap-8 py-1 theme-container md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<FieldDemo />
</div>

View File

@@ -24,7 +24,7 @@ export function InputGroupButtonExample() {
Input Secure
</Label>
<InputGroup className="[--radius:9999px]">
<InputGroupInput id="input-secure-19" className="!pl-0.5" />
<InputGroupInput id="input-secure-19" className="pl-0.5!" />
<Popover>
<PopoverTrigger asChild>
<InputGroupAddon>
@@ -46,7 +46,7 @@ export function InputGroupButtonExample() {
<p>You should not enter any sensitive information on this site.</p>
</PopoverContent>
</Popover>
<InputGroupAddon className="text-muted-foreground !pl-1">
<InputGroupAddon className="pl-1! text-muted-foreground">
https://
</InputGroupAddon>
<InputGroupAddon align="inline-end">

View File

@@ -32,7 +32,7 @@ export function InputGroupDemo() {
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="example.com" className="!pl-1" />
<InputGroupInput placeholder="example.com" className="pl-1!" />
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
@@ -66,18 +66,14 @@ export function InputGroupDemo() {
<DropdownMenuTrigger asChild>
<InputGroupButton variant="ghost">Auto</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:0.95rem]"
>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText className="ml-auto">52% used</InputGroupText>
<Separator orientation="vertical" className="!h-4" />
<Separator orientation="vertical" className="h-4!" />
<InputGroupButton
variant="default"
className="rounded-full"
@@ -91,7 +87,7 @@ export function InputGroupDemo() {
<InputGroup>
<InputGroupInput placeholder="@shadcn" />
<InputGroupAddon align="inline-end">
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
<IconCheck className="size-3 text-white" />
</div>
</InputGroupAddon>

View File

@@ -42,7 +42,7 @@ export function ItemAvatar() {
</Item>
<Item variant="outline">
<ItemMedia>
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
<div className="flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background *:data-[slot=avatar]:grayscale">
<Avatar className="hidden sm:flex">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>

View File

@@ -190,12 +190,12 @@ export function NotionPromptForm() {
<FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt
</FieldLabel>
<InputGroup>
<InputGroup className="rounded-xl">
<InputGroupTextarea
id="notion-prompt"
placeholder="Ask, search, or make anything..."
/>
<InputGroupAddon align="block-start">
<InputGroupAddon align="block-start" className="pt-3">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
@@ -209,7 +209,7 @@ export function NotionPromptForm() {
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="rounded-full transition-transform"
className="transition-transform"
>
<IconAt /> {!hasMentions && "Add context"}
</InputGroupButton>
@@ -235,6 +235,7 @@ export function NotionPromptForm() {
setMentions((prev) => [...prev, currentValue])
setMentionPopoverOpen(false)
}}
className="rounded-lg"
>
<MentionableIcon item={item} />
{item.title}
@@ -246,7 +247,7 @@ export function NotionPromptForm() {
</Command>
</PopoverContent>
</Popover>
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
<div className="-m-1.5 no-scrollbar flex gap-1 overflow-y-auto p-1.5">
{mentions.map((mention) => {
const item = SAMPLE_DATA.mentionable.find(
(item) => item.title === mention
@@ -261,7 +262,7 @@ export function NotionPromptForm() {
key={mention}
size="sm"
variant="secondary"
className="rounded-full !pl-2"
className="rounded-full pl-2!"
onClick={() => {
setMentions((prev) => prev.filter((m) => m !== mention))
}}
@@ -301,9 +302,13 @@ export function NotionPromptForm() {
</TooltipTrigger>
<TooltipContent>Select AI model</TooltipContent>
</Tooltip>
<DropdownMenuContent side="top" align="start" className="w-48">
<DropdownMenuGroup className="w-48">
<DropdownMenuLabel className="text-muted-foreground text-xs">
<DropdownMenuContent
side="top"
align="start"
className="min-w-48"
>
<DropdownMenuGroup>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Select Agent Mode
</DropdownMenuLabel>
{SAMPLE_DATA.models.map((model) => (
@@ -421,7 +426,7 @@ export function NotionPromptForm() {
<DropdownMenuItem>
<IconPlus /> Connect Apps
</DropdownMenuItem>
<DropdownMenuLabel className="text-muted-foreground text-xs">
<DropdownMenuLabel className="text-xs text-muted-foreground">
We&apos;ll only search in the sources selected here.
</DropdownMenuLabel>
</DropdownMenuGroup>

View File

@@ -56,7 +56,7 @@ export default function IndexPage() {
<PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions>
<Button asChild size="sm" className="h-[31px] rounded-lg">
<Link href="/docs/installation">Get Started</Link>
<Link href="/create">New Project</Link>
</Button>
<Button asChild size="sm" variant="ghost" className="rounded-lg">
<Link href="/docs/components">View Components</Link>
@@ -64,12 +64,12 @@ export default function IndexPage() {
</PageActions>
</PageHeader>
<PageNav className="hidden md:flex">
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
<ThemeSelector className="mr-4 hidden md:flex" />
</PageNav>
<div className="container-wrapper section-soft flex-1 pb-6">
<div className="container-wrapper flex-1 section-soft pb-6">
<div className="container overflow-hidden">
<section className="border-border/50 -mx-4 w-[160vw] overflow-hidden rounded-lg border md:hidden md:w-[150vw]">
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
<Image
src="/r/styles/new-york-v4/dashboard-01-light.png"
width={1400}
@@ -87,7 +87,7 @@ export default function IndexPage() {
priority
/>
</section>
<section className="theme-container hidden md:block">
<section className="hidden theme-container md:block">
<RootComponents />
</section>
</div>

View File

@@ -71,7 +71,7 @@ export default function BlocksLayout({
<Link href="/blocks/sidebar">Browse all blocks</Link>
</Button>
</PageNav>
<div className="container-wrapper section-soft flex-1 md:py-12">
<div className="container-wrapper flex-1 section-soft md:py-12">
<div className="container">{children}</div>
</div>
</>

View File

@@ -62,7 +62,7 @@ export default function ColorsLayout({
<div className="hidden">
<div className="container-wrapper">
<div className="container flex items-center justify-between gap-8 py-4">
<ColorsNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
<ColorsNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
</div>
</div>
</div>

View File

@@ -95,12 +95,14 @@ export default async function Page(props: {
<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-24 text-4xl font-semibold tracking-tight sm:text-3xl">
<div className="flex items-center justify-between md:items-start">
<h1 className="scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl">
{doc.title}
</h1>
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:py-1.5 sm:backdrop-blur-none">
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
<div className="docs-nav flex items-center gap-2">
<div className="hidden sm:block">
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
</div>
<div className="ml-auto flex gap-2">
{neighbours.previous && (
<Button
@@ -132,7 +134,7 @@ export default async function Page(props: {
</div>
</div>
{doc.description && (
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
<p className="text-[1.05rem] text-muted-foreground sm:text-base sm:text-balance md:max-w-[80%]">
{doc.description}
</p>
)}
@@ -179,7 +181,7 @@ export default async function Page(props: {
</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="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-(--sidebar-width) flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="h-(--top-spacing) shrink-0"></div>
{doc.toc?.length ? (
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">

View File

@@ -57,7 +57,7 @@ export default function ChangelogPage() {
</a>
</Button>
</div>
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
<p className="text-[1.05rem] text-muted-foreground sm:text-base sm:text-balance md:max-w-[80%]">
Latest updates and announcements.
</p>
</div>
@@ -79,24 +79,28 @@ export default function ChangelogPage() {
})}
{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">
<h2 className="mb-6 font-heading text-xl font-semibold tracking-tight">
More Updates
</h2>
<ul className="flex flex-col gap-4">
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">
{olderPages.map((page) => {
const data = page.data as ChangelogPageData
const [date, ...titleParts] = data.title.split(" - ")
const title = titleParts.join(" - ")
return (
<li key={page.url} className="flex items-center gap-3">
<Link
href={page.url}
className="font-medium hover:underline"
>
{data.title}
</Link>
</li>
<Link
key={page.url}
href={page.url}
className="flex w-full flex-col rounded-xl bg-surface px-4 py-3 text-surface-foreground transition-colors hover:bg-surface/80"
>
<span className="text-xs text-muted-foreground">
{date}
</span>
<span className="text-sm font-medium">{title}</span>
</Link>
)
})}
</ul>
</div>
</div>
)}
</div>
@@ -106,7 +110,7 @@ export default function ChangelogPage() {
<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">
<p className="sticky top-0 h-6 bg-background text-xs font-medium text-muted-foreground">
On This Page
</p>
{latestPages.map((page) => {
@@ -115,7 +119,7 @@ export default function ChangelogPage() {
<Link
key={page.url}
href={page.url}
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
className="text-[0.8rem] text-muted-foreground no-underline transition-colors hover:text-foreground"
>
{data.title}
</Link>
@@ -124,7 +128,7 @@ export default function ChangelogPage() {
{olderPages.length > 0 && (
<a
href="#more-updates"
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
className="text-[0.8rem] text-muted-foreground no-underline transition-colors hover:text-foreground"
>
More Updates
</a>

View File

@@ -10,8 +10,12 @@ 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 [--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}
className="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)] 3xl:fixed:container 3xl:fixed:px-3"
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
} as React.CSSProperties
}
>
<DocsSidebar tree={source.pageTree} />
<div className="h-full w-full">{children}</div>

View File

@@ -43,8 +43,8 @@ export default function AuthenticationPage() {
>
Login
</Link>
<div className="text-primary relative hidden h-full flex-col p-10 lg:flex dark:border-r">
<div className="bg-primary/5 absolute inset-0" />
<div className="relative hidden h-full flex-col p-10 text-primary lg:flex dark:border-r">
<div className="absolute inset-0 bg-primary/5" />
<div className="relative z-20 flex items-center text-lg font-medium">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -74,7 +74,7 @@ export default function AuthenticationPage() {
<h1 className="text-2xl font-semibold tracking-tight">
Create an account
</h1>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
Enter your email below to create your account
</p>
</div>

View File

@@ -159,10 +159,10 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenuItem>
<SidebarMenuButton
asChild
className="data-[slot=sidebar-menu-button]:!p-1.5"
className="data-[slot=sidebar-menu-button]:p-1.5!"
>
<Link href="#">
<IconInnerShadowTop className="!size-5" />
<IconInnerShadowTop className="size-5!" />
<span className="text-base font-semibold">Acme Inc.</span>
</Link>
</SidebarMenuButton>

View File

@@ -174,7 +174,7 @@ export function ChartAreaInteractive() {
value={timeRange}
onValueChange={setTimeRange}
variant="outline"
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
className="hidden *:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex"
>
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>

View File

@@ -128,9 +128,9 @@ function DragHandle({ id }: { id: number }) {
{...listeners}
variant="ghost"
size="icon"
className="text-muted-foreground size-7 hover:bg-transparent"
className="size-7 text-muted-foreground hover:bg-transparent"
>
<IconGripVertical className="text-muted-foreground size-3" />
<IconGripVertical className="size-3 text-muted-foreground" />
<span className="sr-only">Drag to reorder</span>
</Button>
)
@@ -181,7 +181,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
header: "Section Type",
cell: ({ row }) => (
<div className="w-32">
<Badge variant="outline" className="text-muted-foreground px-1.5">
<Badge variant="outline" className="px-1.5 text-muted-foreground">
{row.original.type}
</Badge>
</div>
@@ -191,7 +191,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<Badge variant="outline" className="text-muted-foreground px-1.5">
<Badge variant="outline" className="px-1.5 text-muted-foreground">
{row.original.status === "Done" ? (
<IconCircleCheckFilled className="fill-green-500 dark:fill-green-400" />
) : (
@@ -219,7 +219,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
Target
</Label>
<Input
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
defaultValue={row.original.target}
id={`${row.original.id}-target`}
/>
@@ -244,7 +244,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
Limit
</Label>
<Input
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
defaultValue={row.original.limit}
id={`${row.original.id}-limit`}
/>
@@ -292,7 +292,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="data-[state=open]:bg-muted text-muted-foreground flex size-8"
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
size="icon"
>
<IconDotsVertical />
@@ -425,7 +425,7 @@ export function DataTable({
<SelectItem value="focus-documents">Focus Documents</SelectItem>
</SelectContent>
</Select>
<TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
<TabsList className="hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:px-1 @4xl/main:flex">
<TabsTrigger value="outline">Outline</TabsTrigger>
<TabsTrigger value="past-performance">
Past Performance <Badge variant="secondary">3</Badge>
@@ -488,7 +488,7 @@ export function DataTable({
id={sortableId}
>
<Table>
<TableHeader className="bg-muted sticky top-0 z-10">
<TableHeader className="sticky top-0 z-10 bg-muted">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
@@ -531,7 +531,7 @@ export function DataTable({
</DndContext>
</div>
<div className="flex items-center justify-between px-4">
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
@@ -653,7 +653,7 @@ function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
return (
<Drawer direction={isMobile ? "bottom" : "right"}>
<DrawerTrigger asChild>
<Button variant="link" className="text-foreground w-fit px-0 text-left">
<Button variant="link" className="w-fit px-0 text-left text-foreground">
{item.header}
</Button>
</DrawerTrigger>

View File

@@ -52,7 +52,7 @@ export function NavDocuments({
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="data-[state=open]:bg-accent rounded-sm"
className="rounded-sm data-[state=open]:bg-accent"
>
<IconDots />
<span className="sr-only">More</span>

View File

@@ -55,7 +55,7 @@ export function NavUser({
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
@@ -76,7 +76,7 @@ export function NavUser({
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>

View File

@@ -4,7 +4,7 @@ import { Button } from "@/registry/new-york-v4/ui/button"
export function SiteHeader() {
return (
<header className="bg-background/90 sticky top-0 z-10 flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<header className="sticky top-0 z-10 flex h-(--header-height) shrink-0 items-center gap-2 border-b bg-background/90 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<h1 className="text-base font-medium">Documents</h1>
<div className="ml-auto flex items-center gap-2">

View File

@@ -65,12 +65,12 @@ export default function ExamplesLayout({
</PageActions>
</PageHeader>
<PageNav id="examples" className="hidden md:flex">
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
<ThemeSelector className="mr-4 hidden md:flex" />
</PageNav>
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
<div className="theme-container container flex flex-1 scroll-mt-20 flex-col">
<div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding md:flex-1 xl:rounded-xl">
<div className="container-wrapper flex flex-1 flex-col section-soft pb-6">
<div className="container flex flex-1 scroll-mt-20 flex-col theme-container">
<div className="flex flex-col overflow-hidden rounded-lg border bg-background bg-clip-padding has-[[data-slot=rtl-components]]:overflow-visible has-[[data-slot=rtl-components]]:border-0 has-[[data-slot=rtl-components]]:bg-transparent md:flex-1 xl:rounded-xl">
{children}
</div>
</div>

View File

@@ -76,7 +76,7 @@ export function CodeViewer() {
</pre>
</div>
<div>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
Your API Key can be found here. You should use environment
variables or a secret management tool to expose your key to your
applications.

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider"
import type { Slider as SliderPrimitive } from "radix-ui"
import {
HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider"
interface MaxLengthSelectorProps {
defaultValue: SliderProps["defaultValue"]
defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
}
export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) {
@@ -25,7 +27,7 @@ export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) {
<div className="grid gap-4">
<div className="flex items-center justify-between">
<Label htmlFor="maxlength">Maximum Length</Label>
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
{value}
</span>
</div>

View File

@@ -1,8 +1,8 @@
"use client"
import * as React from "react"
import { type PopoverProps } from "@radix-ui/react-popover"
import { Check, ChevronsUpDown } from "lucide-react"
import type { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { useMutationObserver } from "@/hooks/use-mutation-observer"
@@ -29,7 +29,8 @@ import {
import { type Model, type ModelType } from "../data/models"
interface ModelSelectorProps extends PopoverProps {
interface ModelSelectorProps
extends React.ComponentProps<typeof PopoverPrimitive.Root> {
types: readonly ModelType[]
models: Model[]
}
@@ -77,7 +78,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
>
<div className="grid gap-2">
<h4 className="leading-none font-medium">{peekedModel.name}</h4>
<div className="text-muted-foreground text-sm">
<div className="text-sm text-muted-foreground">
{peekedModel.description}
</div>
{peekedModel.strengths ? (
@@ -85,7 +86,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
<h5 className="text-sm leading-none font-medium">
Strengths
</h5>
<ul className="text-muted-foreground text-sm">
<ul className="text-sm text-muted-foreground">
{peekedModel.strengths}
</ul>
</div>

View File

@@ -1,7 +1,6 @@
"use client"
import * as React from "react"
import { Dialog } from "@radix-ui/react-dialog"
import { MoreHorizontal } from "lucide-react"
import { toast } from "sonner"
@@ -16,6 +15,7 @@ import {
} from "@/registry/new-york-v4/ui/alert-dialog"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
@@ -70,7 +70,7 @@ export function PresetActions() {
</DialogDescription>
</DialogHeader>
<div className="py-6">
<h4 className="text-muted-foreground text-sm">
<h4 className="text-sm text-muted-foreground">
Playground Warnings
</h4>
<div className="flex items-start justify-between gap-4 pt-3">
@@ -79,7 +79,7 @@ export function PresetActions() {
<span className="font-semibold">
Show a warning when content is flagged
</span>
<span className="text-muted-foreground text-sm">
<span className="text-sm text-muted-foreground">
A warning will be shown when sexual, hateful, violent or
self-harm content is detected.
</span>

View File

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

View File

@@ -18,7 +18,7 @@ export function PresetShare() {
<PopoverContent align="end" className="flex w-[520px] flex-col gap-4">
<div className="flex flex-col gap-1 text-center sm:text-left">
<h3 className="text-lg font-semibold">Share preset</h3>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
Anyone who has this link and an OpenAI account will be able to view
this.
</p>

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider"
import type { Slider as SliderPrimitive } from "radix-ui"
import {
HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider"
interface TemperatureSelectorProps {
defaultValue: SliderProps["defaultValue"]
defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
}
export function TemperatureSelector({
@@ -27,7 +29,7 @@ export function TemperatureSelector({
<div className="grid gap-4">
<div className="flex items-center justify-between">
<Label htmlFor="temperature">Temperature</Label>
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
{value}
</span>
</div>

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider"
import type { Slider as SliderPrimitive } from "radix-ui"
import {
HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider"
interface TopPSelectorProps {
defaultValue: SliderProps["defaultValue"]
defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
}
export function TopPSelector({ defaultValue }: TopPSelectorProps) {
@@ -25,7 +27,7 @@ export function TopPSelector({ defaultValue }: TopPSelectorProps) {
<div className="grid gap-4">
<div className="flex items-center justify-between">
<Label htmlFor="top-p">Top P</Label>
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
{value}
</span>
</div>

View File

@@ -278,7 +278,7 @@ export default function PlaygroundPage() {
placeholder="We're writing to [inset]. Congrats from OpenAI!"
className="h-full min-h-[300px] p-4 lg:min-h-[700px] xl:min-h-[700px]"
/>
<div className="bg-muted rounded-md border"></div>
<div className="rounded-md border bg-muted"></div>
</div>
<div className="flex items-center gap-2">
<Button>Submit</Button>
@@ -312,7 +312,7 @@ export default function PlaygroundPage() {
/>
</div>
</div>
<div className="bg-muted min-h-[400px] rounded-md border lg:min-h-[700px]" />
<div className="min-h-[400px] rounded-md border bg-muted lg:min-h-[700px]" />
</div>
<div className="flex items-center gap-2">
<Button>Submit</Button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
"use client"
import { DirectionProvider } from "@/examples/base/ui-rtl/direction"
import { FieldSeparator } from "@/examples/base/ui-rtl/field"
import {
LanguageProvider,
LanguageSelector,
useLanguageContext,
} from "@/components/language-selector"
import { AppearanceSettings } from "./appearance-settings"
import { ButtonGroupDemo } from "./button-group-demo"
import { ButtonGroupInputGroup } from "./button-group-input-group"
import { ButtonGroupNested } from "./button-group-nested"
import { ButtonGroupPopover } from "./button-group-popover"
import { EmptyAvatarGroup } from "./empty-avatar-group"
import { FieldCheckbox } from "./field-checkbox"
import { FieldDemo } from "./field-demo"
import { FieldHear } from "./field-hear"
import { FieldSlider } from "./field-slider"
import { InputGroupButtonExample } from "./input-group-button"
import { InputGroupDemo } from "./input-group-demo"
import { ItemDemo } from "./item-demo"
import { NotionPromptForm } from "./notion-prompt-form"
import { SpinnerBadge } from "./spinner-badge"
import { SpinnerEmpty } from "./spinner-empty"
function RtlComponentsContent() {
const context = useLanguageContext()
if (!context) {
return null
}
const { language } = context
return (
<div
className="relative grid gap-8 p-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8"
dir="rtl"
data-lang={language}
data-slot="rtl-components"
>
<LanguageSelector
value={language}
onValueChange={context.setLanguage}
className="absolute -top-12 right-52 hidden h-8! data-[size=sm]:rounded-lg lg:flex"
languages={["ar", "he"]}
/>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<FieldDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<EmptyAvatarGroup />
<SpinnerBadge />
<ButtonGroupInputGroup />
<FieldSlider />
<InputGroupDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<InputGroupButtonExample />
<ItemDemo />
<FieldSeparator className="my-4">
{language === "he" ? "הגדרות מראה" : "إعدادات المظهر"}
</FieldSeparator>
<AppearanceSettings />
</div>
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">
<NotionPromptForm />
<ButtonGroupDemo />
<FieldCheckbox />
<div className="flex justify-between gap-4">
<ButtonGroupNested />
<ButtonGroupPopover />
</div>
<FieldHear />
<SpinnerEmpty />
</div>
</div>
)
}
export function RtlComponents() {
return (
<LanguageProvider defaultLanguage="ar">
<DirectionProvider direction="rtl">
<RtlComponentsContent />
</DirectionProvider>
</LanguageProvider>
)
}

View File

@@ -0,0 +1,97 @@
"use client"
import * as React from "react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/examples/base/ui-rtl/input-group"
import { Label } from "@/examples/base/ui-rtl/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui-rtl/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
inputLabel: "السعر",
info: "معلومات",
priceInfo: "أدخل السعر بالريال السعودي.",
priceDescription: "سيتم تحويل السعر تلقائياً.",
favorite: "مفضل",
currency: "ر.س",
},
he: {
dir: "rtl" as const,
inputLabel: "מחיר",
info: "מידע",
priceInfo: "הזן את המחיר בשקלים.",
priceDescription: "המחיר יומר אוטומטית.",
favorite: "מועדף",
currency: "₪",
},
}
export function InputGroupButtonExample() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [isFavorite, setIsFavorite] = React.useState(false)
return (
<div dir={t.dir} className="grid w-full max-w-sm gap-6">
<Label htmlFor="input-secure-rtl" className="sr-only">
{t.inputLabel}
</Label>
<InputGroup className="[--radius:9999px]">
<InputGroupInput id="input-secure-rtl" className="pr-0.5!" />
<InputGroupAddon>
<Popover>
<PopoverTrigger
render={
<InputGroupButton
variant="secondary"
size="icon-xs"
aria-label={t.info}
/>
}
>
<IconInfoCircle />
</PopoverTrigger>
<PopoverContent
align="end"
alignOffset={10}
className="flex flex-col gap-1 rounded-xl text-sm"
data-lang={lang}
dir={t.dir}
>
<p className="font-medium">{t.priceInfo}</p>
<p>{t.priceDescription}</p>
</PopoverContent>
</Popover>
</InputGroupAddon>
<InputGroupAddon className="text-muted-foreground">
{t.currency}
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton
onClick={() => setIsFavorite(!isFavorite)}
size="icon-xs"
aria-label={t.favorite}
>
<IconStar
data-favorite={isFavorite}
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
/>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -0,0 +1,140 @@
"use client"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/examples/base/ui-rtl/input-group"
import { Separator } from "@/examples/base/ui-rtl/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import {
IconCheck,
IconChevronDown,
IconInfoCircle,
IconPlus,
} from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
search: "بحث...",
results: "12 نتيجة",
example: "example.com",
tooltipContent: "هذا محتوى في تلميح.",
askSearchChat: "اسأل، ابحث أو تحدث...",
add: "إضافة",
auto: "تلقائي",
agent: "وكيل",
manual: "يدوي",
used: "52% مستخدم",
send: "إرسال",
},
he: {
dir: "rtl" as const,
search: "חיפוש...",
results: "12 תוצאות",
example: "example.com",
tooltipContent: "זה תוכן בטולטיפ.",
askSearchChat: "שאל, חפש או שוחח...",
add: "הוסף",
auto: "אוטומטי",
agent: "סוכן",
manual: "ידני",
used: "52% בשימוש",
send: "שלח",
},
}
export function InputGroupDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder={t.search} />
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupAddon align="inline-end">{t.results}</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder={t.example} />
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton
className="rounded-full"
size="icon-xs"
aria-label={t.add}
/>
}
>
<IconInfoCircle />
</TooltipTrigger>
<TooltipContent>{t.tooltipContent}</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder={t.askSearchChat} />
<InputGroupAddon align="block-end">
<InputGroupButton
variant="outline"
className="rounded-full"
size="icon-xs"
aria-label={t.add}
>
<IconPlus />
</InputGroupButton>
<DropdownMenu>
<DropdownMenuTrigger render={<InputGroupButton variant="ghost" />}>
<IconChevronDown />
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem>{t.auto}</DropdownMenuItem>
<DropdownMenuItem>{t.agent}</DropdownMenuItem>
<DropdownMenuItem>{t.manual}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText className="ms-auto">{t.used}</InputGroupText>
<Separator orientation="vertical" className="h-4!" />
<InputGroupButton
variant="default"
className="rounded-full"
size="icon-xs"
>
<ArrowUpIcon />
<span className="sr-only">{t.send}</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="shadcn" />
<InputGroupAddon align="inline-end">
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
<IconCheck className="size-3 text-white" />
</div>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -0,0 +1,64 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/examples/base/ui-rtl/item"
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
twoFactor: "المصادقة الثنائية",
twoFactorDescription: "التحقق عبر البريد الإلكتروني أو رقم الهاتف.",
enable: "تفعيل",
verified: "تم التحقق من ملفك الشخصي.",
},
he: {
dir: "rtl" as const,
twoFactor: "אימות דו-שלבי",
twoFactorDescription: "אמת באמצעות אימייל או מספר טלפון.",
enable: "הפעל",
verified: "הפרופיל שלך אומת.",
},
}
export function ItemDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="flex w-full max-w-md flex-col gap-6">
<Item variant="outline">
<ItemContent>
<ItemTitle>{t.twoFactor}</ItemTitle>
<ItemDescription className="text-pretty xl:hidden 2xl:block">
{t.twoFactorDescription}
</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm">{t.enable}</Button>
</ItemActions>
</Item>
<Item variant="outline" size="sm">
<ItemMedia>
<BadgeCheckIcon className="size-5" />
</ItemMedia>
<ItemContent>
<ItemTitle>{t.verified}</ItemTitle>
</ItemContent>
<ItemActions>
<ChevronRightIcon className="size-4 rtl:rotate-180" />
</ItemActions>
</Item>
</div>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,7 +79,7 @@ export const columns: ColumnDef<Task>[] = [
return (
<div className="flex w-[100px] items-center gap-2">
{status.icon && (
<status.icon className="text-muted-foreground size-4" />
<status.icon className="size-4 text-muted-foreground" />
)}
<span>{status.label}</span>
</div>
@@ -106,7 +106,7 @@ export const columns: ColumnDef<Task>[] = [
return (
<div className="flex items-center gap-2">
{priority.icon && (
<priority.icon className="text-muted-foreground size-4" />
<priority.icon className="size-4 text-muted-foreground" />
)}
<span>{priority.label}</span>
</div>

View File

@@ -33,7 +33,7 @@ export function DataTableColumnHeader<TData, TValue>({
<Button
variant="ghost"
size="sm"
className="data-[state=open]:bg-accent -ml-3 h-8"
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (

View File

@@ -107,18 +107,18 @@ export function DataTableFacetedFilter<TData, TValue>({
className={cn(
"flex size-4 items-center justify-center rounded-[4px] border",
isSelected
? "bg-primary border-primary text-primary-foreground"
? "border-primary bg-primary text-primary-foreground"
: "border-input [&_svg]:invisible"
)}
>
<Check className="text-primary-foreground size-3.5" />
<Check className="size-3.5 text-primary-foreground" />
</div>
{option.icon && (
<option.icon className="text-muted-foreground size-4" />
<option.icon className="size-4 text-muted-foreground" />
)}
<span>{option.label}</span>
{facets?.get(option.value) && (
<span className="text-muted-foreground ml-auto flex size-4 items-center justify-center font-mono text-xs">
<span className="ml-auto flex size-4 items-center justify-center font-mono text-xs text-muted-foreground">
{facets.get(option.value)}
</span>
)}

View File

@@ -24,7 +24,7 @@ export function DataTablePagination<TData>({
}: DataTablePaginationProps<TData>) {
return (
<div className="flex items-center justify-between px-2">
<div className="text-muted-foreground flex-1 text-sm">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>

View File

@@ -36,7 +36,7 @@ export function DataTableRowActions<TData>({
<Button
variant="ghost"
size="icon"
className="data-[state=open]:bg-muted size-8"
className="size-8 data-[state=open]:bg-muted"
>
<MoreHorizontal />
<span className="sr-only">Open menu</span>

View File

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

View File

@@ -30,7 +30,7 @@ export function UserNav() {
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm leading-none font-medium">shadcn</p>
<p className="text-muted-foreground text-xs leading-none">
<p className="text-xs leading-none text-muted-foreground">
m@example.com
</p>
</div>

View File

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

View File

@@ -12,8 +12,8 @@ export default function ThemesPage() {
<ThemeCustomizer />
</div>
</div>
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
<div className="theme-container container flex flex-1 flex-col">
<div className="container-wrapper flex flex-1 flex-col section-soft pb-6">
<div className="container flex flex-1 flex-col theme-container">
<CardsDemo />
</div>
</div>

View File

@@ -26,23 +26,23 @@ export function MenuAccentPicker({
)
return (
<div className="group/picker relative">
<div className="group/picker relative pr-3 md:pr-0">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Menu Accent</div>
<div className="text-foreground text-sm font-medium">
<div className="text-xs text-muted-foreground">Menu Accent</div>
<div className="text-sm font-medium text-foreground">
{currentAccent?.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">
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
className="text-foreground"
className="size-4 text-foreground"
>
<path
d="M19 12.1294L12.9388 18.207C11.1557 19.9949 10.2641 20.8889 9.16993 20.9877C8.98904 21.0041 8.80705 21.0041 8.62616 20.9877C7.53195 20.8889 6.64039 19.9949 4.85726 18.207L2.83687 16.1811C1.72104 15.0622 1.72104 13.2482 2.83687 12.1294M19 12.1294L10.9184 4.02587M19 12.1294H2.83687M10.9184 4.02587L2.83687 12.1294M10.9184 4.02587L8.89805 2"
@@ -51,7 +51,7 @@ export function MenuAccentPicker({
strokeLinecap="round"
strokeLinejoin="round"
data-accent={currentAccent?.value}
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
className="fill-muted-foreground/30 data-[accent=bold]:fill-foreground"
></path>
<path
d="M22 20C22 21.1046 21.1046 22 20 22C18.8954 22 18 21.1046 18 20C18 18.8954 20 17 20 17C20 17 22 18.8954 22 20Z"
@@ -60,7 +60,7 @@ export function MenuAccentPicker({
strokeLinecap="round"
strokeLinejoin="round"
data-accent={currentAccent?.value}
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
className="fill-muted-foreground/30 data-[accent=bold]:fill-foreground"
></path>
</svg>
</div>
@@ -78,7 +78,16 @@ export function MenuAccentPicker({
>
<PickerGroup>
{MENU_ACCENTS.map((accent) => (
<PickerRadioItem key={accent.value} value={accent.value}>
<PickerRadioItem
key={accent.value}
value={accent.value}
closeOnClick={isMobile}
disabled={
accent.value === "bold" &&
(params.menuColor === "default-translucent" ||
params.menuColor === "inverted-translucent")
}
>
{accent.label}
</PickerRadioItem>
))}
@@ -88,7 +97,7 @@ export function MenuAccentPicker({
</Picker>
<LockButton
param="menuAccent"
className="absolute top-1/2 right-10 -translate-y-1/2"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)

View File

@@ -0,0 +1,88 @@
"use client"
import Script from "next/script"
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/examples/base/ui/command"
import { type RegistryItem } from "shadcn/schema"
import { useActionMenu } from "@/app/(create)/hooks/use-action-menu"
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
export function ActionMenu({
itemsByBase,
}: {
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
}) {
const {
activeRegistryName,
getCommandValue,
groups,
handleSelect,
open,
setOpen,
} = useActionMenu(itemsByBase)
return (
<CommandDialog open={open} onOpenChange={setOpen} className="animate-none!">
<Command loop>
<CommandInput placeholder="Search" />
<CommandList>
<CommandEmpty>No items found.</CommandEmpty>
<CommandGroup>
{groups.map((group) =>
group.items.map((item) => (
<CommandItem
key={item.id}
value={getCommandValue(item)}
data-checked={activeRegistryName === item.registryName}
className="px-2"
onSelect={() => {
handleSelect(item.registryName)
}}
>
{item.label}
</CommandItem>
))
)}
</CommandGroup>
</CommandList>
</Command>
</CommandDialog>
)
}
export function ActionMenuScript() {
return (
<Script
id="design-system-listener"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
(function() {
// Forward Cmd/Ctrl + K (and P) to parent
document.addEventListener('keydown', function(e) {
if ((e.key === 'k' || e.key === 'p') && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: '${CMD_K_FORWARD_TYPE}',
key: e.key
}, '*');
}
}
});
})();
`,
}}
/>
)
}

View File

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

View File

@@ -40,49 +40,47 @@ export function BasePicker({
)
return (
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Component Library</div>
<div className="text-foreground text-sm font-medium">
{currentBase?.title}
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Base</div>
<div className="text-sm font-medium text-foreground">
{currentBase?.title}
</div>
</div>
</div>
{currentBase?.meta?.logo && (
<div
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,
}}
/>
)}
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentBase?.name}
onValueChange={handleValueChange}
{currentBase?.meta?.logo && (
<div
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 text-foreground select-none md:right-2.5 *:[svg]:size-4 *:[svg]:text-foreground!"
dangerouslySetInnerHTML={{
__html: currentBase.meta.logo,
}}
/>
)}
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerGroup>
{BASES.map((base) => (
<PickerRadioItem key={base.name} value={base.name}>
{base.meta?.logo && (
<div
className="text-foreground *:[svg]:text-foreground! size-4 shrink-0 [&_svg]:size-4"
dangerouslySetInnerHTML={{
__html: base.meta.logo,
}}
/>
)}
{base.title}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<PickerRadioGroup
value={currentBase?.name}
onValueChange={handleValueChange}
>
<PickerGroup>
{BASES.map((base) => (
<PickerRadioItem
key={base.name}
value={base.name}
closeOnClick={isMobile}
>
{base.title}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
</div>
)
}

View File

@@ -0,0 +1,136 @@
"use client"
import * as React from "react"
import { useMounted } from "@/hooks/use-mounted"
import {
BASE_COLORS,
getThemesForBaseColor,
type ChartColorName,
} from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function ChartColorPicker({
isMobile,
anchorRef,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const mounted = useMounted()
const [params, setParams] = useDesignSystemSearchParams()
const availableChartColors = React.useMemo(
() => getThemesForBaseColor(params.baseColor),
[params.baseColor]
)
const currentChartColor = React.useMemo(
() =>
availableChartColors.find((theme) => theme.name === params.chartColor),
[availableChartColors, params.chartColor]
)
const currentChartColorIsBaseColor = React.useMemo(
() => BASE_COLORS.find((baseColor) => baseColor.name === params.chartColor),
[params.chartColor]
)
React.useEffect(() => {
if (!currentChartColor && availableChartColors.length > 0) {
setParams({ chartColor: availableChartColors[0].name })
}
}, [currentChartColor, availableChartColors, setParams])
return (
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Chart Color</div>
<div className="text-sm font-medium text-foreground">
{currentChartColor?.title}
</div>
</div>
{mounted && (
<div
style={
{
"--color":
currentChartColor?.cssVars?.dark?.[
currentChartColorIsBaseColor
? "muted-foreground"
: "primary"
],
} as React.CSSProperties
}
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none md:right-2.5"
/>
)}
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-92"
>
<PickerRadioGroup
value={currentChartColor?.name}
onValueChange={(value) => {
setParams({ chartColor: value as ChartColorName })
}}
>
<PickerGroup>
{availableChartColors
.filter((theme) =>
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
)
.map((theme) => (
<PickerRadioItem
key={theme.name}
value={theme.name}
closeOnClick={isMobile}
>
{theme.title}
</PickerRadioItem>
))}
</PickerGroup>
<PickerSeparator />
<PickerGroup>
{availableChartColors
.filter(
(theme) =>
!BASE_COLORS.find(
(baseColor) => baseColor.name === theme.name
)
)
.map((theme) => (
<PickerRadioItem
key={theme.name}
value={theme.name}
closeOnClick={isMobile}
>
{theme.title}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton
param="chartColor"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -0,0 +1,43 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui/button"
import { cn } from "@/lib/utils"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
const presetCode = usePresetCode()
const [hasCopied, setHasCopied] = React.useState(false)
React.useEffect(() => {
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
const handleCopy = React.useCallback(() => {
copyToClipboardWithMeta(`--preset ${presetCode}`, {
name: "copy_preset_command",
properties: {
preset: presetCode,
},
})
setHasCopied(true)
}, [presetCode])
return (
<Button
variant="outline"
onClick={handleCopy}
className={cn(
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
className
)}
>
<span>{hasCopied ? "Copied" : `--preset ${presetCode}`}</span>
</Button>
)
}

View File

@@ -1,29 +1,41 @@
"use client"
import * as React from "react"
import { Settings05Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/examples/base/ui/card"
import { FieldGroup, FieldSeparator } from "@/examples/base/ui/field"
import { type RegistryItem } from "shadcn/schema"
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 { getThemesForBaseColor, STYLES } from "@/registry/config"
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
import { ActionMenu } from "@/app/(create)/components/action-menu"
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
import { BasePicker } from "@/app/(create)/components/base-picker"
import { ChartColorPicker } from "@/app/(create)/components/chart-color-picker"
import { CopyPreset } from "@/app/(create)/components/copy-preset"
import { FontPicker } from "@/app/(create)/components/font-picker"
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
import { MainMenu } from "@/app/(create)/components/main-menu"
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 { ResetDialog } 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 { V0Button } from "@/app/(create)/components/v0-button"
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function Customizer() {
export function Customizer({
itemsByBase,
}: {
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
}) {
const [params] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const anchorRef = React.useRef<HTMLDivElement | null>(null)
@@ -34,55 +46,59 @@ export function Customizer() {
)
return (
<div
className="no-scrollbar -mx-2.5 flex flex-col overflow-y-auto p-1 md:mx-0 md:h-[calc(100svh-var(--header-height)-2rem)] md:w-48 md:gap-0 md:py-0"
<Card
className="dark top-24 right-12 isolate z-10 max-h-full min-h-0 w-full self-start rounded-2xl bg-card/90 shadow-xl backdrop-blur-xl md:w-(--customizer-width)"
ref={anchorRef}
size="sm"
>
<div className="hidden items-center gap-2 px-[calc(--spacing(2.5))] pb-1 md:flex md:flex-col md:items-start">
<HugeiconsIcon
icon={Settings05Icon}
className="size-4"
strokeWidth={2}
/>
<div className="relative flex flex-col gap-1 rounded-lg text-[13px]/snug">
<div className="flex items-center gap-1 font-medium text-balance">
Build your own shadcn/ui
</div>
<div className="hidden md:flex">
When you&apos;re done, click Create Project to start a new project.
</div>
</div>
</div>
<div className="no-scrollbar h-14 overflow-x-auto overflow-y-hidden p-px md:h-full md:overflow-x-hidden md:overflow-y-auto">
<FieldGroup className="flex h-full flex-1 flex-row gap-2 md:flex-col md:gap-0">
<PresetPicker
presets={PRESETS}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<BasePicker isMobile={isMobile} anchorRef={anchorRef} />
<CardHeader className="hidden items-center justify-between gap-2 border-b group-data-reversed/layout:flex-row-reverse md:flex">
<MainMenu />
</CardHeader>
<CardContent className="no-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden md:overflow-y-auto">
<FieldGroup className="flex-row gap-2.5 py-px **:data-[slot=field-separator]:-mx-4 **:data-[slot=field-separator]:w-auto md:flex-col md:gap-3.25">
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
<StylePicker
styles={STYLES}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<FieldSeparator className="hidden md:block" />
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<ThemePicker
themes={availableThemes}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<ChartColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<FieldSeparator className="hidden md:block" />
<FontPicker
label="Heading"
param="fontHeading"
fonts={FONT_HEADING_OPTIONS}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<FontPicker
label="Font"
param="font"
fonts={FONTS}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<FieldSeparator className="hidden md:block" />
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
<FieldSeparator className="hidden md:block" />
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
<div className="mt-auto hidden w-full flex-col items-center gap-0 md:flex">
<RandomButton />
<ResetButton />
</div>
</FieldGroup>
</div>
</div>
</CardContent>
<CardFooter className="flex min-w-0 gap-2 md:flex-col md:**:[button,a]:w-full">
<CopyPreset className="flex-1 md:flex-none" />
<RandomButton className="flex-1 md:flex-none" />
<ActionMenu itemsByBase={itemsByBase} />
<ResetDialog />
</CardFooter>
</Card>
)
}

View File

@@ -9,22 +9,131 @@ import {
} from "@/registry/config"
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
import { FONTS } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
type RegistryThemeCssVars = NonNullable<
ReturnType<typeof buildRegistryTheme>["cssVars"]
>
function removeManagedBodyClasses(body: Element) {
for (const className of Array.from(body.classList)) {
if (
MANAGED_BODY_CLASS_PREFIXES.some((prefix) => className.startsWith(prefix))
) {
body.classList.remove(className)
}
}
}
function buildCssRule(selector: string, cssVars?: Record<string, string>) {
const declarations = Object.entries(cssVars ?? {})
.filter(([, value]) => Boolean(value))
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
if (!declarations) {
return `${selector} {}\n`
}
return `${selector} {\n${declarations}\n}\n`
}
function buildThemeCssText(cssVars: RegistryThemeCssVars) {
return [
buildCssRule(":root", {
...(cssVars.theme ?? {}),
...(cssVars.light ?? {}),
}),
buildCssRule(".dark", cssVars.dark),
].join("\n")
}
export function DesignSystemProvider({
children,
}: {
children: React.ReactNode
}) {
const [
{ style, theme, font, baseColor, menuAccent, menuColor, radius },
setSearchParams,
] = useDesignSystemSearchParams({
const [searchParams, 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)
const {
style,
theme,
font,
fontHeading,
baseColor,
chartColor,
menuAccent,
menuColor,
radius,
} = searchParams
const effectiveRadius = style === "lyra" ? "none" : radius
const selectedFont = React.useMemo(
() => FONTS.find((fontOption) => fontOption.value === font),
[font]
)
const selectedHeadingFont = React.useMemo(() => {
if (fontHeading === "inherit" || fontHeading === font) {
return selectedFont
}
return FONTS.find((fontOption) => fontOption.value === fontHeading)
}, [font, fontHeading, selectedFont])
const initialFontSansRef = React.useRef<string | null>(null)
const initialFontHeadingRef = React.useRef<string | null>(null)
React.useEffect(() => {
initialFontSansRef.current =
document.documentElement.style.getPropertyValue("--font-sans")
initialFontHeadingRef.current =
document.documentElement.style.getPropertyValue("--font-heading")
return () => {
removeManagedBodyClasses(document.body)
document.getElementById(THEME_STYLE_ELEMENT_ID)?.remove()
if (initialFontSansRef.current) {
document.documentElement.style.setProperty(
"--font-sans",
initialFontSansRef.current
)
} else {
document.documentElement.style.removeProperty("--font-sans")
}
if (initialFontHeadingRef.current) {
document.documentElement.style.setProperty(
"--font-heading",
initialFontHeadingRef.current
)
} else {
document.documentElement.style.removeProperty("--font-heading")
}
}
}, [])
const handleDesignSystemMessage = React.useCallback(
(nextParams: DesignSystemSearchParams) => {
setSearchParams(nextParams)
},
[setSearchParams]
)
useIframeMessageListener("design-system-params", handleDesignSystemMessage)
React.useEffect(() => {
if (style === "lyra" && radius !== "none") {
setSearchParams({ radius: "none" })
}
}, [style, radius, setSearchParams])
// Use useLayoutEffect for synchronous style updates to prevent flash.
React.useLayoutEffect(() => {
@@ -34,34 +143,40 @@ export function DesignSystemProvider({
const body = document.body
// Update style class in place (remove old, add new).
body.classList.forEach((className) => {
if (className.startsWith("style-")) {
body.classList.remove(className)
}
})
body.classList.add(`style-${style}`)
// Update base color class in place.
body.classList.forEach((className) => {
if (className.startsWith("base-color-")) {
body.classList.remove(className)
}
})
body.classList.add(`base-color-${baseColor}`)
// Iterate over a snapshot so removals do not affect traversal.
removeManagedBodyClasses(body)
body.classList.add(`style-${style}`, `base-color-${baseColor}`)
// Update font.
const selectedFont = FONTS.find((f) => f.value === font)
// Always set --font-sans for the preview so the selected font is visible.
// The font type (sans/serif/mono) is metadata for the CLI updater.
if (selectedFont) {
const fontFamily = selectedFont.font.style.fontFamily
document.documentElement.style.setProperty("--font-sans", fontFamily)
document.documentElement.style.setProperty(
"--font-sans",
selectedFont.font.style.fontFamily
)
}
if (selectedHeadingFont) {
document.documentElement.style.setProperty(
"--font-heading",
selectedHeadingFont.font.style.fontFamily
)
}
setIsReady(true)
}, [style, theme, font, baseColor])
}, [
style,
theme,
font,
fontHeading,
baseColor,
selectedFont,
selectedHeadingFont,
])
const registryTheme = React.useMemo(() => {
if (!baseColor || !theme || !menuAccent || !radius) {
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
return null
}
@@ -69,12 +184,13 @@ export function DesignSystemProvider({
...DEFAULT_CONFIG,
baseColor,
theme,
chartColor,
menuAccent,
radius,
radius: effectiveRadius,
}
return buildRegistryTheme(config)
}, [baseColor, theme, menuAccent, radius])
}, [baseColor, theme, chartColor, menuAccent, effectiveRadius])
// Use useLayoutEffect for synchronous CSS var updates.
React.useLayoutEffect(() => {
@@ -82,69 +198,83 @@ export function DesignSystemProvider({
return
}
const styleId = "design-system-theme-vars"
let styleElement = document.getElementById(
styleId
THEME_STYLE_ELEMENT_ID
) as HTMLStyleElement | null
if (!styleElement) {
styleElement = document.createElement("style")
styleElement.id = styleId
styleElement.id = THEME_STYLE_ELEMENT_ID
document.head.appendChild(styleElement)
}
const {
light: lightVars,
dark: darkVars,
theme: themeVars,
} = registryTheme.cssVars
let cssText = ":root {\n"
// Add theme vars (shared across light/dark).
if (themeVars) {
Object.entries(themeVars).forEach(([key, value]) => {
if (value) {
cssText += ` --${key}: ${value};\n`
}
})
}
// Add light mode vars.
if (lightVars) {
Object.entries(lightVars).forEach(([key, value]) => {
if (value) {
cssText += ` --${key}: ${value};\n`
}
})
}
cssText += "}\n\n"
cssText += ".dark {\n"
if (darkVars) {
Object.entries(darkVars).forEach(([key, value]) => {
if (value) {
cssText += ` --${key}: ${value};\n`
}
})
}
cssText += "}\n"
styleElement.textContent = cssText
styleElement.textContent = buildThemeCssText(registryTheme.cssVars)
}, [registryTheme])
// Handle menu color inversion by adding/removing dark class to elements with cn-menu-target.
React.useEffect(() => {
// useLayoutEffect to apply classes synchronously before paint, avoiding flash.
React.useLayoutEffect(() => {
if (!menuColor) {
return
}
const isInvertedMenu =
menuColor === "inverted" || menuColor === "inverted-translucent"
const isTranslucentMenu =
menuColor === "default-translucent" ||
menuColor === "inverted-translucent"
let frameId = 0
const updateMenuElements = () => {
const menuElements = document.querySelectorAll(".cn-menu-target")
menuElements.forEach((element) => {
if (menuColor === "inverted") {
element.classList.add("dark")
} else {
element.classList.remove("dark")
const allElements = document.querySelectorAll<HTMLElement>(
".cn-menu-target, [data-menu-translucent]"
)
if (allElements.length === 0) {
return
}
// Disable transitions while toggling classes.
allElements.forEach((element) => {
element.style.transition = "none"
})
allElements.forEach((element) => {
if (element.classList.contains("cn-menu-target")) {
if (isInvertedMenu) {
element.classList.add("dark")
} else {
element.classList.remove("dark")
}
}
// When translucent is enabled, move from data-attr to class so styles apply.
// When disabled, move back to a data-attr so the element stays queryable
// for future toggles without losing its identity as a menu element.
if (isTranslucentMenu) {
element.classList.add("cn-menu-translucent")
element.removeAttribute("data-menu-translucent")
} else if (element.classList.contains("cn-menu-translucent")) {
element.classList.remove("cn-menu-translucent")
element.setAttribute("data-menu-translucent", "")
}
})
// Force a reflow, then re-enable transitions.
void document.body.offsetHeight
allElements.forEach((element) => {
element.style.transition = ""
})
}
const scheduleMenuUpdate = () => {
if (frameId) {
return
}
frameId = window.requestAnimationFrame(() => {
frameId = 0
updateMenuElements()
})
}
@@ -153,7 +283,7 @@ export function DesignSystemProvider({
// Watch for new menu elements being added to the DOM.
const observer = new MutationObserver(() => {
updateMenuElements()
scheduleMenuUpdate()
})
observer.observe(document.body, {
@@ -163,6 +293,9 @@ export function DesignSystemProvider({
return () => {
observer.disconnect()
if (frameId) {
window.cancelAnimationFrame(frameId)
}
}
}, [menuColor])

View File

@@ -2,55 +2,112 @@
import * as React from "react"
import {
Item,
ItemContent,
ItemDescription,
ItemTitle,
} from "@/registry/bases/radix/ui/item"
import { type FontValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
PickerContent,
PickerGroup,
PickerLabel,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { type Font } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { FONTS } from "@/app/(create)/lib/fonts"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
type FontPickerOption = {
name: string
value: string
type: string
font: {
style: {
fontFamily: string
}
} | null
}
export function FontPicker({
label,
param,
fonts,
isMobile,
anchorRef,
}: {
fonts: readonly Font[]
label: string
param: "font" | "fontHeading"
fonts: readonly FontPickerOption[]
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const currentValue = param === "font" ? params.font : params.fontHeading
const handleFontChange = React.useCallback(
(value: string) => {
setParams({
[param]: value,
} as Partial<DesignSystemSearchParams>)
},
[param, setParams]
)
const currentFont = React.useMemo(
() => fonts.find((font) => font.value === params.font),
[fonts, params.font]
() => fonts.find((font) => font.value === currentValue),
[fonts, currentValue]
)
const currentBodyFont = React.useMemo(
() => FONTS.find((font) => font.value === params.font),
[params.font]
)
const inheritsBodyFont = param === "fontHeading" && currentValue === "inherit"
const displayFontName = inheritsBodyFont
? currentBodyFont?.name
: currentFont?.name
const inheritFontLabel = currentBodyFont ? currentBodyFont.name : "Body font"
const groupedFonts = React.useMemo(() => {
const pickerFonts =
param === "fontHeading"
? fonts.filter((font) => font.value !== "inherit")
: fonts
const groups = new Map<string, FontPickerOption[]>()
for (const font of pickerFonts) {
const existing = groups.get(font.type)
if (existing) {
existing.push(font)
continue
}
groups.set(font.type, [font])
}
return Array.from(groups.entries()).map(([type, items]) => ({
type,
label: `${type.charAt(0).toUpperCase()}${type.slice(1)}`,
items,
}))
}, [fonts, param])
return (
<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>
<div className="text-foreground text-sm font-medium">
{currentFont?.name}
<div className="text-xs text-muted-foreground">{label}</div>
<div className="text-sm font-medium text-foreground">
{displayFontName}
</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"
style={{ fontFamily: currentFont?.font.style.fontFamily }}
className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5"
style={{
fontFamily:
currentFont?.font?.style.fontFamily ??
currentBodyFont?.font.style.fontFamily,
}}
>
Aa
</div>
@@ -59,44 +116,42 @@ export function FontPicker({
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-80 md:w-72"
className="max-h-96"
>
<PickerRadioGroup
value={currentFont?.value}
onValueChange={(value) => {
setParams({ font: value as FontValue })
}}
value={currentValue}
onValueChange={handleFontChange}
>
<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>
{param === "fontHeading" ? (
<>
<PickerGroup>
<PickerRadioItem value="inherit" closeOnClick={isMobile}>
{inheritFontLabel}
</PickerRadioItem>
{index < fonts.length - 1 && (
<PickerSeparator className="opacity-50" />
)}
</React.Fragment>
))}
</PickerGroup>
</PickerGroup>
<PickerSeparator />
</>
) : null}
{groupedFonts.map((group) => (
<PickerGroup key={group.type}>
<PickerLabel>{group.label}</PickerLabel>
{group.items.map((font) => (
<PickerRadioItem
key={font.value}
value={font.value}
closeOnClick={isMobile}
>
{font.name}
</PickerRadioItem>
))}
</PickerGroup>
))}
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton
param="font"
className="absolute top-1/2 right-10 -translate-y-1/2"
param={param}
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)

View File

@@ -0,0 +1,78 @@
"use client"
import Script from "next/script"
import { Button } from "@/examples/base/ui/button"
import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useHistory } from "@/app/(create)/hooks/use-history"
export const UNDO_FORWARD_TYPE = "undo-forward"
export const REDO_FORWARD_TYPE = "redo-forward"
export function HistoryButtons() {
const { canGoBack, canGoForward, goBack, goForward } = useHistory()
return (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
title="Undo"
disabled={!canGoBack}
onClick={goBack}
>
<HugeiconsIcon icon={Undo02Icon} />
<span className="sr-only">Undo</span>
</Button>
<Button
variant="ghost"
size="icon"
title="Redo"
disabled={!canGoForward}
onClick={goForward}
>
<HugeiconsIcon icon={Redo02Icon} />
<span className="sr-only">Redo</span>
</Button>
</div>
)
}
export function HistoryScript() {
return (
<Script
id="history-listener"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
(function() {
document.addEventListener('keydown', function(e) {
if (!e.metaKey && !e.ctrlKey) return;
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return;
}
var key = e.key.toLowerCase();
if ((key === 'z' && e.shiftKey) || (key === 'y' && e.ctrlKey)) {
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: '${REDO_FORWARD_TYPE}' }, '*');
}
} else if (key === 'z') {
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: '${UNDO_FORWARD_TYPE}' }, '*');
}
}
});
})();
`,
}}
/>
)
}

View File

@@ -1,14 +1,8 @@
"use client"
import * as React from "react"
import { lazy, memo, Suspense } from "react"
import { Item, ItemContent, ItemTitle } from "@/registry/bases/radix/ui/item"
import {
iconLibraries,
type IconLibrary,
type IconLibraryName,
} from "@/registry/config"
import { iconLibraries, type IconLibraryName } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -16,124 +10,10 @@ import {
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
const IconLucide = lazy(() =>
import("@/registry/icons/icon-lucide").then((mod) => ({
default: mod.IconLucide,
}))
)
const IconTabler = lazy(() =>
import("@/registry/icons/icon-tabler").then((mod) => ({
default: mod.IconTabler,
}))
)
const IconHugeicons = lazy(() =>
import("@/registry/icons/icon-hugeicons").then((mod) => ({
default: mod.IconHugeicons,
}))
)
const 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",
"CircleAlertIcon",
"TrashIcon",
"ShareIcon",
"ShoppingBagIcon",
"MoreHorizontalIcon",
"Loader2Icon",
"PlusIcon",
"MinusIcon",
"ArrowLeftIcon",
"ArrowRightIcon",
"CheckIcon",
"ChevronDownIcon",
"ChevronRightIcon",
],
tabler: [
"IconCopy",
"IconExclamationCircle",
"IconTrash",
"IconShare",
"IconShoppingBag",
"IconDots",
"IconLoader",
"IconPlus",
"IconMinus",
"IconArrowLeft",
"IconArrowRight",
"IconCheck",
"IconChevronDown",
"IconChevronRight",
],
hugeicons: [
"Copy01Icon",
"AlertCircleIcon",
"Delete02Icon",
"Share03Icon",
"ShoppingBag01Icon",
"MoreHorizontalCircle01Icon",
"Loading03Icon",
"PlusSignIcon",
"MinusSignIcon",
"ArrowLeft02Icon",
"ArrowRight02Icon",
"Tick02Icon",
"ArrowDown01Icon",
"ArrowRight01Icon",
],
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 = {
lucide: (
<svg
@@ -248,12 +128,12 @@ export function IconLibraryPicker({
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Icon Library</div>
<div className="text-foreground text-sm font-medium">
<div className="text-xs text-muted-foreground">Icon Library</div>
<div className="text-sm font-medium text-foreground">
{currentIconLibrary?.title}
</div>
</div>
<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">
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5 *:[svg]:text-foreground!">
{logos[currentIconLibrary?.name as keyof typeof logos]}
</div>
</PickerTrigger>
@@ -269,16 +149,14 @@ export function IconLibraryPicker({
}}
>
<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>
{Object.values(iconLibraries).map((iconLibrary) => (
<PickerRadioItem
key={iconLibrary.name}
value={iconLibrary.name}
closeOnClick={isMobile}
>
{iconLibrary.title}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
@@ -286,81 +164,8 @@ export function IconLibraryPicker({
</Picker>
<LockButton
param="iconLibrary"
className="absolute top-1/2 right-10 -translate-y-1/2"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}
function IconLibraryPickerItem({
iconLibrary,
value,
}: {
iconLibrary: IconLibrary
value: string
}) {
return (
<PickerRadioItem
value={value}
className="pr-2 *:data-[slot=dropdown-menu-radio-item-indicator]:hidden"
>
<Item size="xs">
<ItemContent className="gap-1">
<ItemTitle className="text-muted-foreground text-xs font-medium">
{iconLibrary.title}
</ItemTitle>
<IconLibraryPreview iconLibrary={iconLibrary.name} />
</ItemContent>
</Item>
</PickerRadioItem>
)
}
const IconLibraryPreview = memo(function IconLibraryPreview({
iconLibrary,
}: {
iconLibrary: IconLibraryName
}) {
const previewIcons = PREVIEW_ICONS[iconLibrary]
if (!previewIcons) {
return null
}
const IconRenderer =
iconLibrary === "lucide"
? IconLucide
: iconLibrary === "tabler"
? IconTabler
: iconLibrary === "hugeicons"
? IconHugeicons
: iconLibrary === "phosphor"
? IconPhosphor
: IconRemixicon
return (
<Suspense
fallback={
<div className="-mx-1 grid w-full grid-cols-7 gap-2">
{previewIcons.map((iconName) => (
<div
key={iconName}
className="bg-muted size-6 animate-pulse rounded"
/>
))}
</div>
}
>
<div className="-mx-1 grid w-full grid-cols-7 gap-2">
{previewIcons.map((iconName) => (
<div
key={iconName}
className="flex size-6 items-center justify-center *:[svg]:size-5"
>
<IconRenderer name={iconName} />
</div>
))}
</div>
</Suspense>
)
})

View File

@@ -36,6 +36,15 @@ const IconRemixicon = lazy(() =>
}))
)
// Preload all icon renderer modules so switching libraries is instant.
// These warm the browser module cache; React.lazy resolves immediately
// for modules that are already loaded.
void import("@/registry/icons/icon-lucide")
void import("@/registry/icons/icon-tabler")
void import("@/registry/icons/icon-hugeicons")
void import("@/registry/icons/icon-phosphor")
void import("@/registry/icons/icon-remixicon")
export function IconPlaceholder({
...props
}: {

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,83 @@
"use client"
import * as React from "react"
import { type Button } from "@/examples/base/ui/button"
import { Menu09Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import {
Picker,
PickerContent,
PickerGroup,
PickerItem,
PickerSeparator,
PickerShortcut,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useActionMenuTrigger } from "@/app/(create)/hooks/use-action-menu"
import { useHistory } from "@/app/(create)/hooks/use-history"
import { useRandom } from "@/app/(create)/hooks/use-random"
import { useReset } from "@/app/(create)/hooks/use-reset"
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
const [isMac, setIsMac] = React.useState(false)
const { canGoBack, canGoForward, goBack, goForward } = useHistory()
const { openActionMenu } = useActionMenuTrigger()
const { randomize } = useRandom()
const { toggleTheme } = useThemeToggle()
const { setShowResetDialog } = useReset()
React.useEffect(() => {
const platform = navigator.platform
const userAgent = navigator.userAgent
setIsMac(APPLE_PLATFORM_REGEX.test(platform || userAgent))
}, [])
return (
<React.Fragment>
<Picker>
<PickerTrigger
className={cn(
"flex items-center justify-between gap-2 rounded-lg px-1.75 ring-1 ring-foreground/10 focus-visible:ring-1",
className
)}
>
<span className="font-medium">Menu</span>
<HugeiconsIcon icon={Menu09Icon} strokeWidth={2} className="size-5" />
</PickerTrigger>
<PickerContent side="right" align="start" alignOffset={-8}>
<PickerGroup>
<PickerItem onClick={openActionMenu}>
Navigate...
<PickerShortcut>{isMac ? "⌘P" : "Ctrl+P"}</PickerShortcut>
</PickerItem>
<PickerItem onClick={randomize}>
Shuffle <PickerShortcut>R</PickerShortcut>
</PickerItem>
<PickerItem onClick={toggleTheme}>
Light/Dark <PickerShortcut>D</PickerShortcut>
</PickerItem>
</PickerGroup>
<PickerSeparator />
<PickerGroup>
<PickerItem onClick={goBack} disabled={!canGoBack}>
Undo <PickerShortcut>{isMac ? "⌘Z" : "Ctrl+Z"}</PickerShortcut>
</PickerItem>
<PickerItem onClick={goForward} disabled={!canGoForward}>
Redo{" "}
<PickerShortcut>{isMac ? "⇧⌘Z" : "Ctrl+Shift+Z"}</PickerShortcut>
</PickerItem>
<PickerSeparator />
<PickerItem onClick={() => setShowResetDialog(true)}>
Reset
</PickerItem>
</PickerGroup>
</PickerContent>
</Picker>
</React.Fragment>
)
}

View File

@@ -1,6 +1,8 @@
"use client"
import * as React from "react"
import { Menu02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useTheme } from "next-themes"
import { useMounted } from "@/hooks/use-mounted"
@@ -10,99 +12,37 @@ import {
Picker,
PickerContent,
PickerGroup,
PickerLabel,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import {
isTranslucentMenuColor,
useDesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
const MENU_OPTIONS = [
{
value: "default" as const,
label: "Default",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
role="img"
stroke="currentColor"
className="text-foreground"
>
<path
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M8.5 11.5L14.5001 11.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.5 15H13.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 8H15.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
{
value: "inverted" as const,
label: "Inverted",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
role="img"
className="fill-foreground text-foreground"
>
<path
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M8.5 11.5L14.5001 11.5"
stroke="var(--background)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.5 15H13.5"
stroke="var(--background)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 8H15.5"
stroke="var(--background)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
] as const
type ColorChoice = "default" | "inverted"
type SurfaceChoice = "solid" | "translucent"
function getMenuColorValue(
color: ColorChoice,
translucent: boolean
): MenuColorValue {
if (color === "default") {
return translucent ? "default-translucent" : "default"
}
return translucent ? "inverted-translucent" : "inverted"
}
const MENU_OPTIONS: { value: MenuColorValue; label: string }[] = [
{ value: "default", label: "Default / Solid" },
{ value: "default-translucent", label: "Default / Translucent" },
{ value: "inverted", label: "Inverted / Solid" },
{ value: "inverted-translucent", label: "Inverted / Translucent" },
]
export function MenuColorPicker({
isMobile,
@@ -111,25 +51,69 @@ export function MenuColorPicker({
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const { resolvedTheme } = useTheme()
const mounted = useMounted()
const [params, setParams] = useDesignSystemSearchParams()
const lastSolidMenuAccentRef = React.useRef(params.menuAccent)
const isDark = mounted && resolvedTheme === "dark"
const currentMenu = MENU_OPTIONS.find(
(menu) => menu.value === params.menuColor
)
const colorChoice: ColorChoice =
params.menuColor === "inverted" ||
params.menuColor === "inverted-translucent"
? "inverted"
: "default"
const surfaceChoice: SurfaceChoice =
params.menuColor === "default-translucent" ||
params.menuColor === "inverted-translucent"
? "translucent"
: "solid"
React.useEffect(() => {
if (surfaceChoice === "solid") {
lastSolidMenuAccentRef.current = params.menuAccent
}
}, [params.menuAccent, surfaceChoice])
const setColor = (color: ColorChoice) => {
const nextMenuColor = getMenuColorValue(
color,
surfaceChoice === "translucent"
)
setParams({
menuColor: nextMenuColor,
...(isTranslucentMenuColor(nextMenuColor) && { menuAccent: "subtle" }),
})
}
const setSurface = (choice: SurfaceChoice) => {
const isTranslucent = choice === "translucent"
const nextMenuColor = getMenuColorValue(colorChoice, isTranslucent)
setParams({
menuColor: nextMenuColor,
menuAccent: isTranslucent ? "subtle" : lastSolidMenuAccentRef.current,
})
}
return (
<div className="group/picker relative">
<Picker>
<PickerTrigger disabled={mounted && resolvedTheme === "dark"}>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Menu Color</div>
<div className="text-foreground text-sm font-medium">
<div className="text-xs text-muted-foreground">Menu</div>
<div className="line-clamp-1 text-sm font-medium text-foreground">
{currentMenu?.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">
{currentMenu?.icon}
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
<HugeiconsIcon
icon={Menu02Icon}
strokeWidth={2}
className="size-4"
/>
</div>
</PickerTrigger>
<PickerContent
@@ -137,26 +121,48 @@ export function MenuColorPicker({
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
value={currentMenu?.value}
onValueChange={(value) => {
setParams({ menuColor: value as MenuColorValue })
}}
>
<PickerGroup>
{MENU_OPTIONS.map((menu) => (
<PickerRadioItem key={menu.value} value={menu.value}>
{menu.icon}
{menu.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
<PickerGroup>
<PickerLabel>Color</PickerLabel>
<PickerRadioGroup
value={colorChoice}
onValueChange={(value) => {
setColor(value as ColorChoice)
}}
>
<PickerRadioItem value="default" closeOnClick={isMobile}>
Default
</PickerRadioItem>
<PickerRadioItem
value="inverted"
closeOnClick={isMobile}
disabled={isDark}
>
Inverted
</PickerRadioItem>
</PickerRadioGroup>
</PickerGroup>
<PickerSeparator />
<PickerGroup>
<PickerLabel>Appearance</PickerLabel>
<PickerRadioGroup
value={surfaceChoice}
onValueChange={(value) => {
setSurface(value as SurfaceChoice)
}}
>
<PickerRadioItem value="solid" closeOnClick={isMobile}>
Solid
</PickerRadioItem>
<PickerRadioItem value="translucent" closeOnClick={isMobile}>
Translucent
</PickerRadioItem>
</PickerRadioGroup>
</PickerGroup>
</PickerContent>
</Picker>
<LockButton
param="menuColor"
className="absolute top-1/2 right-10 -translate-y-1/2"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)

View File

@@ -0,0 +1,87 @@
"use client"
import * as React from "react"
import Script from "next/script"
import { Button } from "@/examples/base/ui/button"
import { cn } from "@/lib/utils"
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
export function ModeSwitcher({
variant = "ghost",
className,
}: {
variant?: React.ComponentProps<typeof Button>["variant"]
className?: React.ComponentProps<typeof Button>["className"]
}) {
const { toggleTheme } = useThemeToggle()
return (
<Button
variant={variant}
size="icon"
className={cn("group/toggle extend-touch-target", className)}
onClick={toggleTheme}
id="mode-switcher-button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4.5"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 3l0 18" />
<path d="M12 9l4.65 -4.65" />
<path d="M12 14.3l7.37 -7.37" />
<path d="M12 19.6l8.85 -8.85" />
</svg>
<span className="sr-only">Toggle theme</span>
</Button>
)
}
export function DarkModeScript() {
return (
<Script
id="dark-mode-listener"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
(function() {
// Forward D key
document.addEventListener('keydown', function(e) {
if ((e.key === 'd' || e.key === 'D') && !e.metaKey && !e.ctrlKey && !e.altKey) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return;
}
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: '${DARK_MODE_FORWARD_TYPE}',
key: e.key
}, '*');
}
}
});
})();
`,
}}
/>
)
}

View File

@@ -19,7 +19,7 @@ function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
<MenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
className={cn(
"hover:bg-muted data-popup-open:bg-muted border-foreground/10 bg-muted/50 relative w-[160px] shrink-0 touch-manipulation rounded-xl border p-2 select-none disabled:opacity-50 md:w-full md:rounded-lg md:border-transparent md:bg-transparent",
"relative w-40 shrink-0 touch-manipulation rounded-xl p-3 ring-1 ring-foreground/10 select-none hover:bg-muted focus-visible:ring-foreground/50 focus-visible:outline-none disabled:opacity-50 data-popup-open:bg-muted md:w-full md:rounded-lg md:px-2.5 md:py-2",
className
)}
{...props}
@@ -31,7 +31,7 @@ function PickerContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
sideOffset = 20,
anchor,
className,
...props
@@ -53,13 +53,13 @@ function PickerContent({
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground cn-menu-target ring-foreground/10 no-scrollbar z-50 max-h-(--available-height) w-[calc(var(--available-width)-(--spacing(3.5)))] min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border-0 p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden md:w-52",
"cn-menu-target z-50 no-scrollbar max-h-(--available-height) w-[calc(var(--available-width)-(--spacing(6)))] min-w-32 origin-(--transform-origin) translate-y-2 overflow-x-hidden overflow-y-auto rounded-xl border-0 bg-neutral-950/80 p-1.5 text-neutral-100 ring-1 ring-neutral-950/80 backdrop-blur-xl outline-none md:w-52 dark:bg-neutral-800/90 dark:ring-neutral-700/50 data-closed:overflow-hidden",
className
)}
{...props}
/>
</MenuPrimitive.Positioner>
<div className="absolute inset-0 z-40 bg-transparent" />
<div className="absolute inset-y-0 right-0 left-62 z-40 bg-transparent" />
</MenuPrimitive.Portal>
)
}
@@ -80,7 +80,7 @@ function PickerLabel({
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"text-muted-foreground px-2 py-1.5 text-xs font-medium data-[inset]:pl-8",
"px-2 py-1.5 text-xs font-medium text-neutral-400 data-inset:pl-8",
className
)}
{...props}
@@ -103,7 +103,7 @@ function PickerItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium outline-hidden select-none **:text-neutral-100 focus:bg-neutral-600 focus:text-neutral-100 focus:**:text-neutral-100 data-inset:pl-8 dark:focus:bg-neutral-700/80 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
@@ -128,7 +128,7 @@ function PickerSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent/95 focus:text-accent-foreground focus:ring-1 focus:ring-foreground/20 not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-8 data-open:bg-accent/95 data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
@@ -158,7 +158,7 @@ function PickerSubContent({
<PickerContent
data-slot="dropdown-menu-sub-content"
className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground w-auto min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100",
"w-auto min-w-[96px] rounded-md bg-popover/90 p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 backdrop-blur-xs duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
align={align}
@@ -180,7 +180,7 @@ function PickerCheckboxItem({
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent/95 focus:text-accent-foreground focus:ring-1 focus:ring-foreground/20 focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
@@ -220,7 +220,7 @@ function PickerRadioItem({
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-sm font-medium outline-hidden select-none **:text-neutral-100 focus:bg-neutral-600 focus:text-neutral-100 focus:**:text-neutral-100 data-inset:pl-8 dark:focus:bg-neutral-700/80 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
@@ -252,7 +252,10 @@ function PickerSeparator({
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
className={cn(
"-mx-1.5 my-1.5 h-px bg-neutral-600 dark:bg-neutral-700",
className
)}
{...props}
/>
)
@@ -263,7 +266,7 @@ function PickerShortcut({ className, ...props }: React.ComponentProps<"span">) {
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
"ml-auto text-xs tracking-widest text-neutral-400! group-focus/dropdown-menu-item:text-neutral-100",
className
)}
{...props}

View File

@@ -0,0 +1,38 @@
"use client"
import * as React from "react"
import { useRouter } from "next/navigation"
import { generateRandomPreset, isPresetCode } from "shadcn/preset"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function PresetHandler() {
const router = useRouter()
const [params, setParams] = useDesignSystemSearchParams()
const hasConverted = React.useRef(false)
React.useEffect(() => {
if (params.preset === "random") {
router.replace(`/create?preset=${generateRandomPreset()}`)
}
}, [params.preset, router])
React.useEffect(() => {
if (hasConverted.current) {
return
}
hasConverted.current = true
if (!params.preset || params.preset === "random") {
return
}
if (isPresetCode(params.preset)) {
return
}
setParams({ base: params.base })
}, [params.preset, params.base, setParams])
return null
}

View File

@@ -31,8 +31,10 @@ export function PresetPicker({
preset.style === params.style &&
preset.baseColor === params.baseColor &&
preset.theme === params.theme &&
preset.chartColor === params.chartColor &&
preset.iconLibrary === params.iconLibrary &&
preset.font === params.font &&
preset.fontHeading === params.fontHeading &&
preset.menuAccent === params.menuAccent &&
preset.menuColor === params.menuColor &&
preset.radius === params.radius
@@ -43,8 +45,10 @@ export function PresetPicker({
params.style,
params.baseColor,
params.theme,
params.chartColor,
params.iconLibrary,
params.font,
params.fontHeading,
params.menuAccent,
params.menuColor,
params.radius,
@@ -67,8 +71,10 @@ export function PresetPicker({
style: preset.style,
baseColor: preset.baseColor,
theme: preset.theme,
chartColor: preset.chartColor,
iconLibrary: preset.iconLibrary,
font: preset.font,
fontHeading: preset.fontHeading,
menuAccent: preset.menuAccent,
menuColor: preset.menuColor,
radius: preset.radius,
@@ -80,8 +86,8 @@ export function PresetPicker({
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Preset</div>
<div className="text-foreground line-clamp-1 text-sm font-medium">
<div className="text-xs text-muted-foreground">Preset</div>
<div className="line-clamp-1 text-sm font-medium text-foreground">
{currentPreset?.description ?? "Custom"}
</div>
</div>
@@ -100,7 +106,11 @@ export function PresetPicker({
{currentBasePresets.map((preset) => {
const style = STYLES.find((s) => s.name === preset.style)
return (
<PickerRadioItem key={preset.title} value={preset.title}>
<PickerRadioItem
key={preset.title}
value={preset.title}
closeOnClick={isMobile}
>
<div className="flex items-center gap-2">
{style?.icon && (
<div className="flex size-4 shrink-0 items-center justify-center">

View File

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

View File

@@ -1,11 +1,13 @@
"use client"
import * as React from "react"
import { type ImperativePanelHandle } from "react-resizable-panels"
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker"
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/action-menu"
import {
REDO_FORWARD_TYPE,
UNDO_FORWARD_TYPE,
} from "@/app/(create)/components/history-buttons"
import { DARK_MODE_FORWARD_TYPE } from "@/app/(create)/components/mode-switcher"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
import {
@@ -13,17 +15,75 @@ import {
useDesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
// Hoisted — only uses module-level constants, no component state. (rendering-hoist-jsx)
function handleMessage(event: MessageEvent) {
if (
typeof window === "undefined" ||
event.origin !== window.location.origin
) {
return
}
const type = event.data.type
if (type === CMD_K_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "k",
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === RANDOMIZE_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "r",
bubbles: true,
cancelable: true,
})
)
} else if (type === UNDO_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "z",
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === REDO_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "z",
shiftKey: true,
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === DARK_MODE_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "d",
bubbles: true,
cancelable: true,
})
)
}
}
export function Preview() {
const [params] = useDesignSystemSearchParams()
const iframeRef = React.useRef<HTMLIFrameElement>(null)
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
// Sync resizable panel with URL param changes.
React.useEffect(() => {
if (resizablePanelRef.current && params.size) {
resizablePanelRef.current.resize(params.size)
}
}, [params.size])
React.useEffect(() => {
const iframe = iframeRef.current
@@ -45,44 +105,6 @@ export function Preview() {
}
}, [params])
const handleMessage = (event: MessageEvent) => {
if (event.data.type === CMD_K_FORWARD_TYPE) {
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
const key = event.data.key || "k"
const syntheticEvent = new KeyboardEvent("keydown", {
key,
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
document.dispatchEvent(syntheticEvent)
}
if (event.data.type === RANDOMIZE_FORWARD_TYPE) {
const key = event.data.key || "r"
const syntheticEvent = new KeyboardEvent("keydown", {
key,
bubbles: true,
cancelable: true,
})
document.dispatchEvent(syntheticEvent)
}
if (event.data.type === DARK_MODE_FORWARD_TYPE) {
const key = event.data.key || "d"
const syntheticEvent = new KeyboardEvent("keydown", {
key,
bubbles: true,
cancelable: true,
})
document.dispatchEvent(syntheticEvent)
}
}
React.useEffect(() => {
window.addEventListener("message", handleMessage)
return () => {
@@ -104,9 +126,9 @@ export function Preview() {
}, [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" />
<div className="relative flex flex-1 flex-col justify-center overflow-hidden rounded-2xl ring ring-foreground/10 md:ring-muted dark:ring-foreground/10">
<div className="relative z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden">
<div className="absolute inset-0 bg-muted dark:bg-muted/30" />
<iframe
key={params.base + params.item}
ref={iframeRef}
@@ -114,12 +136,6 @@ export function Preview() {
className="z-10 size-full flex-1"
title="Preview"
/>
<Badge
className="absolute right-2 bottom-2 isolate z-10"
variant="secondary"
>
Preview
</Badge>
</div>
</div>
)

View File

@@ -0,0 +1,375 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/examples/base/ui/dialog"
import {
Field,
FieldContent,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/examples/base/ui/field"
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui/radio-group"
import { Switch } from "@/examples/base/ui/switch"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/examples/base/ui/tabs"
import { Copy01Icon, Globe02Icon, Tick02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { BASES, type BaseName } from "@/registry/config"
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
import {
getFramework,
getTemplateValue,
NO_MONOREPO_FRAMEWORKS,
TEMPLATES,
} from "@/app/(create)/lib/templates"
const TURBOREPO_LOGO =
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Turborepo</title><path d="M11.9906 4.1957c-4.2998 0-7.7981 3.501-7.7981 7.8043s3.4983 7.8043 7.7981 7.8043c4.2999 0 7.7982-3.501 7.7982-7.8043s-3.4983-7.8043-7.7982-7.8043m0 11.843c-2.229 0-4.0356-1.8079-4.0356-4.0387s1.8065-4.0387 4.0356-4.0387S16.0262 9.7692 16.0262 12s-1.8065 4.0388-4.0356 4.0388m.6534-13.1249V0C18.9726.3386 24 5.5822 24 12s-5.0274 11.66-11.356 12v-2.9139c4.7167-.3372 8.4516-4.2814 8.4516-9.0861s-3.735-8.749-8.4516-9.0861M5.113 17.9586c-1.2502-1.4446-2.0562-3.2845-2.2-5.3046H0c.151 2.8266 1.2808 5.3917 3.051 7.3668l2.0606-2.0622zM11.3372 24v-2.9139c-2.02-.1439-3.8584-.949-5.3019-2.2018l-2.0606 2.0623c1.975 1.773 4.538 2.9022 7.361 3.0534z"/></svg>'
const ORIGIN = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000"
const IS_LOCAL_DEV = ORIGIN.includes("localhost")
const SHADCN_VERSION = process.env.NEXT_PUBLIC_RC ? "@rc" : "@latest"
const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"] as const
type PackageManager = (typeof PACKAGE_MANAGERS)[number]
export function ProjectForm({
className,
}: React.ComponentProps<typeof Button>) {
const [open, setOpen] = React.useState(false)
const [params, setParams] = useDesignSystemSearchParams()
const presetCode = usePresetCode()
const [config, setConfig] = useConfig()
const [hasCopied, setHasCopied] = React.useState(false)
const packageManager = (config.packageManager || "pnpm") as PackageManager
const framework = React.useMemo(
() => getFramework(params.template ?? "next"),
[params.template]
)
const isMonorepo = React.useMemo(
() => params.template?.endsWith("-monorepo") ?? false,
[params.template]
)
const hasMonorepo = !NO_MONOREPO_FRAMEWORKS.includes(
framework as (typeof NO_MONOREPO_FRAMEWORKS)[number]
)
const commands = React.useMemo(() => {
const presetFlag = ` --preset ${presetCode}`
const baseFlag = params.base !== "radix" ? ` --base ${params.base}` : ""
const templateFlag = ` --template ${framework}`
const monorepoFlag = isMonorepo ? " --monorepo" : ""
const rtlFlag = params.rtl ? " --rtl" : ""
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
return IS_LOCAL_DEV
? {
pnpm: `shadcn init${flags}`,
npm: `shadcn init${flags}`,
yarn: `shadcn init${flags}`,
bun: `shadcn init${flags}`,
}
: {
pnpm: `pnpm dlx shadcn${SHADCN_VERSION} init${flags}`,
npm: `npx shadcn${SHADCN_VERSION} init${flags}`,
yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`,
bun: `bunx --bun shadcn${SHADCN_VERSION} init${flags}`,
}
}, [framework, isMonorepo, params.base, params.rtl, presetCode])
const command = commands[packageManager]
React.useEffect(() => {
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
const handleCopy = React.useCallback(() => {
const properties: Record<string, string> = {
command,
}
if (params.template) {
properties.template = params.template
}
copyToClipboardWithMeta(command, {
name: "copy_npm_command",
properties,
})
setHasCopied(true)
}, [command, params.template])
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button className={cn(className)} />}>
Create Project
</DialogTrigger>
<DialogContent className="dark no-scrollbar max-h-[calc(100svh-2rem)] overflow-y-auto rounded-2xl border-0 bg-neutral-800 p-6 text-foreground shadow-xl ring-1 ring-neutral-950/80 backdrop-blur-xl [--border:var(--color-neutral-700)]! sm:max-w-sm">
<DialogHeader>
<DialogTitle>Create Project</DialogTitle>
<DialogDescription>
Pick a template and configure your project.
</DialogDescription>
</DialogHeader>
<div>
<FieldGroup>
<FieldSeparator className="-mx-6" />
<Field className="-mt-2 gap-3">
<FieldLabel>Template</FieldLabel>
<TemplateGrid template={params.template} setParams={setParams} />
</Field>
<FieldSeparator className="-mx-6" />
<Field className="-mt-2">
<FieldLabel>Base</FieldLabel>
<BaseGrid base={params.base} setParams={setParams} />
</Field>
<FieldSeparator className="-mx-6" />
<FieldSet>
<FieldLegend variant="label" className="sr-only">
Options
</FieldLegend>
<Field
orientation="horizontal"
data-disabled={hasMonorepo ? undefined : "true"}
>
<FieldLabel htmlFor="monorepo">
<span
className="size-4 text-neutral-100 [&_svg]:size-4 [&_svg]:fill-current"
dangerouslySetInnerHTML={{
__html: TURBOREPO_LOGO,
}}
/>
Create a monorepo
</FieldLabel>
<Switch
id="monorepo"
checked={params.template?.endsWith("-monorepo") ?? false}
disabled={!hasMonorepo}
onCheckedChange={(checked) => {
const framework = getFramework(params.template ?? "next")
setParams({
template: getTemplateValue(
framework,
checked === true
) as typeof params.template,
})
}}
/>
</Field>
<FieldSeparator className="-mx-6" />
<Field orientation="horizontal">
<FieldLabel htmlFor="rtl">
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
Enable RTL support
</FieldLabel>
<Switch
id="rtl"
checked={params.rtl}
onCheckedChange={(checked) =>
setParams({ rtl: checked === true })
}
/>
</Field>
</FieldSet>
</FieldGroup>
</div>
<DialogFooter className="-mx-6 -mb-6 min-w-0">
<div className="flex w-full min-w-0 flex-col gap-3">
<Tabs
value={packageManager}
onValueChange={(value) => {
setConfig((prev) => ({
...prev,
packageManager: value as PackageManager,
}))
}}
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 bg-neutral-950/20 ring-1 ring-neutral-950/80 dark:bg-neutral-900/50 dark:ring-neutral-700/50"
>
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
<TabsList className="bg-transparent font-mono">
{PACKAGE_MANAGERS.map((manager) => {
return (
<TabsTrigger
key={manager}
value={manager}
className="py-0 leading-none data-[state=active]:shadow-none"
>
{manager}
</TabsTrigger>
)
})}
</TabsList>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto"
onClick={handleCopy}
>
{hasCopied ? (
<HugeiconsIcon icon={Tick02Icon} />
) : (
<HugeiconsIcon icon={Copy01Icon} />
)}
<span className="sr-only">Copy command</span>
</Button>
</div>
{Object.entries(commands).map(([key, cmd]) => {
return (
<TabsContent key={key} value={key}>
<div className="relative overflow-hidden border-t border-neutral-700/50 bg-neutral-900/50 px-3 py-3 text-neutral-100">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">
{cmd}
</code>
</div>
</div>
</TabsContent>
)
})}
</Tabs>
<Button onClick={handleCopy} className="h-9 w-full">
{hasCopied ? "Copied" : "Copy Command"}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
const TemplateGrid = React.memo(function TemplateGrid({
template,
setParams,
}: {
template: DesignSystemSearchParams["template"]
setParams: ReturnType<typeof useDesignSystemSearchParams>[1]
}) {
const isMonorepo = template?.endsWith("-monorepo") ?? false
const framework = getFramework(template ?? "next")
const handleTemplateChange = React.useCallback(
(value: string) => {
setParams({
template: getTemplateValue(
value,
isMonorepo
) as DesignSystemSearchParams["template"],
})
},
[isMonorepo, setParams]
)
return (
<RadioGroup
value={framework}
onValueChange={handleTemplateChange}
className="grid grid-cols-2 gap-2"
>
{TEMPLATES.map((item) => (
<FieldLabel
key={item.value}
htmlFor={`template-${item.value}`}
className="block w-full"
>
<Field
orientation="horizontal"
className="w-full rounded-md transition-colors duration-150 hover:bg-neutral-700/45"
>
<FieldContent className="flex flex-row items-center gap-2 px-2.5 py-1.5">
<div
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
dangerouslySetInnerHTML={{
__html: item.logo,
}}
></div>
<FieldTitle>{item.title}</FieldTitle>
</FieldContent>
<RadioGroupItem
value={item.value}
id={`template-${item.value}`}
className="sr-only absolute"
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
)
})
const BaseGrid = React.memo(function BaseGrid({
base,
setParams,
}: {
base: DesignSystemSearchParams["base"]
setParams: ReturnType<typeof useDesignSystemSearchParams>[1]
}) {
const handleBaseChange = React.useCallback(
(value: string) => {
setParams({ base: value as BaseName })
},
[setParams]
)
return (
<RadioGroup
value={base}
onValueChange={handleBaseChange}
aria-label="Base"
className="grid grid-cols-2 gap-2"
>
{BASES.map((item) => (
<FieldLabel
key={item.name}
htmlFor={`base-${item.name}`}
className="block w-full"
>
<Field
orientation="horizontal"
className="w-full rounded-md transition-colors duration-150 hover:bg-neutral-700/45"
>
<FieldContent className="flex flex-row items-center gap-2 px-2.5 py-1.5">
<div
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
dangerouslySetInnerHTML={{
__html: item.meta?.logo ?? "",
}}
/>
<FieldTitle>{item.title}</FieldTitle>
</FieldContent>
<RadioGroupItem
value={item.name}
id={`base-${item.name}`}
className="sr-only absolute"
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
)
})

View File

@@ -1,5 +1,7 @@
"use client"
import * as React from "react"
import { RADII, type RadiusValue } from "@/registry/config"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
@@ -21,22 +23,26 @@ export function RadiusPicker({
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const isRadiusLocked = params.style === "lyra"
const selectedRadiusName = isRadiusLocked ? "none" : params.radius
const currentRadius = RADII.find((radius) => radius.name === params.radius)
const currentRadius = RADII.find(
(radius) => radius.name === selectedRadiusName
)
const defaultRadius = RADII.find((radius) => radius.name === "default")
const otherRadii = RADII.filter((radius) => radius.name !== "default")
return (
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<PickerTrigger disabled={isRadiusLocked}>
<div className="flex flex-col justify-start text-left">
<div className="text-muted-foreground text-xs">Radius</div>
<div className="text-foreground text-sm font-medium">
<div className="text-xs text-muted-foreground">Radius</div>
<div className="text-sm font-medium text-foreground">
{currentRadius?.label}
</div>
</div>
<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">
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base text-foreground select-none md:right-2.5">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@@ -63,6 +69,9 @@ export function RadiusPicker({
<PickerRadioGroup
value={currentRadius?.name}
onValueChange={(value) => {
if (isRadiusLocked) {
return
}
setParams({ radius: value as RadiusValue })
}}
>
@@ -71,20 +80,20 @@ export function RadiusPicker({
<PickerRadioItem
key={defaultRadius.name}
value={defaultRadius.name}
closeOnClick={isMobile}
>
<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>
{defaultRadius.label}
</PickerRadioItem>
)}
</PickerGroup>
<PickerSeparator />
<PickerGroup>
{otherRadii.map((radius) => (
<PickerRadioItem key={radius.name} value={radius.name}>
<PickerRadioItem
key={radius.name}
value={radius.name}
closeOnClick={isMobile}
>
{radius.label}
</PickerRadioItem>
))}
@@ -94,7 +103,7 @@ export function RadiusPicker({
</Picker>
<LockButton
param="radius"
className="absolute top-1/2 right-10 -translate-y-1/2"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)

View File

@@ -1,146 +1,34 @@
"use client"
import * as React from "react"
import Script from "next/script"
import { Button } from "@/examples/base/ui/button"
import { DiceFaces05Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import {
BASE_COLORS,
getThemesForBaseColor,
iconLibraries,
MENU_ACCENTS,
MENU_COLORS,
RADII,
STYLES,
} from "@/registry/config"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { useLocks } from "@/app/(create)/hooks/use-locks"
import { FONTS } from "@/app/(create)/lib/fonts"
import {
applyBias,
RANDOMIZE_BIASES,
type RandomizeContext,
} from "@/app/(create)/lib/randomize-biases"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { cn } from "@/lib/utils"
import { useRandom } from "@/app/(create)/hooks/use-random"
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
function randomItem<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)]
}
export function RandomButton() {
const { locks } = useLocks()
const [params, setParams] = useDesignSystemSearchParams()
const handleRandomize = React.useCallback(() => {
// Use current value if locked, otherwise randomize.
const baseColor = locks.has("baseColor")
? params.baseColor
: randomItem(BASE_COLORS).name
const selectedStyle = locks.has("style")
? params.style
: randomItem(STYLES).name
// Build context for bias application.
const context: RandomizeContext = {
style: selectedStyle,
baseColor,
}
const availableThemes = getThemesForBaseColor(baseColor)
const availableFonts = applyBias(FONTS, context, RANDOMIZE_BIASES.fonts)
const availableRadii = applyBias(RADII, context, RANDOMIZE_BIASES.radius)
const selectedTheme = locks.has("theme")
? params.theme
: randomItem(availableThemes).name
const selectedFont = locks.has("font")
? params.font
: randomItem(availableFonts).value
const selectedRadius = locks.has("radius")
? params.radius
: randomItem(availableRadii).name
const selectedIconLibrary = locks.has("iconLibrary")
? params.iconLibrary
: randomItem(Object.values(iconLibraries)).name
const selectedMenuAccent = locks.has("menuAccent")
? params.menuAccent
: randomItem(MENU_ACCENTS).value
const selectedMenuColor = locks.has("menuColor")
? params.menuColor
: randomItem(MENU_COLORS).value
// Update context with selected values for potential future biases.
context.theme = selectedTheme
context.font = selectedFont
context.radius = selectedRadius
setParams({
style: selectedStyle,
baseColor,
theme: selectedTheme,
iconLibrary: selectedIconLibrary,
font: selectedFont,
menuAccent: selectedMenuAccent,
menuColor: selectedMenuColor,
radius: selectedRadius,
})
}, [setParams, locks, params])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === "r" || e.key === "R") && !e.metaKey && !e.ctrlKey) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
e.preventDefault()
handleRandomize()
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [handleRandomize])
export function RandomButton({
variant = "outline",
className,
...props
}: React.ComponentProps<typeof Button>) {
const { randomize } = useRandom()
return (
<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>
<Button
variant={variant}
onClick={randomize}
className={cn(
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
className
)}
{...props}
>
<span className="w-full text-center font-medium">Shuffle</span>
</Button>
)
}

View File

@@ -1,10 +1,5 @@
"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,
@@ -14,60 +9,25 @@ import {
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"
} from "@/examples/base/ui/alert-dialog"
export function ResetButton() {
const [params, setParams] = useDesignSystemSearchParams()
import { useReset } from "@/app/(create)/hooks/use-reset"
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])
export function ResetDialog() {
const { showResetDialog, setShowResetDialog, confirmReset } = useReset()
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">
<AlertDialog open={showResetDialog} onOpenChange={setShowResetDialog}>
<AlertDialogContent size="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>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmReset}>Reset</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -1,37 +1,23 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui/button"
import { Share03Icon, Tick02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function ShareButton() {
const [params] = useDesignSystemSearchParams()
const presetCode = usePresetCode()
const [hasCopied, setHasCopied] = React.useState(false)
const shareUrl = React.useMemo(() => {
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
return `${origin}/create?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&item=${params.item}`
}, [
params.base,
params.style,
params.baseColor,
params.theme,
params.iconLibrary,
params.font,
params.menuAccent,
params.menuColor,
params.radius,
params.item,
])
return `${origin}/create?preset=${presetCode}&item=${params.item}`
}, [presetCode, params.item])
React.useEffect(() => {
if (hasCopied) {
@@ -51,23 +37,21 @@ export function ShareButton() {
}, [shareUrl])
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
className="rounded-lg shadow-none lg:w-8 xl:w-fit"
onClick={handleCopy}
>
{hasCopied ? (
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
) : (
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
)}
<span className="lg:hidden xl:block">Share</span>
</Button>
</TooltipTrigger>
<TooltipContent>Copy Link</TooltipContent>
</Tooltip>
<Button variant="outline" className="hidden md:flex" onClick={handleCopy}>
{hasCopied ? (
<HugeiconsIcon
icon={Tick02Icon}
strokeWidth={2}
data-icon="inline-start"
/>
) : (
<HugeiconsIcon
icon={Share03Icon}
strokeWidth={2}
data-icon="inline-start"
/>
)}
Share
</Button>
)
}

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