Compare commits

...

103 Commits

Author SHA1 Message Date
shadcn
093a538453 feat(registry): build fonts for new-york-v4 (#11116)
* feat: build fonts for new-york-v4

* chore: clean up orphan fonts
2026-07-08 16:59:56 +04:00
github-actions[bot]
5e800ca2bd chore(release): version packages (#11099)
Co-authored-by: shadcn <m@shadcn.com>
2026-07-08 12:10:47 +04:00
shadcn
f6a2647854 fix(react): follow the live edge after an anchored reply fills the viewport (#11106)
* fix(react): follow the live edge after an anchored reply fills the viewport

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186fcQpHo1aLQC4wRpwKFFu

* fix(react): keep the scroll button quiet while follow-output is engaged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186fcQpHo1aLQC4wRpwKFFu

* fix(v4): wire base commands example menu items with onClick

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186fcQpHo1aLQC4wRpwKFFu

* style: format

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 12:07:23 +04:00
shadcn
68e1f171aa refactor(create): consolidate create code into a single route group (#11102)
* refactor(create): consolidate create code into a single route group

Move all create code from app/(app)/create and app/(create) into the
nested route group app/(app)/(create), and the preview iframe pages
into app/(view). URLs are unchanged: /create keeps the (app) chrome
(and the header now persists across / <-> /create), /init keeps its
root-level path for the CLI contract, and /preview stays chrome-less
for the customizer iframe.

Registry sources and published payloads are untouched: two temporary
tsconfig path aliases map @/app/(create)/* and the legacy
@/app/(app)/create/* onto the new location. App code imports the real
path.

Also inline parse-config and build-instructions into the create lib so
the init routes are thin HTTP handlers, and remove the icon-placeholder
and lib re-export shims.

* fix
2026-07-07 15:59:14 +04:00
shadcn
dd6ce77cf1 fix(react): hold newly appended scroll anchors while autoScroll is enabled (#11100)
The tail spacer makes a freshly anchored turn read as "at the end", so
reconcileFollowMode re-armed following-bottom one frame after the anchor
placement and the first streamed chunk yanked the reader off the anchor.
Exclude the anchored-to-message hold from arming, matching the existing
settling-jump exclusion.


Claude-Session: https://claude.ai/code/session_0186fcQpHo1aLQC4wRpwKFFu

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:04:10 +04:00
Erick
628e427231 fix(message-scroller): avoid ResizeObserver loop error by coalescing resize handling into rAF (#11085)
Co-authored-by: shadcn <m@shadcn.com>
2026-07-07 09:37:08 +04:00
shadcn
d8ace420ba feat(create): show a loading placeholder for the customizer and preview (#11093)
* feat(create): show a loading placeholder for the customizer and preview

* fix(create): render the placeholder as the suspense fallback so it prerenders
2026-07-06 13:13:00 +04:00
shadcn
4163c1dbea feat(create): pick shuffle presets from a curated list (#11092)
* feat(create): pick shuffle presets from a curated list

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: format

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:32:29 +04:00
Pulkit
468674b1f0 docs(registry): update @pulkitxm homepage to pulkit.page (#11016)
* docs(registry): update @pulkitxm homepage to pulkit.page

* Update directory.json
2026-07-06 11:51:43 +04:00
Yuta Nakano
b13050763e feat(registry): add @pulld to directory (#10997) 2026-07-06 11:50:27 +04:00
sezabut
c4b22943fe feat(registry): add @mozaika to the registry directory (#11090)
Co-authored-by: Sam C <sam@mozaika.design>
2026-07-06 11:49:51 +04:00
Eric Tsai
34f0dabe48 feat(registry): add @ericts registry (#11087) 2026-07-06 11:49:35 +04:00
heykirman
5b5db1f8ea feat(registry): add @nusaiba registry (#11076)
Add Nusaiba motion-first shadcn marketing blocks to the public registry index.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 11:49:20 +04:00
shadcn
d0fae52822 fix 2026-07-03 16:27:55 +04:00
github-actions[bot]
2ef6388ca1 chore(release): version packages (#11083)
* chore(release): version packages

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-07-03 16:20:38 +04:00
shadcn
f3e7de1175 feat: base (#11082)
* feat(create): default design system config to base ui

* feat(www): default docs and init to base ui

* feat(www): default preview styles to base-nova

* feat(shadcn): default init and prompts to base ui

* docs: default to base ui in guides and examples

* docs: add changelog

* chore: changeset

* fix: address base ui default review findings

* fix: init handling

* feat: add migrate-radix-to-base skills

* fix: typo
2026-07-03 16:14:53 +04:00
jasleenkaur-qed42
a409271270 fix(create): prevent customizer reset when template query param is set (#11074)
* fix(create): prevent customizer reset when template query param is set

Use shallow nuqs updates and remove racing picker effects so selections
persist on /create?template=start. Sync preset to URL once across hook
instances and add regression tests for preset URL encoding.

Fixes #11060

* fix(create): apply undo/redo via shallow preset update

Undo/redo went through router.replace, which triggers a full server
navigation that resets the preset on prod (#11060). Route it through the
shared shallow search-params update instead, and keep the useSearchParams
reader so the transient values nuqs emits mid-update are not recorded as
phantom history entries.

* fix(create): satisfy typecheck and prettier in search-params test

Add the required `template` field to the first buildPresetUrlUpdate
fixture so it matches DesignSystemSearchParams, and apply prettier
formatting to search-params.ts and its test (both were failing
format:check).

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-07-02 19:43:25 +04:00
Utkarsh Singhal
70afde8358 feat(registry): add @vue-bits and @svelte-bits to the registry directory (#11066) 2026-07-02 15:58:38 +04:00
shadcn
574e85e22d Merge branch 'main' of github.com:shadcn-ui/ui 2026-07-02 14:50:44 +04:00
shadcn
3310c45dfb chore: back to neutral 2026-07-02 14:46:25 +04:00
coi
34223a842d refactor(base-drawer): migrate base drawer to @base-ui/react/drawer (#10430)
* refactor(base-drawer): rewrite base drawer wrapper for @base-ui/react/drawer

Replace vaul with @base-ui/react/drawer as the primitive behind the
base drawer wrapper.

Keep the public API aligned with the radix drawer shape while
rewriting DrawerContent to compose Backdrop, Viewport, Popup, and
Content internally.

Keep the registry dependency change in the same commit so the wrapper
rewrite lands as one source-level migration step.

* refactor(drawer): move shared drawer direction rules from stylesheets to primitives

Shared drawer styles previously owned direction-specific layout in CSS.
With base and radix now exposing different direction attributes,
keeping that logic in shared tokens would duplicate primitive-specific
branching in the stylesheets.

Move direction-specific layout into the base/radix drawer wrappers and
leave shared CSS responsible only for visual surface styling.

Also move handle visibility and header alignment into TSX, and unify
the shared drawer token names to cn-drawer-*.

* fix(base-drawer): migrate examples, blocks, and app consumers to render and swipeDirection

Update in-repo base drawer consumers to the new wrapper API.

Replace asChild usage with render, switch direction to swipeDirection,
and align examples, blocks, app consumers, and docs with the new
base drawer usage pattern.

* chore: update registries

* fix(drawer): update base marker example usage

* fix(drawer): clean up base style selectors

* docs: add migration docs

* wip

* fix: nested

* feat: drawer

* fix

* fix

* fix

* fix: pointer events

* fix: clean up radix drawers

* fix: position

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-07-02 14:16:37 +04:00
shadcn
dbf9c5ebc4 chore(deps): bump base-ui to 1.6.0 (#11059) 2026-06-30 10:33:51 +04:00
shadcn
897e9add14 feat(command-menu): add styles group linking to shadcn/create (#11052)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:30:46 +04:00
shadcn
8d6553a7f5 fix: orientation props on attachment (#11053) 2026-06-29 21:29:49 +04:00
shadcn
683073f102 feat(registry): add ai-elements components to new-york-v4 (#11051)
* feat(registry): add ai-elements components to new-york-v4

Adds attachment, bubble, marker, message, and message-scroller (registry:ui) to the new-york-v4 style, ported from radix-vega (also Radix-based, so non-breaking). Regenerates the runtime indexes (__index__.tsx, __components__.tsx) and the built catalog so they stay in sync.

* fix(registry): use resolved orientation for attachment variants

data-orientation used the normalized resolvedOrientation while the variant classes used the raw orientation prop, desyncing them when orientation is null. Use resolvedOrientation for both. Addresses Copilot review on #11051.
2026-06-29 20:56:50 +04:00
shadcn
02e398ab73 feat(skill): add chat component guidance to shadcn skill (#11048)
Add MessageScroller, Message, Bubble, Attachment, and Marker guidance,
driven by a baseline gap analysis: fresh agents hand-rolled all five
primitives, so this adds a rules/chat.md, discovery hooks in SKILL.md,
two evals, and supporting composition/styling rules.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:18:46 +04:00
shadcn
af79276f7e fix(v4): build @shadcn/react before registry build (#11049)
The v4 app imports @shadcn/react/message-scroller, which resolves via the
package's built dist. CI is a fresh clone and registry:build only built
shadcn, so the import was unresolved and the dev server never compiled,
timing out the test job. Build @shadcn/react first.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:54:41 +04:00
Arik Chakma
5a3ad36a5e fix(message-scroller): keep scrollbar gutter stable during autoscroll (#11032) 2026-06-29 16:32:21 +04:00
shrivi-maker
a63e8359ec feat(registry): add @channel3 to the registry directory (#11026)
Co-authored-by: Shrivathsan Sakthisundaram <285776207+shrivi-maker@users.noreply.github.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
2026-06-29 15:24:17 +04:00
Tommaso
a72491cb9b Add evex to registry directory (#11023) 2026-06-27 13:14:17 +04:00
rds_agi
67aec7dcc5 fix(base): wrap dropdown label/items in group in message-scroller commands example (#11025)
DropdownMenuLabel renders Base UI's Menu.GroupLabel, which throws
"MenuGroupContext is missing" (Base UI error #31) unless rendered inside
a Menu.Group. Wrap the label and items in DropdownMenuGroup so the
"Jump to..." menu opens without crashing.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 13:13:58 +04:00
shadcn
40c7064532 docs: changelog 2026-06-26 22:18:01 +04:00
shadcn
5b3369f6ee fix: sidebar link 2026-06-26 21:49:48 +04:00
shadcn
b31e6d63b0 Merge branch 'main' of github.com:shadcn-ui/ui 2026-06-26 21:43:58 +04:00
shadcn
cf5b227565 docs: remove react page 2026-06-26 21:43:41 +04:00
github-actions[bot]
8055a12f46 chore(release): version packages (#11024)
Co-authored-by: shadcn <m@shadcn.com>
2026-06-26 21:25:48 +04:00
shadcn
18fcf0f766 feat: @shadcn/react (#11022)
* feat(@shadcn/react): add message-scroller package

Add the @shadcn/react headless primitives package with MessageScroller
scroll anchoring, streaming follow, history prepend, and jump-to-message
behavior. Includes geometry helpers, use-render utility, and unit,
browser, and perf tests.

* feat(registry): add chat components

Add MessageScroller, Message, Bubble, Attachment, and Marker registry
sources for base and radix, style variants, preview-03 chat blocks,
and registry index wiring.

* feat(v4): integrate chat components into docs site

Wire chat components into the v4 app with docs routes, example preview
pages, message part renderers, markdown support, registry build updates,
and supporting lib utilities.

* feat(examples): add chat component demos

Add base and radix example demos for MessageScroller, Message, Bubble,
Attachment, Marker, scroll-fade, and shimmer.

* docs: add chat component documentation

Add component and utility docs for the chat component set, update docs
navigation, and add the June 2026 chat components changelog entry.

* chore: regenerate registry JSON output

Rebuild public registry artifacts for all style variants with the new
chat components.

* chore(release): add @shadcn/react publish and CI pipeline

Add Changesets prerelease workflow, browser test job, RELEASING docs,
and monorepo wiring for publishing @shadcn/react independently from
the shadcn CLI.

* docs: fix display of component preview on mobile

* fix

* fix

* docs: add message scroller docs

* style: format

* fix
2026-06-26 21:19:31 +04:00
github-actions[bot]
c520191cd4 chore(release): version packages (#10949)
Co-authored-by: shadcn <m@shadcn.com>
2026-06-26 11:15:28 +04:00
Chai Pin Zheng
35983528c2 docs(registry): add @payload-components to directory (#11006)
Summary:
- Add the @payload-components namespace to the public registry directory.
- Include the live registry URL, homepage, description, and logo.

Validation:
- pnpm validate:registries
- curl checks for /r/registry.json and /r/hero-basic.json
2026-06-24 23:55:45 +04:00
shadcn
8692cd4cc1 fix: hookform zod errors (#11007) 2026-06-24 21:30:16 +04:00
shadcn
d3727d8c45 feat: add a /examples/[base]/[name] route 2026-06-24 20:49:27 +04:00
shadcn
e3f98d49e4 chore: update turborepo (#11005) 2026-06-24 20:30:09 +04:00
shadcn
549852bffc chore: update fumadocs (#11004) 2026-06-24 17:52:21 +04:00
Mohammad Shehadeh
4139cff3a0 feat(registry): add hirael to directory (#10965) 2026-06-24 17:35:08 +04:00
Denish Navadiya
95471a0fb9 feat: add @paceui in trusted registries (#10972) 2026-06-24 12:14:35 +04:00
Ray
b59f68ecc5 Add '@uiable' UI component library details and registry (#10976)
* Add '@uiable' UI component library details

Added new entry for '@uiable' with homepage, URL, description, and logo.

* Added @Uiable registry
2026-06-23 10:40:19 +04:00
Oliver Lewandowski
6956a099b3 feat(registry): add @7ovr to registry directory (#10989) 2026-06-23 10:39:46 +04:00
Liamandrew
2417242b12 feat(registry): add @tetra-ui (#10991) 2026-06-23 10:39:20 +04:00
Aniket Pawar
70a7a0c2a8 Update URL format and logo in directory.json (#10988) 2026-06-21 17:46:10 +04:00
Aniket Pawar
5602b81d83 Add new agent '@agentcn' to directory.json (#10985) 2026-06-21 15:49:35 +04:00
Raashish Aggarwal
3ffd3e1c7c fix(registry): update calendar month grid (#10644) 2026-06-19 11:38:39 +04:00
dependabot[bot]
e03df56fcf chore(deps): bump vitest from 2.1.9 to 3.2.6 (#10898)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 2.1.9 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 3.2.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2026-06-18 22:22:32 +04:00
dependabot[bot]
38fb1b6f41 chore(deps): bump astro from 6.3.8 to 6.4.6 in /templates/astro-monorepo (#10960)
Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 6.3.8 to 6.4.6.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.4.6/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 6.4.6
  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-06-18 22:18:47 +04:00
shadcn
13eb6a81d1 fix: active style for carousel 2026-06-18 22:16:29 +04:00
Neon
5cc8a2af42 feat(registry): add @uui to registry directory (#10946) 2026-06-18 21:53:38 +04:00
shadcn
365d53b590 fix(shadcn): preserve existing dependency specifiers in package.json (#10967)
Fixes #10525
2026-06-18 21:46:26 +04:00
Saurabh
c2ddedf5d2 chore(registry): update beUI domain (#10961) 2026-06-18 11:16:18 +04:00
wrappixelTeam
c879483c96 Added Shadcn Space UI Kit to figma docs (#10839)
docs: Added Shadcn Space UI Kit to Figma paid section
2026-06-17 10:53:39 +04:00
jack-at-shadcn-ui-blocks
4885a9a7ad feat(registry): add shadcn-ui-blocks (#10950)
A growing library of shadcn/ui blocks, component variants, and templates —
free to start, with a Pro tier.

Co-authored-by: jack-at-shadcn-ui-blocks <293869725+jack-at-shadcn-ui-blocks@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:53:22 +04:00
dependabot[bot]
2f5929269a chore(deps-dev): bump vite in /templates/start-monorepo (#10959)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.14 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 10:52:59 +04:00
dependabot[bot]
2fdf1bb0d4 chore(deps-dev): bump vite in /templates/vite-monorepo (#10958)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.14 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 10:52:49 +04:00
dependabot[bot]
951750bdbe chore(deps-dev): bump vite in /templates/react-router-monorepo (#10957)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.14 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 10:52:38 +04:00
dependabot[bot]
6c5d5d6374 chore(deps-dev): bump vite in /templates/react-router-app (#10956)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.14 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 10:52:29 +04:00
shadcn
6ec117715f feat: add data-slot to spinner 2026-06-16 13:15:44 +04:00
shadcn
e583c773fe fix: dev server perf (#10954)
* fix: dev server perf

* fix
2026-06-16 11:20:07 +04:00
ChingRu
0f154171a7 feat(registry): add @heatmap to the directory (#10803) 2026-06-16 07:37:15 +04:00
shadcn
9197676b3d chore: replace node-fetch (#10905)
* chore: replace node-fetch

* fix(shadcn): surface network failure reason from fetch errors

* fix(shadcn): avoid Error.cause typings for older TS lib targets

* fix(shadcn): avoid stricter undici engine

* fix(shadcn): use undici 7 proxy agent
2026-06-15 14:08:41 +04:00
Ali Hussein
82dce7e945 fix(registry): remove duplicate entries from directory.json (#10912)
219 entries contained 10 duplicate registry names (209 unique).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: shadcn <m@shadcn.com>
2026-06-15 10:22:53 +04:00
Saurabh
35bc9934bf fix(registry): add beUI logo (#10928) 2026-06-15 10:14:26 +04:00
khanhanh
1fd75c9d7e feat(kaui): add kaui component library (#10929)
* feat(kaui):  add kaui component library

* chore: remove unrelated formatting changes from registry directory

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: shadcn <m@shadcn.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 10:06:21 +04:00
agonist
2ea31d8070 add contentbit registry (#10920) 2026-06-15 09:57:06 +04:00
rajtripathi99
ea9d371a2d Add @untld registry to the directory (#10903)
Registry URL: https://ui.untldlabs.com/r/{name}.json
Homepage: https://ui.untldlabs.com
2026-06-11 20:46:21 +04:00
JOE MON
d15f17d717 feat(registry): add braaile loader registry (#10923) 2026-06-11 20:44:29 +04:00
Saurabh
46ca8c5d4b feat(registry): add beui (#10887) 2026-06-11 20:41:01 +04:00
q32757468
a5eb279650 feat(registry): add gpt-vis registry (#10794) 2026-06-11 19:03:21 +04:00
shadcn
1994caba0b perf: dev server (#10904)
* perf: dev server

* fix
2026-06-10 11:10:01 +04:00
Koishore Roy
1450bea8d6 Add @delego to the registry directory (#10901) 2026-06-10 10:12:19 +04:00
Aniket Pawar
ced2a5beb5 Add new entry for @ogimagecn in directory.json (#10896) 2026-06-09 17:03:32 +04:00
Terra
10f1717a3e Add Saaskit component to directory.json (#10494)
* Add Saaskit component to directory.json

Added Saaskit component with description, URL, author, and logo.

* Update Saaskit entry in directory.json

* Add new registry entry for @saaskit

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 10:00:24 +04:00
Benedict Umeozor
deba036d50 fix(docs): disable ligatures in code blocks (#10809) 2026-06-08 23:51:25 +04:00
shadcn
5bd81beebf docs: add API reference for shadcn 2026-06-08 20:17:12 +04:00
github-actions[bot]
3f2ff18157 chore(release): version packages (#10873)
Co-authored-by: shadcn <m@shadcn.com>
2026-06-08 17:48:39 +04:00
shadcn
05eb2b968b feat(cli): improve search command (#10886)
- Search across multiple registries and make the registry argument
  optional: omit it to search every registry configured in components.json
  (builtins like @shadcn excluded). Without a components.json or configured
  registries, a clear usage error is printed.
- Add a --type filter (accepts "ui" or "registry:ui", comma-separated)
  with validation against the known item types.
- Fetch registries concurrently with a capped worker pool, preserving
  result order.
- Tolerate per-registry failures when searching all configured registries
  (reported in a structured `errors` field); exit non-zero when every
  registry fails. Usage errors print directly instead of routing through
  handleError.
- MCP parity: optional registries (search-all), a `types` filter, and type
  validation across the search/list/examples tools.
- Keep the public registry surface to `searchRegistries` and make it
  self-contained (clears its own context, useCache defaults to false).
- Consolidate search formatting into registry/search, add the `errors`
  field to searchResultsSchema, and update the skill docs.
2026-06-08 17:46:00 +04:00
Truong Giang
a721cc08e5 feat: add @soralabs registry (#10884)
* feat(registry): add @sora-ui to community registry directory

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(registry): rename @sora-ui to @soralabs in community registry directory

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 12:08:49 +04:00
shadcn
8da4592308 feat: update registry build commands (#10880)
* feat: update registry build commands

* fix
2026-06-06 23:19:46 +04:00
Sadman Sakib
f47d48f316 feat(registry): update diceui registry url to support style (#10881) 2026-06-06 23:19:34 +04:00
Franco Zeta
e6d9d6023b feat(registry): update @stepper logo (#10875) 2026-06-06 22:52:49 +04:00
Harshitha Sompura
7dfd933102 fix(cli): move msw to devDependencies (#10851)
* fix: move msw to devDependencies

* chore: update lockfile after moving msw to devDependencies

* chore: add changeset for msw devDependency fix

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-06-05 21:01:37 +04:00
Andrew Luo
9c6a5ee1b1 add extend to registry directory (#10850) 2026-06-05 19:49:46 +04:00
Anish K Srinivasan
c87897b2a5 feat(registry): add @gamekitui to community registry directory (#10864)
Drop-in, themeable browser games for shadcn (Snake, 2048, Minesweeper, and
more) — each a single self-contained file with zero dependencies.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:46:49 +04:00
joncoronel
c61197f627 Add new UI component libraries to directory.json (#10858) 2026-06-05 19:45:13 +04:00
shadcn
a1fb619cef feat(card): add spacing and edge-to-edge variants (#10872) 2026-06-05 19:32:28 +04:00
Subhadip Jana
d84c4a8ca5 feat(registry): add @grootstudio to community registry directory (#10698) 2026-06-04 10:24:24 +04:00
Ajay Patel
cd54e0927f registry: updated shadcnstudio registry url with style support (#10847) 2026-06-01 20:22:30 +04:00
github-actions[bot]
adac7cae1f chore(release): version packages (#10845)
Co-authored-by: shadcn <m@shadcn.com>
2026-06-01 14:58:03 +04:00
shadcn
7c63c46736 feat(registry): add GitHub registry support (#10842)
* feat: add github scheme

* fix

* fix: validate and search

* docs: update docs for GitHub registries

* docs: add changelog

* fix

* chore: update announcement

* docs(skills): update GitHub registry guidance

* fix(registry): reject option-like GitHub refs

* fix(registry): limit search registry discovery

* fix(registry): bound GitHub validation concurrency

* fix(registry): reject whitespace in GitHub refs

* fix(registry): track URL dependency sources

* test(registry): cover local dependency sources
2026-06-01 14:53:34 +04:00
shadcn
916c012132 Sort registry directory entries alphabetically by name. (#10836) 2026-06-01 11:44:56 +04:00
github-actions[bot]
460ad60d84 chore(release): version packages (#10835)
Co-authored-by: shadcn <m@shadcn.com>
2026-05-31 16:18:29 +04:00
shadcn
8e2d2d1439 feat: add shadcn eject (#10834) 2026-05-31 16:11:01 +04:00
shadcn
67cef8fcb9 fix(v4): update homepage mobile demo fallback images (#10810)
* fix(v4): update homepage mobile demo fallback images

Replace registry dashboard screenshots with compressed CardsDemo captures and narrow the mobile bleed width to 140vw.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(v4): remove container-wrapper padding on homepage

Use p-0 on the demo section wrapper so the mobile preview image aligns flush without extra horizontal inset.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(v4): drop redundant md:p-0 on homepage wrapper

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 20:21:35 +04:00
shadcn
4ff43ba694 fix(styles): use color-mix for secondary button hover (#10808)
Align secondary button hover with Rhea: mix 5% foreground into
secondary in OKLCH instead of opacity. Updates registry CSS,
shipped button sources, and public registry JSON. Badges unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:37:28 +04:00
shadcn
efdec3ca45 fix(styles): restore primary button hover for Nova and Lyra (#10807)
The default button variant used [a]:hover, which only applies to anchor
elements. Buttons render as <button>, so hover had no effect on create
and in installed projects. Use hover:bg-primary/80 to match other styles.

Fixes #10798

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:13:58 +04:00
shadcn
5c849297d0 feat(release): add beta and rc prerelease labels (#10806) 2026-05-29 15:13:21 +04:00
1106 changed files with 117945 additions and 42151 deletions

35
.github/collect-prerelease-info.js vendored Normal file
View File

@@ -0,0 +1,35 @@
// Collect the packages that a snapshot prerelease just published, so the
// prerelease comment workflow can render an install line per package.
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
import { join } from "node:path"
const [, , prNumber, channel] = process.argv
const packagesDir = join(process.cwd(), "packages")
const published = []
for (const dir of readdirSync(packagesDir)) {
const pkgPath = join(packagesDir, dir, "package.json")
if (!existsSync(pkgPath)) {
continue
}
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
// Snapshot versions are stamped `0.0.0-<channel>-<timestamp>`, so the channel
// marker is how we tell which packages this run actually versioned.
if (
!pkg.private &&
typeof pkg.version === "string" &&
pkg.version.includes(`-${channel}`)
) {
published.push({ name: pkg.name, version: pkg.version })
}
}
writeFileSync(
"prerelease-info.json",
JSON.stringify({ pr: prNumber, channel, packages: published }, null, 2)
)
console.log(`Collected ${published.length} prerelease package(s).`)

View File

@@ -1,21 +0,0 @@
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
// https://github.com/cloudflare/wrangler2/blob/main/.github/version-script.js
import { exec } from "child_process"
import fs from "fs"
const pkgJsonPath = "packages/shadcn/package.json"
try {
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
exec("git rev-parse --short HEAD", (err, stdout) => {
if (err) {
console.log(err)
process.exit(1)
}
pkg.version = "0.0.0-beta." + stdout.trim()
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, "\t") + "\n")
})
} catch (error) {
console.error(error)
process.exit(1)
}

56
.github/workflows/browser-tests.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Browser tests
on:
pull_request:
branches: ["*"]
paths:
- "packages/react/**"
- ".github/workflows/browser-tests.yml"
jobs:
browser:
runs-on: ubuntu-latest
name: pnpm test:browser
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
version: 10.33.4
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Cache Playwright browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright Chromium
run: pnpm --filter=@shadcn/react exec playwright install --with-deps chromium
- run: pnpm --filter=@shadcn/react test:browser

View File

@@ -78,7 +78,7 @@ jobs:
run: pnpm install
- name: Build packages
run: pnpm --filter=shadcn build
run: pnpm build:packages
- run: pnpm format:check
@@ -117,6 +117,6 @@ jobs:
run: pnpm install
- name: Build packages
run: pnpm --filter=shadcn build
run: pnpm build:packages
- run: pnpm typecheck

View File

@@ -1,5 +1,5 @@
# Adapted from create-t3-app.
name: Write Beta Release comment
name: Write Prerelease comment
on:
workflow_run:
@@ -16,51 +16,79 @@ jobs:
runs-on: ubuntu-latest
name: Write comment to the PR
steps:
- name: "Comment on PR"
# Stable pushes and no-changeset runs upload no artifact, so a missing
# download is expected — gate the rest of the job on it succeeding.
- name: Download prerelease info
id: download
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: prerelease-info
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build comment
id: info
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
const fs = require("fs");
const info = JSON.parse(fs.readFileSync("prerelease-info.json", "utf8"));
for (const artifact of allArtifacts.data.artifacts) {
// Extract the PR number and package version from the artifact name
const match = /^npm-package-shadcn@(.*?)-pr-(\d+)/.exec(artifact.name);
if (match) {
require("fs").appendFileSync(
process.env.GITHUB_ENV,
`\nBETA_PACKAGE_VERSION=${match[1]}` +
`\nWORKFLOW_RUN_PR=${match[2]}` +
`\nWORKFLOW_RUN_ID=${context.payload.workflow_run.id}`
);
break;
}
if (!info.packages || info.packages.length === 0) {
core.info("No prerelease packages to comment.");
return;
}
- name: "Comment on PR with Link"
const installs = info.packages
.map((p) => `pnpm dlx ${p.name}@${p.version}`)
.join("\n");
const links = info.packages
.map(
(p) =>
`- [${p.name}@${p.version}](https://www.npmjs.com/package/${p.name}/v/${p.version})`
)
.join("\n");
const body = [
`A new ${info.channel} prerelease is available for testing:`,
"",
"```sh",
installs,
"```",
"",
links,
].join("\n");
core.setOutput("pr", info.pr);
core.setOutput("channel", info.channel);
core.setOutput("body", body);
- name: Comment on PR
if: steps.info.outputs.body
uses: marocchino/sticky-pull-request-comment@v2
with:
number: ${{ env.WORKFLOW_RUN_PR }}
message: |
A new prerelease is available for testing:
number: ${{ steps.info.outputs.pr }}
message: ${{ steps.info.outputs.body }}
```sh
pnpm dlx shadcn@${{ env.BETA_PACKAGE_VERSION }}
```
- name: "Remove the autorelease label once published"
- name: Remove the prerelease label once published
if: steps.info.outputs.pr
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: '${{ env.WORKFLOW_RUN_PR }}',
name: '🚀 autorelease',
});
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number("${{ steps.info.outputs.pr }}"),
name: `release: ${{ steps.info.outputs.channel }}`,
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
core.info("The prerelease label was already removed.");
}

View File

@@ -2,7 +2,7 @@
name: Release
run-name: ${{ github.event_name == 'pull_request' && format('Release Beta - PR {0}', github.event.number) || 'Release Stable' }}
run-name: ${{ github.event_name == 'pull_request' && format('Release Prerelease - PR {0}', github.event.number) || 'Release Stable' }}
on:
pull_request:
@@ -15,8 +15,8 @@ on:
jobs:
prerelease:
if: ${{ github.event_name == 'pull_request' && github.repository_owner == 'shadcn-ui' && contains(github.event.pull_request.labels.*.name, '🚀 autorelease') }}
name: Publish Beta to NPM
if: "${{ github.event_name == 'pull_request' && github.repository_owner == 'shadcn-ui' && (contains(github.event.pull_request.labels.*.name, 'release: beta') || contains(github.event.pull_request.labels.*.name, 'release: rc')) }}"
name: Publish Prerelease to NPM
runs-on: ubuntu-latest
environment: Preview
permissions:
@@ -24,10 +24,36 @@ jobs:
contents: read
steps:
- name: Select prerelease channel
id: prerelease
uses: actions/github-script@v7
with:
script: |
const prereleaseLabels = [
{ name: "release: beta", channel: "beta" },
{ name: "release: rc", channel: "rc" },
];
const labels = context.payload.pull_request.labels.map((label) => label.name);
const selectedLabels = prereleaseLabels.filter((label) =>
labels.includes(label.name)
);
if (selectedLabels.length !== 1) {
throw new Error(
`Expected exactly one prerelease label, found: ${
selectedLabels.map((label) => label.name).join(", ") || "none"
}.`
);
}
core.setOutput("channel", selectedLabels[0].channel);
core.setOutput("label", selectedLabels[0].name);
- name: Checkout Repo
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Use PNPM
uses: pnpm/action-setup@v4
@@ -47,23 +73,49 @@ jobs:
- name: Install NPM Dependencies
run: pnpm install
- name: Modify package.json version
run: node .github/version-script-beta.js
# A snapshot prerelease needs changesets to compute versions. The
# Changesets version PR consumes them, so a label on that PR is a no-op.
- name: Check for changesets
id: changesets
run: |
shopt -s nullglob
present=false
for file in .changeset/*.md; do
if [ "$(basename "$file")" != "README.md" ]; then
present=true
break
fi
done
echo "present=$present" >> "$GITHUB_OUTPUT"
- name: Publish Beta to NPM
run: pnpm pub:beta
- name: No changesets to prerelease
if: steps.changesets.outputs.present == 'false'
run: echo "::notice::No changesets found on this branch; nothing to prerelease."
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
with:
path: packages/shadcn
# Snapshot versions are stamped per run (timestamped), so each publish is
# unique and can never collide with a real release on the latest tag.
- name: Version snapshot
if: steps.changesets.outputs.present == 'true'
run: pnpm exec changeset version --snapshot ${{ steps.prerelease.outputs.channel }}
- name: Upload packaged artifact
- name: Build packages
if: steps.changesets.outputs.present == 'true'
run: pnpm build:packages
- name: Publish snapshot to NPM
if: steps.changesets.outputs.present == 'true'
run: pnpm exec changeset publish --tag ${{ steps.prerelease.outputs.channel }} --no-git-tag
- name: Collect prerelease info
if: steps.changesets.outputs.present == 'true'
run: node .github/collect-prerelease-info.js "${{ github.event.number }}" "${{ steps.prerelease.outputs.channel }}"
- name: Upload prerelease info
if: steps.changesets.outputs.present == 'true'
uses: actions/upload-artifact@v4
with:
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
path: packages/shadcn/dist/index.js
name: prerelease-info
path: prerelease-info.json
release:
if: ${{ github.event_name == 'push' && github.repository_owner == 'shadcn-ui' }}
@@ -98,11 +150,10 @@ jobs:
- name: Install NPM Dependencies
run: pnpm install
# - name: Check for errors
# run: pnpm check
- name: Build the package
run: pnpm shadcn:build
# Builds every publishable package under packages/* (shadcn, @shadcn/react),
# never apps/v4, so each dist is fresh before changeset publish.
- name: Build the packages
run: pnpm build:packages
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6

View File

@@ -46,3 +46,39 @@ jobs:
run: pnpm install
- run: pnpm test
react:
runs-on: ubuntu-latest
name: pnpm test (@shadcn/react)
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 22
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
version: 10.33.4
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- run: pnpm --filter=@shadcn/react test

7
.gitignore vendored
View File

@@ -45,4 +45,11 @@ tsconfig.tsbuildinfo
.playwright-mcp
.playwright-cli
shadcn-workspace
# vitest browser mode writes these only on test failure.
__screenshots__
.codex-artifacts
.tmp*
CONTEXT.md
docs/adr

View File

@@ -15,6 +15,7 @@
"search.exclude": {
"apps/v4/registry/radix-*": true,
"apps/v4/public/r/*": true,
"packages/shadcn/test/fixtures/*": true
"packages/shadcn/test/fixtures/*": true,
"apps/v4/styles/*": true
}
}

View File

@@ -141,6 +141,11 @@ When adding or modifying components, please ensure that:
2. You update the documentation.
3. You run `pnpm registry:build` to update the registry.
See [`apps/v4/registry/README.md`](apps/v4/registry/README.md) for how the
registry pipeline is structured and for the faster targeted build modes
(`--style`, `--registry`, `--examples`, `--indexes`) you can use while
iterating locally. Always run the full `pnpm registry:build` before committing.
## Commit Convention
Before you create a Pull Request, please check whether your commits comply with

59
RELEASING.md Normal file
View File

@@ -0,0 +1,59 @@
# Releasing
This monorepo publishes two packages independently with [Changesets](https://github.com/changesets/changesets):
- **`shadcn`** — the CLI and tooling.
- **`@shadcn/react`** — headless React primitives.
They version on their own lines. A change to one never bumps the other unless a changeset says so.
## 1. Add a changeset
Every change that should publish needs a changeset. Run:
```sh
pnpm changeset
```
Select the affected package(s) and bump level. One PR can carry separate changesets for `shadcn` and `@shadcn/react` at different levels. A PR with no changeset publishes nothing.
## 2. Stable release
Stable releases are automated by `.github/workflows/release.yml` (the `release` job, on push to `main`):
1. Merged changesets accumulate on `main`.
2. The Changesets action opens/updates a **"Version Packages"** PR that bumps versions and writes changelogs.
3. Merging that PR triggers `changeset publish`, which builds all packages (`pnpm build:packages`) and publishes any whose version is ahead of npm — each to the `latest` tag.
`pnpm build:packages` (`turbo run build --filter=./packages/*`) builds `shadcn` and `@shadcn/react` but never `apps/v4`.
## 3. Prereleases (per-PR snapshots)
Add the **`release: beta`** or **`release: rc`** label to a PR. The `prerelease` job in `release.yml`:
1. Verifies the branch has changesets (a label on the version PR is a no-op, since it consumed them).
2. Runs `changeset version --snapshot <channel>` — stamps a unique `0.0.0-<channel>-<timestamp>` on each changeset'd package.
3. Builds and runs `changeset publish --tag <channel> --no-git-tag`.
4. Uploads the published package list; `prerelease-comment.yml` posts a `pnpm dlx` install line per package and removes the label.
The label selects the **dist-tag/channel**; the **changesets on the branch** select which packages publish. Snapshots are timestamped, so they never touch `latest` and never collide.
```sh
# Install a snapshot from the PR comment, e.g.:
pnpm dlx @shadcn/react@0.0.0-beta-20260624120000
```
## 4. Prerelease trains (sustained `-beta.N` / `-rc.N`)
For a baking release line (e.g. `1.0.0-rc.0`, `-rc.1`, …) rather than throwaway snapshots, use Changesets pre mode:
```sh
pnpm changeset pre enter rc # writes .changeset/pre.json
# ...normal changeset + Version PR cycle now produces -rc.N versions on the rc tag...
pnpm changeset pre exit # back to stable; next Version PR ships X.Y.Z on latest
```
## Notes
- `pnpm-workspace.yaml` sets `minimumReleaseAge: 2880` (48h), so freshly published stable/beta versions take time to resolve in normal installs. Use `pnpm dlx <pkg>@<exact-snapshot-version>` to test immediately.
- Publishing uses npm OIDC/provenance (`id-token: write` + `npm@latest`); no `NPM_TOKEN` secret is needed.

1
apps/v4/.gitignore vendored
View File

@@ -46,3 +46,4 @@ next-env.d.ts
.contentlayer
.content-collections
.source
.devtools

View File

@@ -1,7 +1,7 @@
"use client"
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
import {
Picker,
PickerContent,
@@ -9,8 +9,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function MenuAccentPicker({
isMobile,

View File

@@ -12,7 +12,7 @@ import {
CommandItem,
CommandList,
} from "@/styles/base-nova/ui/command"
import { useActionMenu } from "@/app/(app)/create/hooks/use-action-menu"
import { useActionMenu } from "@/app/(app)/(create)/hooks/use-action-menu"
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { useMounted } from "@/hooks/use-mounted"
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
import {
Picker,
PickerContent,
@@ -12,8 +12,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function BaseColorPicker({
isMobile,

View File

@@ -10,8 +10,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function BasePicker({
isMobile,

View File

@@ -8,7 +8,7 @@ import {
getThemesForBaseColor,
type ChartColorName,
} from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
import {
Picker,
PickerContent,
@@ -17,8 +17,8 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function ChartColorPicker({
isMobile,
@@ -46,12 +46,6 @@ export function ChartColorPicker({
[params.chartColor]
)
React.useEffect(() => {
if (!currentChartColor && availableChartColors.length > 0) {
setParams({ chartColor: availableChartColors[0].name })
}
}, [currentChartColor, availableChartColors, setParams])
return (
<div className="group/picker relative">
<Picker>

View File

@@ -5,7 +5,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/styles/base-nova/ui/button"
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
import { usePresetCode } from "@/app/(app)/(create)/hooks/use-design-system"
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
const presetCode = usePresetCode()

View File

@@ -0,0 +1,72 @@
"use client"
import * as React from "react"
import { BASES } from "@/registry/config"
import { Button } from "@/registry/new-york-v4/ui/button"
import { getDocsPathForItem } from "@/app/(app)/(create)/lib/devtools"
import {
serializeDesignSystemSearchParams,
useDesignSystemSearchParams,
} from "@/app/(app)/(create)/lib/search-params"
export function CreateDevtools() {
const [params, setParams] = useDesignSystemSearchParams()
const previewUrl = React.useMemo(
() =>
serializeDesignSystemSearchParams(
`/preview/${params.base}/${params.item}`,
params
),
[params]
)
const docsUrl = React.useMemo(
() => getDocsPathForItem(params.base, params.item),
[params.base, params.item]
)
if (process.env.NODE_ENV === "production") {
return null
}
return (
<div className="dark absolute bottom-3 left-1/2 z-20 flex -translate-x-1/2 items-center gap-0.5 rounded-xl bg-card/90 p-1 shadow-xl backdrop-blur-xl">
{BASES.map((base) => (
<Button
key={base.name}
variant="ghost"
size="sm"
data-active={params.base === base.name}
className="h-7 min-w-8 cursor-pointer rounded-lg px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
onClick={() => setParams({ base: base.name })}
>
{base.name === "radix" ? "Radix" : "Base"}
</Button>
))}
<div className="mx-0.5 h-4 w-px bg-border/80" />
<Button
asChild
variant="ghost"
size="sm"
className="h-7 rounded-lg px-2.5 text-xs font-medium text-muted-foreground hover:text-foreground"
>
<a href={previewUrl} target="_blank" rel="noreferrer">
Open in New Tab
</a>
</Button>
<div className="mx-0.5 h-4 w-px bg-border/80" />
<Button
asChild
variant="ghost"
size="sm"
className="h-7 rounded-lg px-2.5 text-xs font-medium text-muted-foreground hover:text-foreground"
>
<a href={docsUrl} target="_blank" rel="noreferrer">
Open Docs
</a>
</Button>
</div>
)
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
export function CreateSkeleton() {
return (
<div className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden section-soft [--customizer-width:--spacing(48)] [--gap:--spacing(4)] md:[--gap:--spacing(6)] 2xl:[--customizer-width:--spacing(56)]">
<div
data-slot="designer"
className="flex min-h-0 flex-1 flex-col gap-(--gap) p-(--gap) pt-[calc(var(--gap)*0.25)] md:flex-row-reverse"
>
<Skeleton className="flex-1 rounded-2xl" />
<Skeleton className="min-h-[151px] w-full self-start rounded-2xl md:h-full md:max-h-full md:min-h-0 md:w-(--customizer-width)" />
</div>
</div>
)
}

View File

@@ -6,6 +6,7 @@ import { type RegistryItem } from "shadcn/schema"
import { useIsMobile } from "@/hooks/use-mobile"
import { getThemesForBaseColor, STYLES } from "@/registry/config"
import { Button } from "@/styles/base-nova/ui/button"
import {
Card,
CardContent,
@@ -13,30 +14,41 @@ import {
CardHeader,
} from "@/styles/base-nova/ui/card"
import { FieldGroup, FieldSeparator } from "@/styles/base-nova/ui/field"
import { MenuAccentPicker } from "@/app/(app)/create/components/accent-picker"
import { ActionMenu } from "@/app/(app)/create/components/action-menu"
import { BaseColorPicker } from "@/app/(app)/create/components/base-color-picker"
import { BasePicker } from "@/app/(app)/create/components/base-picker"
import { ChartColorPicker } from "@/app/(app)/create/components/chart-color-picker"
import { CopyPreset } from "@/app/(app)/create/components/copy-preset"
import { FontPicker } from "@/app/(app)/create/components/font-picker"
import { IconLibraryPicker } from "@/app/(app)/create/components/icon-library-picker"
import { MainMenu } from "@/app/(app)/create/components/main-menu"
import { MenuColorPicker } from "@/app/(app)/create/components/menu-picker"
import { OpenPreset } from "@/app/(app)/create/components/open-preset"
import { RadiusPicker } from "@/app/(app)/create/components/radius-picker"
import { RandomButton } from "@/app/(app)/create/components/random-button"
import { ResetDialog } from "@/app/(app)/create/components/reset-button"
import { StylePicker } from "@/app/(app)/create/components/style-picker"
import { ThemePicker } from "@/app/(app)/create/components/theme-picker"
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/create/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { MenuAccentPicker } from "@/app/(app)/(create)/components/accent-picker"
import { ActionMenu } from "@/app/(app)/(create)/components/action-menu"
import { BaseColorPicker } from "@/app/(app)/(create)/components/base-color-picker"
import { BasePicker } from "@/app/(app)/(create)/components/base-picker"
import { ChartColorPicker } from "@/app/(app)/(create)/components/chart-color-picker"
import { CopyPreset } from "@/app/(app)/(create)/components/copy-preset"
import { FontPicker } from "@/app/(app)/(create)/components/font-picker"
import { IconLibraryPicker } from "@/app/(app)/(create)/components/icon-library-picker"
import { MainMenu } from "@/app/(app)/(create)/components/main-menu"
import { MenuColorPicker } from "@/app/(app)/(create)/components/menu-picker"
import { OpenPreset } from "@/app/(app)/(create)/components/open-preset"
import { RadiusPicker } from "@/app/(app)/(create)/components/radius-picker"
import { RandomButton } from "@/app/(app)/(create)/components/random-button"
import { ResetDialog } from "@/app/(app)/(create)/components/reset-button"
import { StylePicker } from "@/app/(app)/(create)/components/style-picker"
import { ThemePicker } from "@/app/(app)/(create)/components/theme-picker"
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
// Only visible when user clicks "Create Project".
const ProjectForm = dynamic(() =>
import("@/app/(app)/create/components/project-form").then(
(m) => m.ProjectForm
)
// Only visible when user clicks "Create Project". Rendered client-only to
// avoid a useId hydration mismatch on the Base UI dialog trigger. The loading
// placeholder mirrors the trigger button exactly so there is no layout shift.
const ProjectForm = dynamic(
() =>
import("@/app/(app)/(create)/components/project-form").then(
(m) => m.ProjectForm
),
{
ssr: false,
loading: () => (
<Button disabled aria-hidden>
Get Code
</Button>
),
}
)
export function Customizer({

View File

@@ -8,12 +8,12 @@ import {
POINTER_CURSOR_SELECTOR,
type DesignSystemConfig,
} from "@/registry/config"
import { useIframeMessageListener } from "@/app/(app)/create/hooks/use-iframe-sync"
import { FONTS } from "@/app/(app)/create/lib/fonts"
import { useIframeMessageListener } from "@/app/(app)/(create)/hooks/use-iframe-sync"
import { FONTS } from "@/app/(app)/(create)/lib/fonts"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/lib/search-params"
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const

View File

@@ -2,7 +2,7 @@
import * as React from "react"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
import {
Picker,
PickerContent,
@@ -12,12 +12,12 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { FONTS } from "@/app/(app)/create/lib/fonts"
} from "@/app/(app)/(create)/components/picker"
import { FONTS } from "@/app/(app)/(create)/lib/fonts"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/lib/search-params"
type FontPickerOption = {
name: string

View File

@@ -5,7 +5,7 @@ import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { Button } from "@/styles/base-nova/ui/button"
import { useHistory } from "@/app/(app)/create/hooks/use-history"
import { useHistory } from "@/app/(app)/(create)/hooks/use-history"
export const UNDO_FORWARD_TYPE = "undo-forward"
export const REDO_FORWARD_TYPE = "redo-forward"

View File

@@ -3,7 +3,7 @@
import * as React from "react"
import { iconLibraries, type IconLibraryName } from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
import {
Picker,
PickerContent,
@@ -11,8 +11,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
const logos = {
lucide: (

View File

@@ -4,7 +4,7 @@ import { lazy, Suspense } from "react"
import { SquareIcon } from "lucide-react"
import type { IconLibraryName } from "shadcn/icons"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
const IconLucide = lazy(() =>
import("@/registry/icons/icon-lucide").then((mod) => ({

View File

@@ -21,8 +21,8 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/styles/base-nova/ui/sidebar"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(app)/(create)/lib/utils"
const cachedGroupedItems = React.cache(
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {

View File

@@ -10,7 +10,7 @@ import { cn } from "@/lib/utils"
import {
useLocks,
type LockableParam,
} from "@/app/(app)/create/hooks/use-locks"
} from "@/app/(app)/(create)/hooks/use-locks"
export function LockButton({
param,

View File

@@ -14,13 +14,13 @@ import {
PickerSeparator,
PickerShortcut,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useActionMenuTrigger } from "@/app/(app)/create/hooks/use-action-menu"
import { useHistory } from "@/app/(app)/create/hooks/use-history"
import { useOpenPresetTrigger } from "@/app/(app)/create/hooks/use-open-preset"
import { useRandom } from "@/app/(app)/create/hooks/use-random"
import { useReset } from "@/app/(app)/create/hooks/use-reset"
import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle"
} from "@/app/(app)/(create)/components/picker"
import { useActionMenuTrigger } from "@/app/(app)/(create)/hooks/use-action-menu"
import { useHistory } from "@/app/(app)/(create)/hooks/use-history"
import { useOpenPresetTrigger } from "@/app/(app)/(create)/hooks/use-open-preset"
import { useRandom } from "@/app/(app)/(create)/hooks/use-random"
import { useReset } from "@/app/(app)/(create)/hooks/use-reset"
import { useThemeToggle } from "@/app/(app)/(create)/hooks/use-theme-toggle"
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/

View File

@@ -7,7 +7,7 @@ import { useTheme } from "next-themes"
import { useMounted } from "@/hooks/use-mounted"
import { type MenuColorValue } from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
import {
Picker,
PickerContent,
@@ -17,11 +17,11 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
} from "@/app/(app)/(create)/components/picker"
import {
isTranslucentMenuColor,
useDesignSystemSearchParams,
} from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/lib/search-params"
type ColorChoice = "default" | "inverted"
type SurfaceChoice = "solid" | "translucent"

View File

@@ -5,7 +5,7 @@ import Script from "next/script"
import { cn } from "@/lib/utils"
import { Button } from "@/styles/base-nova/ui/button"
import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle"
import { useThemeToggle } from "@/app/(app)/(create)/hooks/use-theme-toggle"
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"

View File

@@ -31,9 +31,9 @@ import { Input } from "@/styles/base-nova/ui/input"
import {
OPEN_PRESET_FORWARD_TYPE,
useOpenPreset,
} from "@/app/(app)/create/hooks/use-open-preset"
import { parsePresetInput } from "@/app/(app)/create/lib/parse-preset-input"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/hooks/use-open-preset"
import { parsePresetInput } from "@/app/(app)/(create)/lib/parse-preset-input"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
const PRESET_EXAMPLE = "b2D0wqNxT"
const PRESET_TITLE = "Open Preset"
@@ -111,11 +111,12 @@ export function OpenPreset({
if (isMobile) {
return (
<Drawer open={open} onOpenChange={handleOpenChange}>
<DrawerTrigger asChild>
<Button variant="outline" className={triggerClassName}>
{label}
</Button>
<DrawerTrigger
render={<Button variant="outline" className={triggerClassName} />}
>
{label}
</DrawerTrigger>
<DrawerContent className="dark rounded-t-2xl!">
<DrawerHeader>
<DrawerTitle className="text-xl">{PRESET_TITLE}</DrawerTitle>
@@ -127,10 +128,12 @@ export function OpenPreset({
<Button type="submit" className="h-10" disabled={!nextPreset}>
Open
</Button>
<DrawerClose asChild>
<Button variant="outline" type="button" className="h-10">
Cancel
</Button>
<DrawerClose
render={
<Button variant="outline" type="button" className="h-10" />
}
>
Cancel
</DrawerClose>
</DrawerFooter>
</form>

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { cn } from "@/registry/bases/base/lib/utils"
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
import { IconPlaceholder } from "@/app/(app)/(create)/components/icon-placeholder"
function Picker({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { useRouter } from "next/navigation"
import { generateRandomPreset, isPresetCode } from "shadcn/preset"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function PresetHandler() {
const router = useRouter()

View File

@@ -10,8 +10,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function PresetPicker({
presets,

View File

@@ -1,7 +1,7 @@
"use client"
import { Button } from "@/registry/new-york-v4/ui/button"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
const PREVIEW_ITEMS = [
{ label: "01", value: "preview-02" },

View File

@@ -2,21 +2,22 @@
import * as React from "react"
import { CMD_K_FORWARD_TYPE } from "@/app/(app)/create/components/action-menu"
import { CMD_K_FORWARD_TYPE } from "@/app/(app)/(create)/components/action-menu"
import { CreateDevtools } from "@/app/(app)/(create)/components/create-devtools"
import {
REDO_FORWARD_TYPE,
UNDO_FORWARD_TYPE,
} from "@/app/(app)/create/components/history-buttons"
import { DARK_MODE_FORWARD_TYPE } from "@/app/(app)/create/components/mode-switcher"
import { PreviewSwitcher } from "@/app/(app)/create/components/preview-switcher"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(app)/create/components/random-button"
import { sendToIframe } from "@/app/(app)/create/hooks/use-iframe-sync"
import { OPEN_PRESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-open-preset"
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
} from "@/app/(app)/(create)/components/history-buttons"
import { DARK_MODE_FORWARD_TYPE } from "@/app/(app)/(create)/components/mode-switcher"
import { PreviewSwitcher } from "@/app/(app)/(create)/components/preview-switcher"
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(app)/(create)/components/random-button"
import { sendToIframe } from "@/app/(app)/(create)/hooks/use-iframe-sync"
import { OPEN_PRESET_FORWARD_TYPE } from "@/app/(app)/(create)/hooks/use-open-preset"
import { RESET_FORWARD_TYPE } from "@/app/(app)/(create)/hooks/use-reset"
import {
serializeDesignSystemSearchParams,
useDesignSystemSearchParams,
} from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/lib/search-params"
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
@@ -160,6 +161,7 @@ export function Preview() {
title="Preview"
/>
</div>
<CreateDevtools />
<PreviewSwitcher />
</div>
)

View File

@@ -50,17 +50,17 @@ import {
ToggleGroup,
ToggleGroupItem,
} from "@/styles/base-nova/ui/toggle-group"
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
import { usePresetCode } from "@/app/(app)/(create)/hooks/use-design-system"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/lib/search-params"
import {
getFramework,
getTemplateValue,
NO_MONOREPO_FRAMEWORKS,
TEMPLATES,
} from "@/app/(app)/create/lib/templates"
} from "@/app/(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>'
@@ -148,7 +148,8 @@ export function ProjectForm({
const commands = React.useMemo(() => {
const presetFlag = ` --preset ${presetCode}`
const baseFlag = params.base !== "radix" ? ` --base ${params.base}` : ""
const baseFlag =
params.base !== DEFAULT_CONFIG.base ? ` --base ${params.base}` : ""
const templateFlag = ` --template ${framework}`
const monorepoFlag = isMonorepo ? " --monorepo" : ""
const rtlFlag = params.rtl ? " --rtl" : ""

View File

@@ -3,7 +3,7 @@
import * as React from "react"
import { RADII, type RadiusValue } from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
import {
Picker,
PickerContent,
@@ -12,8 +12,8 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function RadiusPicker({
isMobile,

View File

@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { Button } from "@/styles/base-nova/ui/button"
import { useRandom } from "@/app/(app)/create/hooks/use-random"
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
import { useRandom } from "@/app/(app)/(create)/hooks/use-random"
import { RESET_FORWARD_TYPE } from "@/app/(app)/(create)/hooks/use-reset"
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"

View File

@@ -10,7 +10,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/styles/base-nova/ui/alert-dialog"
import { useReset } from "@/app/(app)/create/hooks/use-reset"
import { useReset } from "@/app/(app)/(create)/hooks/use-reset"
export function ResetDialog() {
const { showResetDialog, setShowResetDialog, confirmReset } = useReset()

View File

@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { Button } from "@/styles/base-nova/ui/button"
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { usePresetCode } from "@/app/(app)/(create)/hooks/use-design-system"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function ShareButton() {
const [params] = useDesignSystemSearchParams()

View File

@@ -3,7 +3,7 @@
import * as React from "react"
import { type Style, type StyleName } from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
import {
Picker,
PickerContent,
@@ -11,8 +11,8 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function StylePicker({
styles,

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { useMounted } from "@/hooks/use-mounted"
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
import {
Picker,
PickerContent,
@@ -13,8 +13,8 @@ import {
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(app)/create/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function ThemePicker({
themes,
@@ -38,12 +38,6 @@ export function ThemePicker({
[params.theme]
)
React.useEffect(() => {
if (!currentTheme && themes.length > 0) {
setParams({ theme: themes[0].name })
}
}, [currentTheme, themes, setParams])
return (
<div className="group/picker relative">
<Picker>

View File

@@ -8,7 +8,7 @@ import { useMounted } from "@/hooks/use-mounted"
import { Icons } from "@/components/icons"
import { Button } from "@/styles/base-nova/ui/button"
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function V0Button({ className }: { className?: string }) {
const [params] = useDesignSystemSearchParams()

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react"
import { CreateSkeleton } from "@/app/(app)/(create)/components/create-skeleton"
import { HistoryProvider } from "@/app/(app)/(create)/hooks/use-history"
import { LocksProvider } from "@/app/(app)/(create)/hooks/use-locks"
export default function CreateLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<LocksProvider>
{/* HistoryProvider reads useSearchParams(), which opts this subtree out
of prerendering up to this boundary. The fallback is what paints on
first load, so it must be the page placeholder, not empty. */}
<Suspense fallback={<CreateSkeleton />}>
<HistoryProvider>{children}</HistoryProvider>
</Suspense>
</LocksProvider>
)
}

View File

@@ -0,0 +1,5 @@
import { CreateSkeleton } from "@/app/(app)/(create)/components/create-skeleton"
export default function CreateLoading() {
return <CreateSkeleton />
}

View File

@@ -5,14 +5,14 @@ import dynamic from "next/dynamic"
import { siteConfig } from "@/lib/config"
import { absoluteUrl } from "@/lib/utils"
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
import { Customizer } from "@/app/(app)/create/components/customizer"
import { PresetHandler } from "@/app/(app)/create/components/preset-handler"
import { Preview } from "@/app/(app)/create/components/preview"
import { getAllItems } from "@/app/(app)/create/lib/api"
import { Customizer } from "@/app/(app)/(create)/components/customizer"
import { PresetHandler } from "@/app/(app)/(create)/components/preset-handler"
import { Preview } from "@/app/(app)/(create)/components/preview"
import { getAllItems } from "@/app/(app)/(create)/lib/api"
// Only shown on first visit (checks localStorage).
const WelcomeDialog = dynamic(() =>
import("@/app/(app)/create/components/welcome-dialog").then(
import("@/app/(app)/(create)/components/welcome-dialog").then(
(m) => m.WelcomeDialog
)
)

View File

@@ -4,8 +4,8 @@ import * as React from "react"
import { type RegistryItem } from "shadcn/schema"
import useSWR from "swr"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(app)/(create)/lib/utils"
const ACTION_MENU_OPEN_KEY = "create:action-menu-open"

View File

@@ -1,7 +1,7 @@
"use client"
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
// Returns the canonical preset code derived from the current search params.
export function usePresetCode() {

View File

@@ -2,7 +2,12 @@
import * as React from "react"
import { Suspense } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useSearchParams } from "next/navigation"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(app)/(create)/lib/search-params"
type HistoryContextValue = {
canGoBack: boolean
@@ -15,6 +20,9 @@ const HistoryContext = React.createContext<HistoryContextValue | null>(null)
// Reads useSearchParams() in its own Suspense boundary so the
// provider never blanks out children while search params resolve.
// useSearchParams reflects the *settled* preset, which coalesces the
// transient values nuqs emits mid-update — reading nuqs state directly
// here would record those transients as phantom history entries.
function PresetSync({
onPresetChange,
}: {
@@ -31,7 +39,11 @@ function PresetSync({
}
export function HistoryProvider({ children }: { children: React.ReactNode }) {
const router = useRouter()
// Write through the shared search-params hook (shallow) instead of
// router.replace. router.replace triggers a full server navigation that
// resets the preset on prod — the same failure the customizer avoids by
// going shallow (#11060).
const [, setParams] = useDesignSystemSearchParams()
const [preset, setPreset] = React.useState("")
@@ -81,17 +93,16 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
indexRef.current = nextIndex
setIndex(nextIndex)
const targetPreset = entriesRef.current[nextIndex]
const params = new URLSearchParams(window.location.search)
if (targetPreset) {
params.set("preset", targetPreset)
} else {
params.delete("preset")
}
const pathname = window.location.pathname
const query = params.toString()
router.replace(query ? `${pathname}?${query}` : pathname)
}, [router])
// The first history entry is "" (no preset in the URL); null clears the
// param to restore that state. nuqs accepts null to clear a key, which the
// wrapper's input type does not model.
setParams(
{
preset: entriesRef.current[nextIndex] || null,
} as Partial<DesignSystemSearchParams>,
{ history: "replace" }
)
}, [setParams])
const goForward = React.useCallback(() => {
if (indexRef.current >= maxIndexRef.current) {
@@ -103,17 +114,16 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
indexRef.current = nextIndex
setIndex(nextIndex)
const targetPreset = entriesRef.current[nextIndex]
const params = new URLSearchParams(window.location.search)
if (targetPreset) {
params.set("preset", targetPreset)
} else {
params.delete("preset")
}
const pathname = window.location.pathname
const query = params.toString()
router.replace(query ? `${pathname}?${query}` : pathname)
}, [router])
// The first history entry is "" (no preset in the URL); null clears the
// param to restore that state. nuqs accepts null to clear a key, which the
// wrapper's input type does not model.
setParams(
{
preset: entriesRef.current[nextIndex] || null,
} as Partial<DesignSystemSearchParams>,
{ history: "replace" }
)
}, [setParams])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {

View File

@@ -2,7 +2,7 @@
import * as React from "react"
import type { DesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import type { DesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
type ParentToIframeMessage = {
type: "design-system-params"

View File

@@ -1,6 +1,7 @@
"use client"
import * as React from "react"
import { decodePreset } from "shadcn/preset"
import {
BASE_COLORS,
@@ -10,19 +11,30 @@ import {
MENU_COLORS,
RADII,
STYLES,
type BaseColorName,
type ChartColorName,
type FontHeadingValue,
type FontValue,
type IconLibraryName,
type MenuAccentValue,
type MenuColorValue,
type RadiusValue,
type StyleName,
type ThemeName,
} from "@/registry/config"
import { useLocks } from "@/app/(app)/create/hooks/use-locks"
import { FONTS } from "@/app/(app)/create/lib/fonts"
import { useLocks } from "@/app/(app)/(create)/hooks/use-locks"
import { FONTS } from "@/app/(app)/(create)/lib/fonts"
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
import {
applyBias,
RANDOMIZE_BIASES,
type RandomizeContext,
} from "@/app/(app)/create/lib/randomize-biases"
} from "@/app/(app)/(create)/lib/randomize-biases"
import {
isTranslucentMenuColor,
useDesignSystemSearchParams,
} from "@/app/(app)/create/lib/search-params"
} from "@/app/(app)/(create)/lib/search-params"
import { SHUFFLE_PRESETS } from "@/app/(app)/(create)/lib/shuffle-presets"
function randomItem<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)]
@@ -37,7 +49,9 @@ export function useRandom() {
paramsRef.current = params
}, [params])
const randomize = React.useCallback(() => {
// True-random implementation. Kept, but shuffle is now wired to the
// curated preset list below.
const randomizeTrueRandom = React.useCallback(() => {
const selectedStyle = locks.has("style")
? paramsRef.current.style
: randomItem(STYLES).name
@@ -149,6 +163,60 @@ export function useRandom() {
setParams(nextParams)
}, [setParams, locks])
// Picks a random preset from the curated list instead of true random.
// Locked params are preserved over the decoded preset values.
const randomize = React.useCallback(() => {
// Avoid re-picking the preset that is currently applied.
const currentCode = getPresetCode(paramsRef.current)
const availableCodes = SHUFFLE_PRESETS.filter(
(code) => code !== currentCode
)
const decoded = decodePreset(
randomItem(availableCodes.length > 0 ? availableCodes : SHUFFLE_PRESETS)
)
if (!decoded) {
return
}
const current = paramsRef.current
const nextParams = {
style: locks.has("style") ? current.style : (decoded.style as StyleName),
baseColor: locks.has("baseColor")
? current.baseColor
: (decoded.baseColor as BaseColorName),
theme: locks.has("theme") ? current.theme : (decoded.theme as ThemeName),
chartColor: locks.has("chartColor")
? current.chartColor
: ((decoded.chartColor ?? decoded.theme) as ChartColorName),
iconLibrary: locks.has("iconLibrary")
? current.iconLibrary
: (decoded.iconLibrary as IconLibraryName),
font: locks.has("font") ? current.font : (decoded.font as FontValue),
fontHeading: locks.has("fontHeading")
? current.fontHeading
: (decoded.fontHeading as FontHeadingValue),
menuAccent: locks.has("menuAccent")
? current.menuAccent
: (decoded.menuAccent as MenuAccentValue),
menuColor: locks.has("menuColor")
? current.menuColor
: (decoded.menuColor as MenuColorValue),
radius: locks.has("radius")
? current.radius
: (decoded.radius as RadiusValue),
}
// Keep the ref in sync so rapid repeats use the latest randomized state
// even before the URL state finishes committing.
paramsRef.current = {
...paramsRef.current,
...nextParams,
}
setParams(nextParams)
}, [setParams, locks])
const randomizeRef = React.useRef(randomize)
React.useEffect(() => {
randomizeRef.current = randomize
@@ -177,5 +245,5 @@ export function useRandom() {
}
}, [])
return { randomize }
return { randomize, randomizeTrueRandom }
}

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import useSWR from "swr"
import { DEFAULT_CONFIG, PRESETS } from "@/registry/config"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
const RESET_DIALOG_KEY = "create:reset-dialog-open"
export const RESET_FORWARD_TYPE = "reset-forward"

View File

@@ -1,9 +1,8 @@
import { type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
import { buildInstructions } from "./build-instructions"
import { buildInstructions } from "@/app/(app)/(create)/lib/build-instructions"
import { parseDesignSystemConfig } from "@/app/(app)/(create)/lib/parse-config"
export async function GET(request: NextRequest) {
try {

View File

@@ -8,8 +8,8 @@ import {
buildRegistryBase,
parseRegistryBaseParts,
} from "@/registry/config"
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
import { parseDesignSystemConfig } from "@/app/(app)/(create)/lib/parse-config"
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
export async function GET(request: NextRequest) {
try {

View File

@@ -2,9 +2,9 @@ import { after, NextResponse, type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import { isPresetCode } from "shadcn/preset"
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
import { buildV0Payload } from "@/app/(app)/create/lib/v0"
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
import { parseDesignSystemConfig } from "@/app/(app)/(create)/lib/parse-config"
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
import { buildV0Payload } from "@/app/(app)/(create)/lib/v0"
export async function GET(request: NextRequest) {
try {

View File

@@ -6,7 +6,7 @@ import { BASES, getThemesForBaseColor, type BaseName } from "@/registry/config"
import {
ALLOWED_ITEM_TYPES,
EXCLUDED_ITEMS,
} from "@/app/(app)/create/lib/constants"
} from "@/app/(app)/(create)/lib/constants"
export async function getItemsForBase(base: BaseName) {
const { Index } = await import("@/registry/bases/__index__")
@@ -42,7 +42,8 @@ export async function getBaseComponent(name: string, base: BaseName) {
return null
}
return index[name].component
const { Components } = await import("@/registry/bases/__components__")
return Components[base]?.[name] ?? null
}
export async function getAllItems() {

View File

@@ -0,0 +1,10 @@
import { type BaseName } from "@/registry/config"
export function getDocsPathForItem(base: BaseName, item: string) {
if (item.endsWith("-example")) {
const component = item.slice(0, -"-example".length)
return `/docs/components/${base}/${component}`
}
return "/docs/components"
}

View File

@@ -4,7 +4,7 @@ import {
designSystemConfigSchema,
type DesignSystemConfig,
} from "@/registry/config"
import { resolvePresetOverrides } from "@/app/(app)/create/lib/preset-query"
import { resolvePresetOverrides } from "@/app/(app)/(create)/lib/preset-query"
// Parses design system config from URL search params.
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
@@ -20,7 +20,7 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
configInput = {
...decoded,
...presetOverrides,
base: searchParams.get("base") ?? "radix",
base: searchParams.get("base") ?? "base",
template: searchParams.get("template") ?? undefined,
rtl: searchParams.get("rtl") === "true",
pointer: searchParams.get("pointer") === "true",

View File

@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest"
import { DEFAULT_CONFIG } from "@/registry/config"
import { buildPresetUrlUpdate } from "./search-params"
describe("buildPresetUrlUpdate", () => {
it("encodes design system params into preset and clears individual keys", () => {
const update = buildPresetUrlUpdate({
...DEFAULT_CONFIG,
preset: "b0",
template: "next",
item: "preview-02",
size: 100,
custom: false,
})
expect(update.preset).toBeTypeOf("string")
expect(update.style).toBeNull()
expect(update.theme).toBeNull()
expect(update.baseColor).toBeNull()
})
it("preserves template when syncing preset from ?template=start", () => {
const update = buildPresetUrlUpdate({
...DEFAULT_CONFIG,
preset: "b0",
template: "start",
item: "preview-02",
size: 100,
custom: false,
})
expect(update.template).toBe("start")
expect(update.preset).toBeTypeOf("string")
})
it("applies non-design-system updates from resolvedUpdates", () => {
const update = buildPresetUrlUpdate(
{
...DEFAULT_CONFIG,
preset: "b0",
template: "next",
item: "preview-02",
size: 100,
custom: false,
},
{ template: "vite" }
)
expect(update.template).toBe("vite")
})
})

View File

@@ -36,9 +36,9 @@ import {
type StyleName,
type ThemeName,
} from "@/registry/config"
import { FONTS } from "@/app/(app)/create/lib/fonts"
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
import { resolvePresetOverrides } from "@/app/(app)/create/lib/preset-query"
import { FONTS } from "@/app/(app)/(create)/lib/fonts"
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
import { resolvePresetOverrides } from "@/app/(app)/(create)/lib/preset-query"
const designSystemSearchParams = {
preset: parseAsString.withDefault("b0"),
@@ -132,6 +132,9 @@ const NON_DESIGN_SYSTEM_KEYS = [
"custom",
] as const
// Shared across hook instances so only one mount effect encodes the initial URL.
let hasSyncedPresetToUrl = false
export const loadDesignSystemSearchParams = createLoader(
designSystemSearchParams
)
@@ -235,13 +238,42 @@ function resolvePresetParams(
return normalizeDesignSystemParams(rawParams)
}
export function buildPresetUrlUpdate(
merged: DesignSystemSearchParams,
resolvedUpdates: Partial<DesignSystemSearchParams> = {}
) {
const code = getPresetCode(merged)
const rawUpdate: Record<string, unknown> = { preset: code }
for (const key of DESIGN_SYSTEM_KEYS) {
rawUpdate[key] = null
}
for (const key of NON_DESIGN_SYSTEM_KEYS) {
if (key === "preset") {
continue
}
rawUpdate[key] =
key in resolvedUpdates
? (resolvedUpdates as Record<string, unknown>)[key]
: merged[key]
}
return rawUpdate
}
// Wraps nuqs useQueryStates with transparent preset encoding/decoding.
// - Reads: if ?preset=CODE is in the URL, decodes it and returns individual values.
// - Writes: when design system params are set, encodes them into a preset code.
//
// Default options use shallow: true so picker selections do not trigger a full
// Next.js server navigation. This prevents the customizer panel from flickering
// or resetting while the URL update propagates (#10910, #11060).
export function useDesignSystemSearchParams(options: Options = {}) {
const searchParams = useSearchParams()
const [rawParams, rawSetParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
shallow: true,
history: "push",
...options,
})
@@ -257,6 +289,19 @@ export function useDesignSystemSearchParams(options: Options = {}) {
paramsRef.current = params
}, [params])
React.useEffect(() => {
if (hasSyncedPresetToUrl || searchParams.has("preset")) {
return
}
hasSyncedPresetToUrl = true
const merged = normalizeDesignSystemParams(paramsRef.current)
void rawSetParams(buildPresetUrlUpdate(merged) as RawSetParamsInput, {
history: "replace",
})
}, [rawSetParams, searchParams])
type RawSetParamsInput = Parameters<typeof rawSetParams>[0]
const setParams = React.useCallback(
@@ -286,24 +331,10 @@ export function useDesignSystemSearchParams(options: Options = {}) {
...paramsRef.current,
...resolvedUpdates,
})
// Encode design system fields into a preset code.
// Cast needed: merged values may include null from nuqs resets,
// but encodePreset handles missing values by falling back to defaults.
const code = getPresetCode(merged)
// Build update: set preset, clear individual DS params from URL.
const rawUpdate: Record<string, unknown> = { preset: code }
for (const key of DESIGN_SYSTEM_KEYS) {
rawUpdate[key] = null
}
// Pass through non-DS params that were explicitly in the update.
for (const key of NON_DESIGN_SYSTEM_KEYS) {
if (key in resolvedUpdates) {
rawUpdate[key] = (resolvedUpdates as Record<string, unknown>)[key]
}
}
return rawSetParams(rawUpdate as RawSetParamsInput, setOptions)
return rawSetParams(
buildPresetUrlUpdate(merged, resolvedUpdates) as RawSetParamsInput,
setOptions
)
},
[rawSetParams]
)

View File

@@ -0,0 +1,28 @@
// Curated preset codes from https://dialectcn.xyz.
export const SHUFFLE_PRESETS = [
"b6sUj34d9",
"b2tqYzpa88",
"b1W4tDrk",
"b1aIuQ2XC",
"b7jsW1RxJ5",
"b870VEw0in",
"b3Zheoix4U",
"b1x9M2c4aI",
"b1W7jDEW",
"b51GFh7y6",
"b2fms620zo",
"b1Q5GC",
"buKEvLs",
"b5rR41Mtnc",
"b6tOz2I0x",
"b2hNTREGRN",
"bdIJ7Sq",
"b6TqMNb5Wb",
"bJIirQ",
"b4aRK5K0fb",
"b5HCiD38LI",
"bdHjvCi",
"b7QDHijUjj",
"b4ZVZIPi9h",
"b1W4bcno",
]

View File

@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { DEFAULT_CONFIG } from "@/registry/config"
import { buildV0Payload } from "@/app/(app)/create/lib/v0"
import { buildV0Payload } from "@/app/(app)/(create)/lib/v0"
vi.mock("shadcn/schema", async () => {
return await vi.importActual("shadcn/schema")

View File

@@ -17,7 +17,6 @@ const chartData = [
{ month: "Feb", amount: 900 },
{ month: "Mar", amount: 1300 },
{ month: "Apr", amount: 750 },
{ month: "May", amount: 1400 },
]
export function ContributionHistory() {
@@ -35,13 +34,14 @@ export function ContributionHistory() {
role="img"
aria-label="Last 6 months of contribution activity"
>
{chartData.map((item) => (
{chartData.map((item, index) => (
<div
key={item.month}
className="flex h-full flex-1 flex-col justify-end gap-2"
>
<div
className="min-h-2 rounded-t-md bg-chart-2"
data-index={index}
className="data-[index=5]:bg-chart-6 min-h-2 rounded-lg data-[index=0]:bg-chart-1 data-[index=1]:bg-chart-2 data-[index=2]:bg-chart-3 data-[index=3]:bg-chart-4 data-[index=4]:bg-chart-5"
style={{ height: `${(item.amount / maxAmount) * 100}%` }}
/>
<span className="text-center text-xs text-muted-foreground">

View File

@@ -1,3 +1,5 @@
import { MessageScrollerDemo } from "@/examples/radix/message-scroller-demo"
import { AccountAccess } from "./account-access"
import { AnalyticsCard } from "./analytics-card"
import { ClaimableBalance } from "./claimable-balance"
@@ -93,17 +95,20 @@ export function CardsDemo() {
<ClaimableBalance />
<DividendIncome />
</div>
<div className="hidden flex-col gap-(--gap) 3xl:flex!">
<div className="hidden flex-col gap-(--gap) min-[1400px]:flex">
<NewMilestone />
<PayoutThreshold />
<AccountAccess />
</div>
<div className="hidden flex-col gap-(--gap) md:flex">
<QrConnect />
<TransferFunds />
<div className="**:[.text-center.text-xs]:hidden">
<MessageScrollerDemo />
</div>
{/* <TransferFunds /> */}
<Payments />
</div>
<div className="hidden flex-col gap-(--gap) min-[1400px]:flex">
<div className="hidden flex-col gap-(--gap) min-[1900px]:flex">
<EmptyDistributeTrack />
<AnalyticsCard />
<NotificationSettings />

View File

@@ -117,7 +117,7 @@ export function UIElements() {
</span>
<span className="flex md:hidden style-sera:md:flex">Dialog</span>
</AlertDialogTrigger>
<AlertDialogContent size="sm">
<AlertDialogContent size="sm" className="theme-neutral">
<AlertDialogHeader>
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
<AlertDialogDescription>

View File

@@ -60,23 +60,23 @@ export default function IndexPage() {
</Button>
</PageActions>
</PageHeader>
<div className="container-wrapper flex-1 pb-6 md:px-0">
<div className="container-wrapper flex-1 p-0">
<div className="container overflow-hidden md:px-0 lg:max-w-none">
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
<section className="-mx-4 w-[140vw] overflow-hidden md:hidden">
<Image
src="/r/styles/new-york-v4/dashboard-01-light.png"
width={1400}
height={875}
src="/images/full-light.png"
width={2560}
height={2764}
alt="Dashboard"
className="block dark:hidden"
className="block h-auto w-full dark:hidden"
priority
/>
<Image
src="/r/styles/new-york-v4/dashboard-01-dark.png"
width={1400}
height={875}
src="/images/full-dark.png"
width={2560}
height={2764}
alt="Dashboard"
className="hidden dark:block"
className="hidden h-auto w-full dark:block"
priority
/>
</section>

View File

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

View File

@@ -185,7 +185,7 @@ export default async function Page(props: {
<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">
<div className="flex scroll-fade scrollbar-none flex-col gap-8 overflow-y-auto px-8">
<DocsTableOfContents toc={doc.toc} />
</div>
) : null}

View File

@@ -3,7 +3,7 @@ import { NextResponse, type NextRequest } from "next/server"
import { processMdxForLLMs } from "@/lib/llm"
import { source } from "@/lib/source"
import { getActiveStyle, type Style } from "@/registry/_legacy-styles"
import { type Style } from "@/registry/_legacy-styles"
export const revalidate = false
export const dynamic = "force-static"
@@ -26,7 +26,7 @@ export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const [{ slug }, activeStyle] = await Promise.all([params, getActiveStyle()])
const { slug } = await params
const page = source.getPage(slug)
@@ -34,7 +34,8 @@ export async function GET(
notFound()
}
const effectiveStyle = getStyleFromSlug(slug, activeStyle.name)
// Default to the base style. Legacy content pins new-york-v4 per tag.
const effectiveStyle = getStyleFromSlug(slug, "base-nova")
const processedContent = processMdxForLLMs(
await page.data.getText("raw"),

View File

@@ -1,3 +0,0 @@
"use client"
export { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"

View File

@@ -1 +0,0 @@
export * from "@/app/(app)/create/lib/fonts"

View File

@@ -1 +0,0 @@
export * from "@/app/(app)/create/lib/search-params"

View File

@@ -0,0 +1,74 @@
import { type Metadata } from "next"
import { notFound } from "next/navigation"
import { ExamplesComponents } from "@/examples/__components__"
import { ExamplesIndex } from "@/examples/__index__"
import { siteConfig } from "@/lib/config"
import { absoluteUrl } from "@/lib/utils"
export const dynamicParams = true
export const revalidate = 3600
function getExample(base: string, name: string) {
const item = ExamplesIndex[base]?.[name]
const Component = ExamplesComponents[base]?.[name]
if (!item || !Component) {
return null
}
return { item, Component }
}
export async function generateMetadata({
params,
}: {
params: Promise<{ base: string; name: string }>
}): Promise<Metadata> {
const { base, name } = await params
const example = getExample(base, name)
if (!example) {
return {}
}
const title = example.item.name
return {
title,
openGraph: {
title,
type: "article",
url: absoluteUrl(`/examples/${base}/${title}`),
images: [
{
url: siteConfig.ogImage,
width: 1200,
height: 630,
alt: siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
title,
images: [siteConfig.ogImage],
creator: "@shadcn",
},
}
}
export default async function ExamplePage({
params,
}: {
params: Promise<{ base: string; name: string }>
}) {
const { base, name } = await params
const example = getExample(base, name)
if (!example) {
return notFound()
}
const { Component } = example
return <Component />
}

View File

@@ -6,16 +6,17 @@ import { siteConfig } from "@/lib/config"
import { absoluteUrl } from "@/lib/utils"
import { TailwindIndicator } from "@/components/tailwind-indicator"
import { BASES, type Base } from "@/registry/config"
import { ActionMenuScript } from "@/app/(app)/create/components/action-menu"
import { DesignSystemProvider } from "@/app/(app)/create/components/design-system-provider"
import { HistoryScript } from "@/app/(app)/create/components/history-buttons"
import { DarkModeScript } from "@/app/(app)/create/components/mode-switcher"
import { OpenPresetScript } from "@/app/(app)/create/components/open-preset"
import { PreviewStyle } from "@/app/(app)/create/components/preview-style"
import { RandomizeScript } from "@/app/(app)/create/components/random-button"
import { getBaseComponent, getBaseItem } from "@/app/(app)/create/lib/api"
import { ActionMenuScript } from "@/app/(app)/(create)/components/action-menu"
import { DesignSystemProvider } from "@/app/(app)/(create)/components/design-system-provider"
import { HistoryScript } from "@/app/(app)/(create)/components/history-buttons"
import { DarkModeScript } from "@/app/(app)/(create)/components/mode-switcher"
import { OpenPresetScript } from "@/app/(app)/(create)/components/open-preset"
import { PreviewStyle } from "@/app/(app)/(create)/components/preview-style"
import { RandomizeScript } from "@/app/(app)/(create)/components/random-button"
import { getBaseComponent, getBaseItem } from "@/app/(app)/(create)/lib/api"
import "@/app/style-registry.css"
import "streamdown/styles.css"
export const revalidate = false
export const dynamic = "force-static"

View File

@@ -1,5 +1,5 @@
import { PreviewFontVariables } from "@/app/(create)/preview/font-variables"
import { previewFontVariables } from "@/app/(create)/preview/fonts"
import { PreviewFontVariables } from "@/app/(view)/preview/font-variables"
import { previewFontVariables } from "@/app/(view)/preview/fonts"
export default function PreviewLayout({
children,

View File

@@ -3,6 +3,8 @@
@import "shadcn/tailwind.css";
@import "./legacy-themes.css";
@source "../node_modules/streamdown/dist/*.js";
@custom-variant style-vega (&:where(.style-vega *));
@custom-variant style-nova (&:where(.style-nova *));
@custom-variant style-lyra (&:where(.style-lyra *));
@@ -168,6 +170,7 @@
@apply overscroll-y-none;
}
body {
position: relative;
font-synthesis-weight: none;
text-rendering: optimizeLegibility;
}
@@ -284,6 +287,14 @@
}
}
[data-rehype-pretty-code-figure] code,
[data-rehype-pretty-code-figure] code span {
font-variant-ligatures: none;
font-feature-settings:
"liga" 0,
"calt" 0;
}
[data-rehype-pretty-code-title] {
border-bottom: color-mix(in oklab, var(--border) 30%, transparent);
border-bottom-width: 1px;

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