Compare commits

...

466 Commits

Author SHA1 Message Date
shadcn
dcc95f4983 chore(release): version packages 2026-07-07 12:00:12 +00: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
github-actions[bot]
2baa86081d chore(release): version packages (#10791)
Co-authored-by: shadcn <m@shadcn.com>
2026-05-29 12:10:45 +04:00
shadcn
980f288149 ci(templates): test pnpm 11 (#10790) 2026-05-29 11:15:40 +04:00
Raashish Aggarwal
07900769d9 fix(cli): update template handling for pnpm 11 (#10659)
* fix(cli): allow esbuild builds in Vite templates

* fix(cli): extend pnpm 11 build-script allowlists across app templates

- Add packages: [] to single-app pnpm-workspace.yaml so pnpm 9 does
  not reject the file with "packages field missing or empty".
- Add astro-app, react-router-app, start-app, next-app workspace
  yamls with the build-script allowlist each template needs
  (esbuild, sharp, unrs-resolver as applicable).
- Set msw: false across all app allowlists so the registry component
  install runs cleanly under pnpm 11 without executing msw's
  service-worker postinstall.
- Add a scaffold test pinning the packages:[] + allowBuilds shape
  so the parser keeps treating it as single-app.

* chore: changeset

* fix(templates): allow monorepo pnpm builds

* ci(templates): validate app workspace conversion

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-05-29 08:24:31 +04:00
Artyom Konoplyov
360e8a19c3 fix(transform-rtl): preserve quotes in transformed className literals (#10495)
* fix(transform-rtl): preserve string literal escapes

* chore(changeset): add rtl quote preservation note

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-05-27 22:29:44 +04:00
github-actions[bot]
e2fa0101e3 chore(release): version packages (#10789)
Co-authored-by: shadcn <m@shadcn.com>
2026-05-27 21:09:59 +04:00
shadcn
55ea86f252 chore: update templates (#10786)
* chore: update templates

* fix(cli): parse pnpm workspace packages

* chore(changeset): add shadcn patch

* chore(changeset): update description

* ci(templates): validate bun and npx init

* ci(templates): expand package manager validation

* ci(templates): parallelize validation

* ci(templates): allow yarn template lockfiles

* fix(cli): allow yarn template installs in ci
2026-05-27 21:08:26 +04:00
shadcn
f584f05489 fix: padding 2026-05-27 19:43:38 +04:00
shadcn
a06ba18dcc Revert "chore: update templates (#10784)" (#10785)
This reverts commit f3e16e7db7.
2026-05-27 19:06:53 +04:00
shadcn
f3e16e7db7 chore: update templates (#10784)
* chore: update templates

* ci(templates): validate generated starters

* fix

* fix(templates): support pnpm 9 workspace config

* ci(templates): test supported pnpm version
2026-05-27 19:04:17 +04:00
shadcn
64afddefd9 chore(v4): update base-ui to 1.5.0 (#10783) 2026-05-27 15:27:43 +04:00
shadcn
c873713992 fix(v4): serve registries from directory (#10781)
* fix(v4): serve registries from directory

* fix(v4): statically cache registries route

* fix
2026-05-27 09:16:59 +04:00
shadcn
3751fdfa4c fix(create): update lock state during render (#10782) 2026-05-26 23:56:45 +04:00
github-actions[bot]
c824d6b78d chore(release): version packages (#10780)
Co-authored-by: shadcn <m@shadcn.com>
2026-05-26 22:58:26 +04:00
shadcn
df1752dfe0 feat: rhea (#10779)
* feat: add rhea

* fix: blocks

* feat: build chat example

* fix

* fix: sidebar

* fix

* feat: update home

* fix

* fix

* fix

* feat: optimizine fonts

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix: font in preview

* fix
2026-05-26 22:54:07 +04:00
Dominik K.
e826e543f2 fix(registry): restore missing @blockus object in registry JSON (#10778)
The dominik-ui registry entry accidentally dropped the opening brace for @blockus, leaving invalid JSON in both registry files.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:11:01 +04:00
Dominik K.
f7eecafb45 chore(registry): add dominik-ui (#10776)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 21:44:26 +04:00
LN
6e6cf9ee96 Feat/add registry directory blockus (#10711)
* feat: add new registry entry for blockus with logo and description

* chore: rebuild registries.json with blockus entry
2026-05-26 21:18:51 +04:00
Antunes
5b628e23e3 feat(registry): add @toc-cn directory entry (#10759) 2026-05-25 21:38:57 +04:00
shadcn
4a4dc8eb0f Update pnpm release age settings (#10719)
* chore: update pnpm release age settings

* fix: declare pnpm 10 tool dependencies

* fix: align template pnpm versions
2026-05-22 20:10:00 +04:00
Shuta Kumano
a33becad35 feat(registry): add @gymnopedies registry (#10728)
* feat(registry): add @gymnopedies to directory

* chore(registry): rebuild registries.json
2026-05-22 16:25:16 +04:00
KapishDima
d60e8b6ce3 fix: wrap DirectoryList with React.Suspense (#10727) 2026-05-22 15:56:12 +04:00
github-actions[bot]
072c27fcd5 chore(release): version packages (#10568)
Co-authored-by: shadcn <m@shadcn.com>
2026-05-21 17:57:06 +04:00
shadcn
194dcc4571 docs: update changelog 2026-05-21 17:48:36 +04:00
shadcn
51e3cfaf32 feat(registry): add validate command (#10715)
* feat: implement registry include

* feat: updates

* fix

* refactor: implementation

* fix(registry): correct directory registry json

* fix(registry): stop warning for external registry dependencies

* feat(registry): add validate command
2026-05-21 17:32:34 +04:00
shadcn
c8ab3801ec feat: add include to registry.json (#10708)
* feat: implement registry include

* feat: updates

* fix

* refactor: implementation

* fix(registry): correct directory registry json

* fix(registry): stop warning for external registry dependencies

* fix(registry): address include review feedback
2026-05-21 17:13:04 +04:00
shadcn
731e6dd8a2 fix 2026-05-20 17:27:42 +04:00
Tushar Jolly
d7066f4a2d feat: add @next-ui registry (#10693)
* feat: add @next-ui registry

## New Registry: @next-ui

- **Homepage:** https://nexus-ui.com
- **Registry URL:** https://nexus-ui.com/r/{name}.json
- **Registry index:** https://nexus-ui.com/registry.json
- **Components:** 57 motion-native components for Next.js

### Checklist
- [x] registry.json live at https://nexus-ui.com/registry.json
- [x] Component files live at https://nexus-ui.com/r/{name}.json
- [x] Open source
- [x] Works with shadcn CLI

* Add Nexus Labs registry entry to registries.json
2026-05-20 16:49:50 +04:00
Matt
5274de83d6 feat(registry): add @bklit registry (#10707)
Add Bklit UI to the open source registry index (directory.json and
registries.json) for shadcn add and shadcn search.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 16:44:20 +04:00
Fabian Hiller
7e4dac7f31 feat: add Formisch (form library) examples to docs (#10342)
* feat: add Formisch (form library) examples to docs

* docs: update Formisch docs with additional form methods and examples

* chore: update valibot dependency to version 1.4.0

* style(docs): format formisch imports

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-05-20 15:18:41 +04:00
Charlie Hopkins-Brinicombe
28122dba18 feat: add @trophy-ui registry (#10700) 2026-05-20 09:44:13 +04:00
Parth Sharma
93cde61946 feat(registry): add Wensity (#10676) 2026-05-18 22:13:41 +04:00
lcorrigan704
c2dc06a99c add ui.corr.sh registry (#10675) 2026-05-18 18:57:42 +04:00
shadcn
c9930b7fda fix(toggle-group): update default spacing (#10681) 2026-05-17 12:52:42 +04:00
shadcn
d1149454a8 fix(create): preserve settings when changing style (#10680) 2026-05-17 12:31:37 +04:00
Michael McGovern
36139f6200 feat(registry): add @paddle registry (#10643)
* Add Paddle UI to directory

* Update description for @ncdai #10591
2026-05-14 15:45:15 +04:00
shadcn
15ac1be92b feat: shadcn/create for existing projects (#10622)
* feat: update project form

* feat: switch to toggle

* fix

* fix: color
2026-05-12 15:15:07 +04:00
Sergey Kuznetsov
8ca30ed32c feat(registry): add @turbopills-ui registry (#10579) 2026-05-10 20:34:04 +04:00
Alex Kostyniuk
e2605bc7c2 Update glasscn registry URL (#10597) 2026-05-10 20:33:38 +04:00
Chánh Đại
b8608d0976 Update description for @ncdai (#10591) 2026-05-09 16:50:22 +04:00
shadcn
fc1ca40af4 fix(v4): revert next dependency bump (#10584) 2026-05-08 10:28:15 +04:00
Yasuhiro Yamada
f454f6e4d1 fix(sidebar): use oklch sidebar tokens in outline shadows (#10577)
Co-authored-by: Yasuhiro Yamada <7986171+galoi@users.noreply.github.com>
2026-05-08 05:27:41 +04:00
Aniket Pawar
8cc7073aec feat(registry): add @framecn (#10558)
* Add new registry entry for @framecn

* Add new entry for @framecn with video components

* Fix JSON formatting in registries.json

* revert @text-ui in registries.json
2026-05-08 05:27:02 +04:00
shadcn
031387a471 deps: update next (#10581)
* deps: update next

* deps
2026-05-08 05:26:02 +04:00
Alexandre Schrammel
dd3567c39d feat(registry): add @amplo registry (#10573) 2026-05-07 10:27:07 +04:00
joe
ad2b8891a5 adds text-ui to the registry (#10571) 2026-05-06 23:23:40 +04:00
shadcn
f6e18c65cf perf: isr preview and llm (#10566)
* perf(create): limit preview prerender params

* perf(llm): generate markdown routes on demand
2026-05-06 14:57:42 +04:00
shadcn
1c4a53a37a test(shadcn): derive previous minor assertion (#10567)
* test(shadcn): derive previous minor assertion

* chore(changeset): add previous minor assertion

* chore: update changeset
2026-05-06 14:29:02 +04:00
shadcn
bc2db187aa feat(docs): add clickable mdx heading anchors 2026-05-06 13:27:03 +04:00
Krishna Agarwal
92b4927a80 feat(registry): add @indiacn registry (#10548) 2026-05-06 13:10:45 +04:00
Matheus Lima
3cbabe012e feat(registry): add @cognicatch to the ecosystem directory (#10217)
* feat(registry): add @cognicatch to directory

* feat(registry): add @cognicatch to directory

* build: update registries.json

* build: update registries.json

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-05-06 13:09:20 +04:00
github-actions[bot]
1137b24a97 chore(release): version packages (#10556)
Co-authored-by: shadcn <m@shadcn.com>
2026-05-05 16:56:17 +04:00
shadcn
bb251e2ab6 docs: add changelog 2026-05-05 16:52:06 +04:00
shadcn
28b3e5f360 fix(cli): suggest previous minor on errors (#10559) 2026-05-05 16:15:58 +04:00
shadcn
309d95017f feat(shadcn): alias placeholders in target (#10528)
* feat: add support for package imports

* fix

* test(cli): surface add command failures

* test(cli): remove stale pnpm pin from fixture

* fix(cli): reject invalid package import targets

* fix(cli): address package import review feedback

* feat(shadcn): alias placeholders in target

* docs: update docs for alias and examples

* chore: remove lockfile drift

* chore: clean up

* fix(shadcn): route target aliases by workspace

* docs(registry): document target subdirectories
2026-05-05 14:55:47 +04:00
shadcn
eb42ae25fd feat: add support for package imports (#10519)
* feat: add support for package imports

* fix

* test(cli): surface add command failures

* test(cli): remove stale pnpm pin from fixture

* fix(cli): reject invalid package import targets

* fix(cli): address package import review feedback

* test: expand coverage

* docs: add package imports docs
2026-05-05 12:24:21 +04:00
Franco Zeta
3977fb9ace feat(registry): add @stepper registry (#10552) 2026-05-04 19:04:54 +04:00
Jay Sharma
7865621397 feat: add @evilbuttons registry with animated button components (#10536)
* feat: add @evilbuttons registry with animated button components

* feat(registry): add @delta registry (#10476)

Co-authored-by: shadcn <m@shadcn.com>

* Added @nordaun registry (#10537)

* feat: add @evilbuttons registry with animated button components

* feat(fix): fix mismatched dis...

---------

Co-authored-by: Patrick Prunty <58374462+pprunty@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
Co-authored-by: Nordaun <admin@nordaun.com>
2026-04-30 17:02:03 +04:00
Nordaun
b07070cd07 Added @nordaun registry (#10537) 2026-04-29 22:30:23 +04:00
Patrick Prunty
ad68a44717 feat(registry): add @delta registry (#10476)
Co-authored-by: shadcn <m@shadcn.com>
2026-04-29 21:09:28 +04:00
Justin Levine
56161142f1 feat: add @shieldcn registry (#10487) 2026-04-28 21:30:30 +04:00
github-actions[bot]
c2e1a5793f chore(release): version packages (#10517)
Co-authored-by: shadcn <m@shadcn.com>
2026-04-28 20:45:27 +04:00
shadcn
ea6086cbcc feat: add preset commands (#10530)
* feat(cli): add preset commands

* docs(skill): update preset command guidance

* docs(cli): document preset commands

* chore: changeset

* fix(cli): refine preset command output

* fix(cli): align preset decode output

* fix(cli): update preset output fields

* docs(changelog): add preset commands entry

* docs(changelog): show preset command output

* docs(changelog): clarify preset resolve examples

* docs(changelog): refine preset examples

* docs(changelog): add preset command sections

* docs(changelog): show preset resolve output

* docs(changelog): clarify preset open example

* docs(changelog): update preset resolve example

* docs: update preset announcement

* docs: link preset announcement to changelog

* test: increase next init timeout
2026-04-28 20:43:16 +04:00
shadcn
68a69d81f7 chore: add devl registry (#10529) 2026-04-28 12:28:07 +04:00
shadcn
55fd4dc71b feat(shadcn): add code redirect (#10526) 2026-04-28 10:32:29 +04:00
shadcn
6dea65ebcb fix(shadcn): apply for monorepo (#10524)
* fix(shadcn): apply for monorepo

* fix
2026-04-28 09:52:10 +04:00
Gurbinder
ba10089b8d feat(registry): Add @evilcharts registry (#10502) 2026-04-27 15:41:47 +04:00
shadcn
8a814f926b ci: fix signed commits permissions (#10518) 2026-04-27 15:13:07 +04:00
shadcn
c236d0c009 feat: add preset code to shadcn info (#10516)
* feat: shadcn info preset code

* chore: changeset

* refactor(shadcn): simplify color catalog

* refactor(shadcn): clean up preset resolver

* chore: format
2026-04-27 12:17:33 +04:00
Shawn.
fd0e0c369b chore: add @dotmatrix to registry directory (#10504) 2026-04-27 10:54:52 +04:00
shadcn
07d14abde1 ci: check signed commits (#10506) 2026-04-27 10:54:04 +04:00
shadcn
8dd51c49f8 fix(registry): use https for xcn homepage 2026-04-26 19:05:42 +04:00
Alex Kostyniuk
c20e0cc596 feat(registry): add @glasscn registry (#10491)
* feat(registry): add @glasscn registry

* chore(registry): remove unrelated @xcn diff
2026-04-26 19:02:23 +04:00
github-actions[bot]
0126502236 chore(release): version packages (#10489)
Co-authored-by: shadcn <m@shadcn.com>
2026-04-25 14:39:56 +04:00
shadcn
94074e4bb2 ci: release 2026-04-25 14:36:50 +04:00
shadcn
eb6e783fb3 feat: add --pointer option (#10488) 2026-04-25 14:24:25 +04:00
Wolfr
f785bfab44 docs(figma): replace Obra Studio entry with Pro edition (#10474)
- Remove free Obra shadcn/ui entry from Free section
- Add Obra shadcn/ui Pro under Paid
2026-04-25 11:26:31 +04:00
shadcn
cc20c8a794 chore: changelog 2026-04-23 10:15:13 +04:00
Mason James
05948dce8e Update shadcnblocks registry description (#10463) 2026-04-22 15:38:03 +04:00
Ray
5d23df4e35 fix(templates): add notFoundComponent to start root route (#10369) 2026-04-22 10:56:23 +04:00
akazwz
abbdd32953 fix(v4): add missing sera styles to public schema (#10452)
* Add Sera styles to v4 public schema

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(v4): tighten public schema style assertion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2026-04-22 09:50:30 +04:00
Jay Sharma
3f14ffa632 Update URL for @xcn entry in directory.json 2026-04-21 19:27:34 +04:00
ericzakariasson
5927f6de80 chore: update cursor plugin description
Made-with: Cursor
2026-04-21 17:47:35 +04:00
shadcn
39eb34104b Merge pull request #10444 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-04-21 16:28:15 +04:00
github-actions[bot]
7cbc7e8d53 chore(release): version packages 2026-04-21 12:26:54 +00:00
shadcn
d0ac558ce2 Merge pull request #10396 from ramonclaudio/docs/dark-mode-tanstack-start
docs(dark-mode): add tanstack start guide
2026-04-21 16:25:48 +04:00
shadcn
bc0c46a93c Merge pull request #10445 from ericzakariasson/add-cursor-plugin-manifest
feat: add Cursor plugin manifest
2026-04-21 16:23:46 +04:00
shadcn
a64575d8a4 Merge pull request #10454 from Bartek532/feat/loading-ui-registry
feat: add loading-ui registry to index
2026-04-21 16:22:16 +04:00
shadcn
5d0cd7819b Merge pull request #10449 from radiumcoders/main
Add @xcn registry entry
2026-04-21 16:21:23 +04:00
shadcn
13478b26b6 Merge pull request #10451 from shadcn-ui/shadcn/apply-only
feat: add apply --only
2026-04-21 16:20:14 +04:00
Jay Sharma
aee8a71679 Merge branch 'main' into main 2026-04-21 17:18:34 +05:30
Bartek532
4507f1c794 chore: refine loading-ui registry description
Update the Loading UI directory description copy and regenerate the public registries index to keep generated metadata in sync.
2026-04-21 13:22:24 +02:00
Bartosz Zagrodzki
81cd2266aa Merge branch 'main' into feat/loading-ui-registry 2026-04-21 13:20:14 +02:00
Bartek532
cf756b1b55 feat: add loading-ui registry to index
Add the Loading UI registry to the curated directory and regenerate registries.json so the new source appears in the public registry index.
2026-04-21 13:19:18 +02:00
shadcn
5e61f9c4a4 test: ensure --radius is coming through 2026-04-21 13:03:40 +04:00
shadcn
c4def9305f docs: update 2026-04-21 13:03:25 +04:00
shadcn
e456fed9d3 feat: add apply --only 2026-04-21 12:57:56 +04:00
Ray
b95cd29508 Merge branch 'main' into docs/dark-mode-tanstack-start 2026-04-21 03:46:24 -04:00
shadcn
11cbc32840 refactor: caching for build registry 2026-04-21 11:25:56 +04:00
shadcn
01539fb4d7 refactor: add getThemeScript 2026-04-21 10:35:34 +04:00
Radiumcoders
e47ee89dcf Add @xcn registry entry 2026-04-20 20:16:26 +05:30
Ray
2f5c32c0b1 Merge branch 'main' into docs/dark-mode-tanstack-start 2026-04-20 10:22:34 -04:00
ericzakariasson
fbfe9f34bb feat: add Cursor plugin manifest
Adds .cursor-plugin/plugin.json so this repo installs as a Cursor
plugin via /add-plugin shadcn-ui/ui.

- Loads the existing skills/shadcn/SKILL.md skill (auto-discovered
  via the manifest's skills field).
- Registers the shadcn MCP server (npx shadcn@latest mcp) inline so
  users get the same MCP config already documented for every other
  client without hand-editing .cursor/mcp.json.
- Reuses skills/shadcn/assets/shadcn.png as the plugin logo.

No skill content or MCP changes — purely manifest wiring.

Made-with: Cursor
2026-04-20 10:40:32 +02:00
shadcn
d55e059fda Merge pull request #10440 from uiNerd16/add-aicanvas-registry
feat: add @aicanvas registry
2026-04-20 12:37:59 +04:00
shadcn
9c572ab778 fix: chartColor in presets 2026-04-20 12:29:55 +04:00
shadcn
91403eeb63 Merge pull request #10439 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-04-20 12:18:33 +04:00
github-actions[bot]
3411d53856 chore(release): version packages 2026-04-20 08:16:18 +00:00
shadcn
efa2b38d07 Merge pull request #10179 from EthanThatOneKid/fix/accept-header-issue-10164
fix(cli): add Accept: application/json header to registry fetch
2026-04-20 12:15:20 +04:00
shadcn
d00605c5fb chore: changeset 2026-04-20 11:55:18 +04:00
shadcn
4bdeea4c63 docs: update docs 2026-04-20 11:55:13 +04:00
shadcn
f632f5d798 feat: rename header 2026-04-20 11:55:06 +04:00
Ethan Davidson
7d6d489f83 Merge branch 'main' into fix/accept-header-issue-10164 2026-04-19 15:59:29 -07:00
uiNerd16
e8b1be1f22 feat: add @aicanvas registry 2026-04-19 22:10:56 +02:00
shadcn
d987955893 Merge pull request #10399 from ysds/registry-exabase
chore(registry): add @exabase registry
2026-04-19 20:54:40 +04:00
shadcn
7b5435ac0b Merge pull request #10436 from shadcn-ui/shadcn/fix-init-git-new-project-only
fix: ensure git init runs for new projects only
2026-04-19 20:49:03 +04:00
shadcn
f289497e35 Merge branch 'main' into shadcn/fix-init-git-new-project-only 2026-04-19 15:06:58 +04:00
shadcn
0d266984e6 Merge pull request #10438 from shadcn-ui/shadcn/release-workflows
Consolidate release workflows and clarify run names
2026-04-19 15:06:49 +04:00
shadcn
cf92d4f8f2 Consolidate release workflows and beta comment handling 2026-04-19 14:59:14 +04:00
shadcn
b7cfc364ac chore: changeset 2026-04-19 13:11:24 +04:00
shadcn
de385d04fc fix: ensure git init runs for new projects only 2026-04-19 12:55:07 +04:00
shadcn
b9f78c8a35 Merge pull request #10418 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-04-16 20:56:50 +04:00
github-actions[bot]
97b9e7b0ae chore(release): version packages 2026-04-16 16:56:01 +00:00
shadcn
e4b25981bf Merge pull request #10416 from shadcn-ui/shadcn/style-sera
feat: sera
2026-04-16 20:54:32 +04:00
shadcn
1017410468 chore: changeset 2026-04-16 20:51:21 +04:00
shadcn
fa71bb8624 fix 2026-04-16 15:52:36 +04:00
shadcn
d99839ec2a fix 2026-04-16 15:15:30 +04:00
shadcn
70b6bfd687 fix 2026-04-16 14:32:05 +04:00
shadcn
541c08f112 fix 2026-04-16 14:22:28 +04:00
shadcn
420433ae6f Merge branch 'main' into shadcn/style-sera 2026-04-16 14:17:19 +04:00
shadcn
a7d77e0cf7 fix 2026-04-16 13:38:49 +04:00
shadcn
7ec2acc87d fix 2026-04-15 21:04:08 +04:00
ysds
eeb5d22fe5 chore(registry): add @exabase registry 2026-04-15 12:08:19 +09:00
Ray
a757e80242 docs(dark-mode): add tanstack start guide 2026-04-14 15:31:40 -04:00
shadcn
84d1d476b1 Merge pull request #9728 from htmujahid/main
Update URL for @shadcn-editor in registries.json
2026-04-14 21:03:02 +04:00
shadcn
a52a606fb5 Merge branch 'main' into shadcn/style-sera 2026-04-14 00:04:39 +04:00
shadcn
6ba39bb720 fix 2026-04-14 00:03:30 +04:00
shadcn
dd4b5c287c fix 2026-04-13 17:15:11 +04:00
shadcn
aa534e5875 fix 2026-04-13 12:59:38 +04:00
shadcn
2be9640c88 feat: build registry 2026-04-10 11:35:48 +04:00
shadcn
56567ae21a fix 2026-04-10 05:37:42 +04:00
shadcn
429e258322 fix 2026-04-09 16:12:35 +04:00
shadcn
2f57100061 feat: init 2026-04-09 13:49:02 +04:00
shadcn
fc62d5781d Merge pull request #10337 from ramonclaudio/docs/llms-txt-drift
docs(llms.txt): fix 404 and backfill missing routes
2026-04-09 05:13:18 +04:00
shadcn
d86c5e5939 Merge pull request #9484 from ramonclaudio/fix/docs-copy-page-components-list
fix(docs): replace <ComponentsList /> in copy-page and markdown output
2026-04-08 22:02:29 +04:00
shadcn
8006dd1c93 Merge branch 'main' into fix/docs-copy-page-components-list 2026-04-08 21:43:08 +04:00
Ray
1dcbb4c88a docs(llms.txt): fix 404 and backfill missing routes
llms.txt was added in #8460 and hasn't kept up with the docs tree.
Audited every URL against apps/v4/content/docs and fixed the drift
in one pass.

Removed:
- About (/docs/about): returns 404, no about.mdx exists
- Form (/docs/components/form): points at a phantom. No radix/form.mdx
  exists post-#9304. URL only resolves because of a redirect in
  next.config.mjs, which lands at /docs/forms. That page is already
  listed as 'Forms Overview' in the ## Forms section, and the real
  form library docs (React Hook Form, TanStack Form, Next.js) are
  listed there too. The Form component entry is a stale duplicate.

Added to Overview:
- Skills (/docs/skills)
- Directory (/docs/directory)

Added whole RTL section (new since #8460):
- RTL (/docs/rtl)
- RTL - Next.js
- RTL - Vite
- RTL - TanStack Start

Added to Components:
- Direction (Misc)
- Native Select (Form & Input, after Select)
- Sonner (Feedback & Status, after Toast, since Sonner has its own
  docs page even though Toast already uses it under the hood)

Added to Registry:
- Namespaces
- Add a Registry (open source registry index)
- Open in v0 integration
- registry.json schema docs
- registry-item.json spec docs

Descriptions match the short curated style of the rest of the file.
Noticed while working on #9484.
2026-04-08 12:36:44 -04:00
shadcn
4f4ffde4aa chore: update registries 2026-04-08 20:01:13 +04:00
Ray
6d7a0ed93b fix(docs): replace <ComponentsList /> in copy-page and markdown output
The <ComponentsList /> tag on /docs/components was emitted as-is by
the Copy Page button and the /llm/[slug] markdown endpoint because
getComponentsList() walked components.children for pages directly.
After #9304 restructured the folder into components/radix/ and
components/base/ subfolders, the filter always returned [] and the
tag was replaced with an empty string (or, in the copy-page case,
never replaced at all).

- Reuse getPagesFromFolder() from lib/page-tree so the walker stays
  in sync with the on-screen ComponentsList React component.
- Match the existing llms.txt format: flat absolute URLs (the
  /docs/components/:name -> /docs/components/radix/:name redirect in
  next.config.mjs is the canonical form) plus the frontmatter
  description pulled via source.getPage() on each page.
- Export replaceComponentsList() and call it from
  docs/[[...slug]]/page.tsx so the Copy Page button goes through the
  same replacement path as processMdxForLLMs.
2026-04-08 11:50:07 -04:00
shadcn
b909b0363f Merge pull request #10324 from wrappixelTeam/feat/added-shadcn-dashboard
feat(registry): added new registry ( @shadcn-dashboard )
2026-04-08 19:15:16 +04:00
shadcn
a6fa6893eb Merge pull request #10333 from kapishdima/feat/remocn
feat: added @remocn to directory.json
2026-04-08 19:08:48 +04:00
KapishDima
561586bd98 Merge branch 'main' into feat/remocn 2026-04-08 16:41:56 +03:00
kapishdima
7ddb30aade feat: added @remocn to directory.json 2026-04-08 16:38:33 +03:00
shadcn
024425d45a fix: directory pager 2026-04-08 17:05:50 +04:00
Mihir Koshti
4bdaf48f9b Merge branch 'main' into feat/added-shadcn-dashboard 2026-04-08 18:15:55 +05:30
shadcn
e9546e87ff Merge pull request #10332 from shadcn-ui/shadcn/open-preset
feat: add open preset
2026-04-08 16:43:39 +04:00
shadcn
0b34d581f9 feat: add open preset 2026-04-08 16:33:32 +04:00
shadcn
5c2ed5e90e Merge branch 'main' of github.com:shadcn-ui/ui 2026-04-08 14:42:57 +04:00
shadcn
e9443ccd4a docs: add apply changelog 2026-04-08 14:42:52 +04:00
shadcn
1fe0fe65e8 Merge pull request #10331 from shadcn-ui/shadcn/directory-refactor
refactor: directory
2026-04-08 12:31:58 +04:00
shadcn
6823bad998 refactor: directory 2026-04-08 12:23:34 +04:00
shadcn
398e6c3406 fix: formatting in registries.json 2026-04-08 11:52:03 +04:00
shadcn
710cc27de7 Merge pull request #10330 from Aniket-508/feat/termcn-directory
feat(registry): add @termcn
2026-04-08 10:42:26 +04:00
Aniket Pawar
08212a478d feat(registry): add @termcn 2026-04-08 02:47:14 +00:00
shadcn
d718a8045f Merge pull request #10328 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-04-07 22:13:37 +04:00
github-actions[bot]
2c4678c8c8 chore(release): version packages 2026-04-07 17:48:58 +00:00
shadcn
2466a300f4 Merge pull request #10313 from shadcn-ui/shadcn/apply-preset
feat: add apply
2026-04-07 21:47:54 +04:00
shadcn
66fcf1e853 Merge branch 'shadcn/apply-preset' of github.com:shadcn-ui/ui into shadcn/apply-preset 2026-04-07 21:36:15 +04:00
shadcn
5ebd54198d fix 2026-04-07 21:36:09 +04:00
shadcn
3a2d812510 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 21:24:14 +04:00
shadcn
7811557088 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 21:23:59 +04:00
shadcn
575f1602a1 fix 2026-04-07 21:05:08 +04:00
htmujahid
50dc9b506b fix: update URL for @shadcn-editor to point to raw GitHub content 2026-04-07 19:10:12 +05:00
shadcn
ae70ecc2f3 Merge branch 'shadcn/apply-preset' of github.com:shadcn-ui/ui into shadcn/apply-preset 2026-04-07 18:02:20 +04:00
shadcn
42284f4e64 test: update rtl 2026-04-07 18:02:05 +04:00
Mihir Koshti
6b5aa16668 updated name to @shadcndashboard 2026-04-07 18:31:07 +05:30
Mihir Koshti
706806a207 feat(registry): added new registry ( @shadcn-dashboard ) 2026-04-07 18:07:26 +05:30
Talha Mujahid
8a7502d7fa Merge branch 'shadcn-ui:main' into main 2026-04-07 17:34:11 +05:00
shadcn
abc65a4871 Merge branch 'main' into shadcn/apply-preset 2026-04-07 16:27:29 +04:00
shadcn
7d5af61468 style: fix code block 2026-04-07 16:11:26 +04:00
shadcn
2badcdc31f Merge pull request #10323 from shadcn-ui/docs/component-composition-sections
docs: add composition section
2026-04-07 15:57:44 +04:00
shadcn
64b8263450 docs: add changelog 2026-04-07 15:49:54 +04:00
shadcn
13b4593f37 fix 2026-04-07 15:49:26 +04:00
shadcn
7dc65da6b2 fix 2026-04-07 15:28:19 +04:00
shadcn
98e56b773c Merge branch 'shadcn/apply-preset' of github.com:shadcn-ui/ui into shadcn/apply-preset 2026-04-07 15:27:56 +04:00
shadcn
7ff9778ff0 fix 2026-04-07 15:27:33 +04:00
shadcn
4af7bbf4ba Merge branch 'main' into docs/component-composition-sections 2026-04-07 15:25:02 +04:00
shadcn
f00a94d9e5 docs: add composition section 2026-04-07 15:23:27 +04:00
shadcn
187ae44fa7 Merge pull request #10318 from shadcn-ui/dependabot/npm_and_yarn/templates/start-monorepo/vite-7.3.2
chore(deps-dev): bump vite from 7.3.1 to 7.3.2 in /templates/start-monorepo
2026-04-07 14:40:02 +04:00
shadcn
034178bf7d Merge pull request #10317 from shadcn-ui/dependabot/npm_and_yarn/templates/react-router-monorepo/vite-7.3.2
chore(deps-dev): bump vite from 7.3.1 to 7.3.2 in /templates/react-router-monorepo
2026-04-07 14:39:46 +04:00
shadcn
4064c78bc7 Merge pull request #10316 from shadcn-ui/dependabot/npm_and_yarn/templates/vite-monorepo/vite-7.3.2
chore(deps-dev): bump vite from 7.3.1 to 7.3.2 in /templates/vite-monorepo
2026-04-07 14:39:28 +04:00
shadcn
943b023b7c Merge pull request #10314 from shadcn-ui/dependabot/npm_and_yarn/vite-7.3.2
chore(deps): bump vite from 7.1.12 to 7.3.2
2026-04-07 14:39:12 +04:00
shadcn
e3d654fd26 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 14:01:23 +04:00
dependabot[bot]
71d0470be1 chore(deps-dev): bump vite in /templates/start-monorepo
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 22:12:00 +00:00
dependabot[bot]
53bbdc738f chore(deps-dev): bump vite in /templates/react-router-monorepo
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 21:45:55 +00:00
dependabot[bot]
97707ec08e chore(deps-dev): bump vite in /templates/vite-monorepo
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 21:45:51 +00:00
dependabot[bot]
b9ce2f10c3 chore(deps): bump vite from 7.1.12 to 7.3.2
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.12 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 20:17:13 +00:00
shadcn
7cb3b13a33 fix 2026-04-06 23:32:48 +04:00
shadcn
e3d2b14911 fix 2026-04-06 23:27:15 +04:00
shadcn
58c9dc2a7e fix 2026-04-06 23:27:09 +04:00
shadcn
3bdf60340d fix 2026-04-06 23:19:37 +04:00
shadcn
c1e29824cd feat: add apply command 2026-04-06 23:14:57 +04:00
shadcn
62f6df75f2 Merge pull request #10310 from shadcn-ui/shadcn/create-cls
fix: page cls
2026-04-06 17:51:50 +04:00
shadcn
62bae86e86 fix: page cls 2026-04-06 17:37:56 +04:00
shadcn
aa69fbf85a Merge pull request #10309 from shadcn-ui/shadcn/create-perf
fix: create page perf
2026-04-06 16:56:13 +04:00
shadcn
8d41295f2c fix: create page perf 2026-04-06 16:45:17 +04:00
shadcn
2b053d916d Merge pull request #10285 from vzkiss/add-flowkit-ui-registry
feat(registry): add @flowkit-ui
2026-04-06 15:22:32 +04:00
shadcn
0d1309f322 Merge branch 'main' of github.com:shadcn-ui/ui 2026-04-06 15:15:59 +04:00
shadcn
c26250dcfe docs: changelog updates 2026-04-06 15:15:42 +04:00
vzkiss
07c5c36be8 Merge branch 'main' into add-flowkit-ui-registry 2026-04-06 08:51:51 +02:00
shadcn
21c9cc5246 Merge pull request #10303 from oliviertassinari/keywords
fix: add base-ui keyword to match GitHub topic
2026-04-06 10:41:35 +04:00
shadcn
058960046a Merge pull request #10167 from oliviertassinari/fix-base-ui-use-client-v2
fix: remove unnecessary Base UI use client
2026-04-06 10:41:17 +04:00
Olivier Tassinari
be80c18ea9 fix: add base-ui keyword to match GitHub topic 2026-04-06 00:29:21 +02:00
vzkiss
3c59a0cd95 Merge branch 'main' into add-flowkit-ui-registry 2026-04-05 23:16:10 +02:00
shadcn
26d0228ee9 fix 2026-04-04 13:51:50 +04:00
shadcn
9050646893 chore: rebuild registry 2026-04-04 13:44:23 +04:00
shadcn
3ca09b9647 Merge branch 'main' into fix-base-ui-use-client-v2
# Conflicts:
#	apps/v4/examples/base/button-render.tsx
#	apps/v4/public/r/styles/base-lyra/button.json
#	apps/v4/public/r/styles/base-mira/slider.json
#	apps/v4/public/r/styles/base-nova/button.json
#	apps/v4/public/r/styles/base-vega/button.json
#	apps/v4/styles/base-luma/ui/slider.tsx
#	apps/v4/styles/base-lyra/ui/accordion.tsx
#	apps/v4/styles/base-lyra/ui/slider.tsx
#	apps/v4/styles/base-nova/ui-rtl/accordion.tsx
#	apps/v4/styles/base-nova/ui-rtl/button.tsx
#	apps/v4/styles/base-nova/ui/button.tsx
2026-04-04 13:42:29 +04:00
shadcn
720ccca653 Merge pull request #10242 from Yngesh-Raman-QED42/fix/native-select-option-colors
fix(native-select): use system colors for option and optgroup
2026-04-04 13:33:45 +04:00
shadcn
1e3dff8daa chore: rebuild registry 2026-04-04 13:21:15 +04:00
shadcn
c116b325ab Merge branch 'main' into fix/native-select-option-colors 2026-04-04 13:11:50 +04:00
shadcn
5b266d3fc9 Merge pull request #10229 from MKSinghDev/patch-1
Add @mksingh to the directory registry
2026-04-04 13:10:25 +04:00
shadcn
6095e6272d Merge pull request #10272 from vinihvc/feat/add-shark-ui-registry
Add @shark to open source registry index
2026-04-04 13:00:14 +04:00
shadcn
f3fc5a62f2 Merge pull request #10241 from glsee/gamifykit-patch-2
chore: update GamifyKit logo
2026-04-04 12:55:24 +04:00
shadcn
ef7507cc9a Merge pull request #10263 from ridemountainpig/add-flightcn-registry
feature: add @flightcn registry
2026-04-04 12:52:27 +04:00
vzkiss
16b7bea50d feat(registry): build @flowkit-ui to registries.json 2026-04-04 01:36:16 +02:00
vzkiss
ccc4caad9c feat(registry): add @flowkit-ui to directory.json 2026-04-04 01:13:04 +02:00
Mukesh Singh
ba2c4fc586 added @mksingh in public registries 2026-04-03 09:39:54 +05:30
Mukesh Singh
bb5afb2df1 Merge branch 'shadcn-ui:main' into patch-1 2026-04-02 21:07:47 -07:00
Vinicius Vicentini
53f45f5f6f running pnpm registry:build 2026-04-02 15:43:11 -06:00
Vinicius Vicentini
990040691c Update directory.json 2026-04-02 12:44:52 -07:00
Vinicius Vicentini
83857679cb Rename registry entry from '@shark-ui' to '@shark' 2026-04-02 12:40:37 -07:00
Vinicius Vicentini
61989da8ec chore(registry): add @shark-ui to open source registry index
Adds Shark UI (Ark UI + Tailwind) to directory.json and regenerates
public/r/registries.json per registry index workflow.

Homepage: https://shark.vini.one
Registry template: https://shark.vini.one/r/{name}.json
Source: https://github.com/vinihvc/shark-ui

Made-with: Cursor
2026-04-02 13:32:45 -06:00
shadcn
768d8a808f Merge pull request #10268 from shadcn-ui/claude/update-schema-luma-atRnG 2026-04-02 18:47:23 +04:00
Claude
95479a06bb Add radix-luma and base-luma styles to schema.json
https://claude.ai/code/session_01UBbkLbn8ihvnnzw62FpBax
2026-04-02 10:32:47 +00:00
ridemountainpig
4289d5fe02 feature: add @flightcn registry 2026-04-02 09:14:21 +08:00
shadcn
5a6702845d feat: adjust slider for mira 2026-04-01 05:14:28 +04:00
github-actions[bot]
ebf2192d98 chore(release): version packages (#10247)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-31 22:27:14 +04:00
shadcn
44c09a19b0 feat: luma (#10246)
* feat: init style-luma

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* chore: changeset

* fix

* fix
2026-03-31 22:24:48 +04:00
shadcn
4101ec98af fix: colors 2026-03-31 11:53:03 +04:00
Yngesh Raman
a7c3300d7a fix(native-select): use system colors for option and optgroup 2026-03-31 13:11:40 +05:30
Kaiden See (Github-verified)
b50acc9d21 chore: update GamifyKit logo 2026-03-31 15:37:54 +08:00
shadcn
fc76a9ada2 fix: customizer 2026-03-31 04:55:01 +04:00
Johurul Haque
d6b4bf8ddc Add @tailgrids to registry directory (#10062)
* feat: add @tailgrids to the registry directory.

* chore: run registry:build script
2026-03-30 16:55:32 +04:00
Mukesh Singh
2c334c3c2d Add @mksingh to the registry
#10228 
Added new registry entry for @mksingh with details and logo.
2026-03-29 03:19:27 -07:00
shadcn
d3de6aa760 refactor: clean up unused files (#10227)
* refactor: clean up unused files

* fix
2026-03-29 12:04:18 +04:00
shadcn
23b2ac4dcf refactor: create page (#10212)
* refactor: create page

* fix
2026-03-29 11:17:39 +04:00
shadcn
e56c476105 fix: debug e2es (#10204)
* fix

* fix

* fix

* fix

* fix

* fix

* Revert "fix"

This reverts commit 98cbe82048.

* fix
2026-03-27 19:37:47 +04:00
github-actions[bot]
14bb486174 chore(release): version packages (#10203)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-27 11:37:12 +04:00
shadcn
12b49c986f fix: packageManager in package.json (#10202)
* fix: packageManager in package.json

* fix
2026-03-27 11:25:05 +04:00
Jamie Davenport
64c8cd99ee feat(registry): adds openpolicy components (#10196) 2026-03-27 10:52:23 +04:00
shadcn
7d718ddaa9 fix: refactor styles (#10190)
* feat: refactor styles handling across v4

* fix

* fix

* fix

* fix

* fix

* fix
2026-03-26 14:36:00 +04:00
shadcn
5570b3e24a Revert "deps: update next to 16.2.1 (#10180)" (#10189)
This reverts commit 8bd161d453.
2026-03-26 14:23:24 +04:00
Ethan Davidson
945298ed2d Merge branch 'main' into fix/accept-header-issue-10164 2026-03-26 00:29:45 -07:00
Ethan Davidson
f9b216af77 docs(registry): document content negotiation with Express example 2026-03-26 00:24:48 -07:00
Ethan Davidson
6525227036 fix(cli): add Accept and User-Agent headers to support content negotiation (fixes #10164) 2026-03-26 00:24:48 -07:00
shadcn
214b1b8479 Revert "feat: refactor styles handling across v4 (#10176)" (#10185)
This reverts commit 64b88b6cdb.
2026-03-26 11:14:52 +04:00
shadcn
8bd161d453 deps: update next to 16.2.1 (#10180)
* deps: update next to 16.2.1

* fix
2026-03-26 10:10:50 +04:00
shadcn
64b88b6cdb feat: refactor styles handling across v4 (#10176)
* feat: refactor styles handling across v4

* fix

* fix

* fix

* fix

* fix

* fix
2026-03-26 09:50:58 +04:00
Olivier Tassinari
0c25e712e1 pnpm registry:build 2026-03-25 11:55:07 +01:00
shadcn
6a070bf8c5 docs: update theming 2026-03-25 12:50:08 +04:00
shadcn
124495f0df fix: yarn (#10173) 2026-03-25 12:04:25 +04:00
shadcn
43f64065b7 docs: rewrite installation docs (#10172) 2026-03-25 11:35:14 +04:00
Olivier Tassinari
4f421aba65 fix: remove unnecessary Base UI use client 2026-03-25 01:35:03 +01:00
shadcn
8bec9c1234 Merge branch 'main' of github.com:shadcn-ui/ui 2026-03-24 22:52:22 +04:00
shadcn
ba6ac6ec63 fix: button 2026-03-24 22:51:51 +04:00
shadcn
b75796ed76 fix: navigation menu (#10162) 2026-03-24 22:29:59 +04:00
shadcn
d82b4a7d98 fix: radial charts for v3 2026-03-24 17:15:45 +04:00
Luis Llanes
5b79499d23 fix(registry): update @shadcncraft entry in directory (#10151)
* fix(registry): update @shadcncraft entry in directory with new homepage, description, and logo

* fix(registry): update @shadcncraft entry in `registries.json` after running `pnpm registry:build` command
2026-03-24 12:12:37 +04:00
shadcn
d78ff8b858 feat: update colors for popovers (#10155)
* feat: update colors for popovers

* fix: project form
2026-03-24 12:12:01 +04:00
shadcn
ef78384bfd feat: replace "Add a block" button with "View Components" linking to /docs/components (#10154)
https://claude.ai/code/session_01NnmghguJjhqSxuHE8PBLNi

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-24 10:13:30 +04:00
Ray
d3ab7fb00b feat(registry): add @ramonclaudio-coderabbit to directory (#9331) 2026-03-23 20:08:25 +04:00
shadcn
bebc4356af fix: chart types (#10147) 2026-03-23 15:58:15 +04:00
shadcn
14bc966fee feat: recharts docs (#10146)
* feat: recharts docs

* docs: update
2026-03-23 15:26:55 +04:00
shadcn
6a4b27b80d fix 2026-03-23 13:30:05 +04:00
shadcn
c5b4080649 fix(v4): restore active chart demos 2026-03-23 13:14:10 +04:00
shadcn
408b25c82a fix(v4): stabilize chart legend keys 2026-03-23 12:29:31 +04:00
shadcn
228b0e3ecd feat(v4): support Recharts v3 2026-03-23 12:16:35 +04:00
shadcn
f900bd57d0 feat: implement reset shortcut (#10145)
* feat: implement reset shortcut

* fix

* fix
2026-03-23 11:31:45 +04:00
Kaiden / GL
6b190c6a18 chore: add GamifyKit to directory (#9286) (#9289)
Co-authored-by: shadcn <m@shadcn.com>
2026-03-23 11:11:14 +04:00
Aryan Gupta
c43bc4f5d6 chore(registry): add @launchui registry (#9332)
Co-authored-by: shadcn <m@shadcn.com>
2026-03-23 11:07:11 +04:00
Sina Bayandorian
9cd14a684f chore: @react-slot added to the list of registries (#9522)
Co-authored-by: shadcn <m@shadcn.com>
2026-03-23 10:58:26 +04:00
shadcn
fc1675e54d Revert "Registry: Add @code-blocks to open registry directory (#9639)" (#10144)
This reverts commit a5abe1aa0f.
2026-03-23 10:52:10 +04:00
Pablo Hdez
a5abe1aa0f Registry: Add @code-blocks to open registry directory (#9639)
* feat: add code-blocks to registries data

* feat: add ``code-blocks`` to directory json file

* feat: add code-blocks to registries file

* Fix @code-blocks registry logo

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-03-23 10:51:38 +04:00
Pulkit
031998436f feat: add pulkitxm's components (#9775)
* chore: add pulkitxm's shadcn component ref.

* Update apps/v4/registry/directory.json

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

* feat: add animated shadcn components by pulkitxm with updated descriptions

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2026-03-23 10:26:37 +04:00
DSikeres1
29cb65c26b feat(registry): add @dsikeres1 registry (#10044)
* feat(registry): add @dsikeres1 registry (react-date-range-picker)

* chore: add @dsikeres1 entry to registries.json

Run registry:build output — adds the public-facing entry
(without logo) to match directory.json.

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-03-23 10:16:23 +04:00
Alexandre Joly
179c0c0b23 Add @react-easy-modals registry (#10064)
* Add @react-easy-modals registry

Add react-easy-modals to the registry directory. Provides a Modal component that integrates with shadcn Dialog for a simple, powerful modal system with TypeScript support and promise-based API.

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

* use {style} URL for auto Radix/Base UI resolution

* build: run registry:build to update registries.json

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: shadcn <m@shadcn.com>
2026-03-23 10:06:15 +04:00
Petar Stoev
03430e03bf feat(registry): add @w3-kit Web3 components registry (#10081)
* feat(registry): add @w3-kit Web3 components registry

* build: regenerate registries.json

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-03-23 09:54:56 +04:00
Shrey Kuvera
169682d87a Add @odysseyui to directory (#10083)
Co-authored-by: shadcn <m@shadcn.com>
2026-03-23 09:35:08 +04:00
Paul Tsnobiladzé
336eee688e add @rescript-shadcn registry (#10103) 2026-03-23 09:20:35 +04:00
Luis Llanes
32e4827559 docs(figma): update shadcncraft UI Kit entry to reflect new branding and features (#10138) 2026-03-23 09:05:24 +04:00
OrcDev
7a81328b23 feat: update 8bitcn logo to new pixel-art design (#10142) 2026-03-23 09:04:50 +04:00
shadcn
5b40b9de5a Revert "feat: update customizer (#10129)" (#10130)
This reverts commit e327cef2c1.
2026-03-20 16:56:57 +04:00
shadcn
e327cef2c1 feat: update customizer (#10129) 2026-03-20 16:46:40 +04:00
github-actions[bot]
563d572ba0 chore(release): version packages (#10120)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-19 23:36:28 +04:00
shadcn
687f09817b feat: chartColor and fontHeading (#10115)
* feat: chart color

* fix

* fix

* fix: chart color

* chore: changeset

* chore: restore directory registry formatting

* feat: add fontHeading

* feat: rebuild registry

* fix: v0

* refactor

* fix

* fix

* fix

* fix

* fix

* fix: refactor preset handling

* fix

* fix

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

* chore(templates): bump minor dependencies

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

---------

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

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

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

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

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

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

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

Two issues caused the previous fix to fail:

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

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

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

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

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

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

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

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

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

Two more issues found in CI logs:

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

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

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

---------

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

* fix

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

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

* Initial plan

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

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

---------

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

---------

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

* chore: remove changeset

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

---------

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

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

* Rename @hooks registry to @shadcnhooks

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

---------

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

Fixes #10028.

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

* chore: changeset

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 10:01:15 +04:00
Ahmed Zougari
b2cc0dfe59 fix(docs): update tanstack commands (#10045) 2026-03-15 10:00:58 +04:00
github-actions[bot]
af99d4ebd3 chore(release): version packages (#10037) 2026-03-14 18:50:45 +04:00
shadcn
a0a072dcdd Merge pull request #9929 from kapishdima/fix/registry-font
Add fontsource and support override for registry:font install
2026-03-14 18:30:41 +04:00
KapishDima
447c7aac06 Merge branch 'main' into fix/registry-font 2026-03-14 16:29:44 +02:00
shadcn
752615f231 Merge pull request #10032 from shadcn-ui/codex/rename-blocks-so-registry
chore: rename @blocks registry to @blocks-so
2026-03-14 18:21:18 +04:00
shadcn
f9b365bc7f chore: changeset 2026-03-14 16:13:05 +04:00
shadcn
17a1a9093a Merge branch 'fix/registry-font' of github.com:kapishdima/ui into fix/registry-font 2026-03-14 16:12:34 +04:00
shadcn
8159e98075 feat: update schema 2026-03-14 16:12:27 +04:00
shadcn
6a527b3e75 chore: rebuild registry 2026-03-14 16:12:18 +04:00
shadcn
ebe689e85c docs: update font examples 2026-03-14 16:12:07 +04:00
shadcn
8b683b44e6 Merge branch 'main' into fix/registry-font 2026-03-14 15:56:54 +04:00
shadcn
725ca574f6 Rename test to clarify non-variable font coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:37:06 +04:00
shadcn
1dc39e2484 Add dependency field to font schema for explicit fontsource package control.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:34:33 +04:00
shadcn
090556691c Revert "fix: added fountsource, @support ovveride when registry:font install, monorepo init"
This reverts commit ad99fc9a73.
2026-03-14 15:33:15 +04:00
shadcn
8e9f781cdb Merge pull request #9911 from lior-pesoa/main
Add @paletteui entry to directory.json
2026-03-14 14:13:34 +04:00
shadcn
9d7c205442 Merge branch 'main' into pr-9911 and resolve conflicts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:56:31 +04:00
shadcn
902379fa3e Merge pull request #9932 from felipemenezes098/feat/registry-add-flx
feat(registry): add @flx
2026-03-14 13:53:57 +04:00
shadcn
94dcf37add Rename @blocks registry to @blocks‑s 2026-03-14 12:39:37 +04:00
felipemenezes098
843a5e2334 feat(registry): add @flx 2026-03-13 16:57:36 -03:00
shadcn
cdaad392ae Merge pull request #10025 from Ziane-Badreddine/main
fix: swap homepage and url fields for @waves-cn registry
2026-03-13 20:53:35 +04:00
shadcn
49abe0d594 Merge branch 'main' into main 2026-03-13 20:52:16 +04:00
shadcn
eeb33ae9c9 fix: homepage and url order in registries.json 2026-03-13 20:47:35 +04:00
Ziane-Badreddine
55fa1bb7cc fix: correct homepage url in waves-cn registry entry 2026-03-13 12:45:26 +00:00
KapishDima
90bbbb7993 Merge branch 'main' into fix/registry-font 2026-03-13 09:19:02 +02:00
shadcn
fc9705665c Merge pull request #9976 from withden/patch-4
Add @pacekit-gsap registry information
2026-03-13 09:49:34 +04:00
Denish Navadiya
52c477e118 Merge branch 'main' into patch-4 2026-03-13 10:59:00 +05:30
withden
41a4573002 Add @pacekit-gsap to registry directory 2026-03-13 10:58:23 +05:30
shadcn
f813fb5884 Merge pull request #10024 from shadcn-ui/chore/remove-deprecated
chore: remove deprecated/ directory and related workflow
2026-03-13 09:24:37 +04:00
shadcn
429c001412 chore: remove deprecated/ directory and related workflow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:19:17 +04:00
shadcn
cd7743cbc1 Merge pull request #9923 from tsubasakong/lucas/fix-shadcn-ui-9914-preserve-base-on-preset-switch
docs: preserve current base when switching preset codes
2026-03-13 09:00:27 +04:00
shadcn
cadc3f96de chore: small nit in phrasing 2026-03-13 08:59:07 +04:00
shadcn
a74515d6e1 Merge pull request #9973 from jal-co/add-jalco-registry
feat(registry): add @jalco registry
2026-03-13 08:56:28 +04:00
shadcn
0df9af0d75 Merge pull request #9935 from Ziane-Badreddine/main
feat: add @waves-cn registry
2026-03-13 08:56:04 +04:00
Justin Levine
2bf8ef86b9 fix: revert unintended changes and run registry:build 2026-03-12 21:12:44 -07:00
shadcn
624a4fe320 Merge branch 'main' into patch-4 2026-03-13 08:03:23 +04:00
shadcn
5508b5e4ec Merge branch 'main' into add-jalco-registry 2026-03-13 08:01:39 +04:00
Justin Levine
40a00278ab Merge branch 'main' into add-jalco-registry 2026-03-12 14:08:03 -07:00
Denish Navadiya
5ab89f3ae3 Add @pacekit-gsap registry information 2026-03-12 17:10:30 +05:30
KapishDima
40dc195fad Merge branch 'main' into fix/registry-font 2026-03-12 09:19:14 +02:00
Justin Levine
d06e54d2bb feat(registry): add @jalco registry 2026-03-11 18:53:23 -07:00
Ziane-Badreddine
65ddce2886 chore: rebase on main 2026-03-11 19:45:55 +00:00
KapishDima
75cc35272a Merge branch 'main' into fix/registry-font 2026-03-11 10:32:27 +02:00
KapishDima
aa786280a3 Merge branch 'main' into fix/registry-font 2026-03-11 08:29:16 +02:00
KapishDima
6ad0590d87 Merge branch 'main' into fix/registry-font 2026-03-11 08:26:52 +02:00
KapishDima
dac13c90f2 Merge branch 'main' into fix/registry-font 2026-03-10 20:39:50 +02:00
tsubasakong
7bc47bb858 Merge remote-tracking branch 'upstream/main' into sync/pr-9923 2026-03-10 00:29:48 -07:00
KapishDima
e149aac756 Merge branch 'main' into fix/registry-font 2026-03-10 08:54:20 +02:00
KapishDima
729708ad2e Merge branch 'main' into fix/registry-font 2026-03-10 08:26:03 +02:00
kapishdima
ad99fc9a73 fix: added fountsource, @support ovveride when registry:font install, monorepo init 2026-03-09 12:39:36 +02:00
Lior Pesoa
da05ee321c fix: run registry:build and fix directory.json syntax
- Fix missing comma between entries in directory.json
- Fix multiline SVG logo string
- Regenerate registries.json with @paletteui entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-09 10:56:16 +02:00
Frank
d13d42d434 docs: preserve base when switching preset codes 2026-03-08 21:54:17 -07:00
lior-pesoa
554a1a69a7 Add @paletteui entry to directory.json 2026-03-08 18:50:13 +02:00
Talha Mujahid
b57e192965 Update URL for @shadcn-editor in registries.json 2026-02-25 08:01:05 +05:00
9637 changed files with 389526 additions and 334364 deletions

View File

@@ -9,5 +9,6 @@
"WebFetch(domain:github.com)"
],
"deny": []
}
},
"outputStyle": "Explanatory"
}

View File

@@ -0,0 +1,41 @@
{
"name": "shadcn",
"displayName": "shadcn/ui",
"version": "1.0.0",
"description": "UI component and design system framework. Search registries, install components as source code, and audit your project.",
"author": {
"name": "shadcn"
},
"homepage": "https://ui.shadcn.com",
"repository": "https://github.com/shadcn-ui/ui",
"license": "MIT",
"logo": "skills/shadcn/assets/shadcn.png",
"keywords": [
"shadcn",
"shadcn-ui",
"ui",
"components",
"tailwind",
"tailwindcss",
"radix",
"react",
"design-system",
"registry",
"mcp"
],
"category": "developer-tools",
"tags": [
"ui",
"components",
"design-system",
"react",
"tailwind"
],
"skills": "./skills/",
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}

View File

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

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

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

View File

@@ -1,9 +1,9 @@
# Adapted from create-t3-app.
name: Write Beta Release comment
name: Write Prerelease comment
on:
workflow_run:
workflows: ["Release - Beta"]
workflows: ["Release"]
types:
- completed
@@ -11,55 +11,84 @@ jobs:
comment:
if: |
github.repository_owner == 'shadcn-ui' &&
${{ github.event.workflow_run.conclusion == 'success' }}
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
name: Write comment to the PR
steps:
- name: "Comment on PR"
uses: actions/github-script@v6
# 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:
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"
uses: actions/github-script@v6
- 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

@@ -1,64 +0,0 @@
# Adapted from create-t3-app.
name: Release - Beta
on:
pull_request:
types: [labeled]
branches:
- main
permissions:
id-token: write
contents: read
jobs:
prerelease:
if: |
github.repository_owner == 'shadcn-ui' &&
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
name: Build & Publish a beta release to NPM
runs-on: ubuntu-latest
environment: Preview
steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use PNPM
uses: pnpm/action-setup@v4
with:
version: 9.0.6
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Install NPM Dependencies
run: pnpm install
- name: Modify package.json version
run: node .github/version-script-beta.js
- name: Publish Beta to NPM
run: pnpm pub:beta
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
with:
path: packages/shadcn
- name: Upload packaged artifact
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

View File

@@ -2,31 +2,63 @@
name: Release
run-name: ${{ github.event_name == 'pull_request' && format('Release Prerelease - PR {0}', github.event.number) || 'Release Stable' }}
on:
pull_request:
types: [labeled]
branches:
- main
push:
branches:
- main
permissions:
id-token: write
contents: write
pull-requests: write
jobs:
release:
if: ${{ github.repository_owner == 'shadcn-ui' }}
name: Create a PR for release workflow
prerelease:
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:
id-token: write
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@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Use PNPM
uses: pnpm/action-setup@v4
with:
version: 9.0.6
version: 10.33.4
- name: Use Node.js 20
uses: actions/setup-node@v4
@@ -41,16 +73,101 @@ jobs:
- name: Install NPM Dependencies
run: pnpm install
# - name: Check for errors
# run: pnpm check
# 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: Build the package
run: pnpm shadcn:build
- name: No changesets to prerelease
if: steps.changesets.outputs.present == 'false'
run: echo "::notice::No changesets found on this branch; nothing to prerelease."
# 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: 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: prerelease-info
path: prerelease-info.json
release:
if: ${{ github.event_name == 'push' && github.repository_owner == 'shadcn-ui' }}
name: Create Version PR or Publish Stable Release
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use PNPM
uses: pnpm/action-setup@v4
with:
version: 10.33.4
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Install NPM Dependencies
run: pnpm install
# 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
with:
gpg_private_key: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true
git_tag_gpgsign: true
- name: Create Version PR or Publish to NPM
id: changesets
uses: changesets/action@v1
with:
setupGitUser: false
commit: "chore(release): version packages"
title: "chore(release): version packages"
version: node .github/changeset-version.js

75
.github/workflows/signed-commits.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: Signed commits
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- ready_for_review
permissions:
pull-requests: write
jobs:
signed-commits:
if: github.repository_owner == 'shadcn-ui'
runs-on: ubuntu-latest
name: Signed commits
steps:
- name: Check PR commits
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const body = "Can you sign the commits please? See https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits. Thank you."
const { owner, repo } = context.repo
const pullNumber = context.payload.pull_request.number
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner,
repo,
pull_number: pullNumber,
per_page: 100,
})
const unsignedCommits = commits.filter((commit) => {
return commit.commit.verification?.reason === "unsigned"
})
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: pullNumber,
per_page: 100,
})
const existingComments = comments.filter((comment) => {
return comment.user.type === "Bot" && comment.body.trim() === body
})
if (unsignedCommits.length > 0) {
core.info(`Found ${unsignedCommits.length} unsigned commits.`)
if (existingComments.length === 0) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pullNumber,
body,
})
}
return
}
core.info("All commits are signed.")
for (const comment of existingComments) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id,
})
}

314
.github/workflows/templates.yml vendored Normal file
View File

@@ -0,0 +1,314 @@
name: Templates
on:
pull_request:
branches: ["*"]
paths:
- ".github/workflows/templates.yml"
- "apps/v4/registry/**"
- "package.json"
- "packages/shadcn/src/commands/add.ts"
- "packages/shadcn/src/commands/init.ts"
- "packages/shadcn/src/templates/**"
- "packages/shadcn/src/utils/create-project.ts"
- "packages/shadcn/src/utils/get-monorepo-info.ts"
- "packages/shadcn/src/utils/get-package-manager.ts"
- "pnpm-lock.yaml"
- "templates/**"
jobs:
validate:
runs-on: ubuntu-latest
name: ${{ matrix.package-manager == 'pnpm' && format('pnpm {0}', matrix.pnpm-version) || matrix.package-manager }} ${{ matrix.template }}
permissions:
contents: read
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
template: [next, vite, astro, start, react-router]
package-manager: [pnpm, bun, npm, yarn]
pnpm-version: [10.33.4, 11]
exclude:
- package-manager: bun
pnpm-version: 11
- package-manager: npm
pnpm-version: 11
- package-manager: yarn
pnpm-version: 11
env:
NEXT_PUBLIC_APP_URL: http://localhost:4000
NEXT_PUBLIC_V0_URL: https://v0.dev
REGISTRY_URL: http://localhost:4000/r
ROOT_PNPM_VERSION: 10.33.4
TEMPLATE_PNPM_VERSION: ${{ matrix.pnpm-version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
version: ${{ env.ROOT_PNPM_VERSION }}
run_install: false
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install Yarn
if: matrix.package-manager == 'yarn'
run: |
corepack enable
COREPACK_ENABLE_PROJECT_SPEC=0 corepack prepare yarn@4.12.0 --activate
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
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: Build packages
run: |
pnpm --filter=shadcn build
pnpm --filter=v4 registry:build
- name: Validate templates
env:
TEMPLATE: ${{ matrix.template }}
TEMPLATE_PACKAGE_MANAGER: ${{ matrix.package-manager }}
SHADCN_TEMPLATE_DIR: ${{ github.workspace }}/templates
run: |
set -euo pipefail
root_pnpm="$(command -v pnpm)"
validation_script="$RUNNER_TEMP/validate-templates.sh"
cat > "$validation_script" <<'BASH'
set -euo pipefail
bin_dir="$RUNNER_TEMP/template-pnpm-bin"
mkdir -p "$bin_dir"
cat > "$bin_dir/pnpm" <<'PNPM'
#!/usr/bin/env bash
exec npx -y "pnpm@${TEMPLATE_PNPM_VERSION}" "$@"
PNPM
chmod +x "$bin_dir/pnpm"
export PATH="$bin_dir:$PATH"
echo "Using template pnpm $(pnpm --version)"
cli="$GITHUB_WORKSPACE/packages/shadcn/dist/index.js"
template_root="$RUNNER_TEMP/generated-template-${TEMPLATE_PACKAGE_MANAGER}-${TEMPLATE}"
rm -rf "$template_root"
mkdir -p "$template_root"
modes=(app monorepo)
has_script() {
node -e "const pkg = require('./package.json'); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1"
}
run_script_if_present() {
local script="$1"
if has_script "$script"; then
pnpm run "$script"
else
echo "No $script script found; skipping."
fi
}
validate_non_pnpm_project() {
local package_manager="$1"
local project_path="$2"
local check_workspace_protocol="$3"
local is_monorepo="$4"
cd "$project_path"
test ! -f pnpm-workspace.yaml
test ! -f pnpm-lock.yaml
EXPECTED_PACKAGE_MANAGER="$package_manager" \
CHECK_WORKSPACE_PROTOCOL="$check_workspace_protocol" \
IS_MONOREPO="$is_monorepo" \
node <<'NODE'
const fs = require("node:fs")
const path = require("node:path")
const expectedPackageManager = process.env.EXPECTED_PACKAGE_MANAGER
const checkWorkspaceProtocol =
process.env.CHECK_WORKSPACE_PROTOCOL === "true"
const isMonorepo = process.env.IS_MONOREPO === "true"
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
if (isMonorepo) {
const workspaces = pkg.workspaces ?? []
if (!Array.isArray(workspaces)) {
throw new Error("Expected package.json workspaces to be an array.")
}
if (workspaces.length === 0) {
throw new Error("Expected package.json workspaces to have entries.")
}
for (const workspace of ["sharp", "unrs-resolver", "esbuild"]) {
if (workspaces.includes(workspace)) {
throw new Error(`Unexpected workspace entry: ${workspace}`)
}
}
if (!pkg.packageManager?.startsWith(`${expectedPackageManager}@`)) {
throw new Error(
`Expected packageManager to use ${expectedPackageManager}, got ${pkg.packageManager}`
)
}
} else {
if (pkg.workspaces !== undefined) {
throw new Error("Did not expect package.json workspaces for app template.")
}
if (pkg.packageManager !== undefined) {
throw new Error(
`Did not expect packageManager for app template, got ${pkg.packageManager}`
)
}
}
if (checkWorkspaceProtocol) {
const packageJsonFiles = []
function collectPackageJsonFiles(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name === "node_modules") {
continue
}
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
collectPackageJsonFiles(fullPath)
} else if (entry.name === "package.json") {
packageJsonFiles.push(fullPath)
}
}
}
collectPackageJsonFiles(process.cwd())
for (const file of packageJsonFiles) {
const json = fs.readFileSync(file, "utf8")
if (json.includes("workspace:")) {
throw new Error(`Unexpected workspace: protocol in ${file}`)
}
}
}
NODE
}
for mode in "${modes[@]}"; do
project="test-${TEMPLATE}-${mode}-${TEMPLATE_PACKAGE_MANAGER}"
project_path="$template_root/$project"
echo "::group::${TEMPLATE} ${mode} ${TEMPLATE_PACKAGE_MANAGER}"
args=(
init
--defaults
--name "$project"
--template "$TEMPLATE"
--cwd "$template_root"
--silent
)
if [ "$mode" = "monorepo" ]; then
args+=(--monorepo)
is_monorepo="true"
else
args+=(--no-monorepo)
is_monorepo="false"
fi
case "$TEMPLATE_PACKAGE_MANAGER" in
pnpm)
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
REGISTRY_URL="$REGISTRY_URL" \
npm_config_user_agent="pnpm/${TEMPLATE_PNPM_VERSION}" \
node "$cli" "${args[@]}"
cd "$project_path"
pnpm install --frozen-lockfile
run_script_if_present typecheck
run_script_if_present build
;;
bun)
(
cd "$template_root"
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
REGISTRY_URL="$REGISTRY_URL" \
npm_config_user_agent="bun/$(bun --version)" \
bunx --bun --package "$GITHUB_WORKSPACE/packages/shadcn" \
shadcn "${args[@]}"
)
validate_non_pnpm_project \
"bun" \
"$project_path" \
"false" \
"$is_monorepo"
;;
npm)
(
cd "$template_root"
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
REGISTRY_URL="$REGISTRY_URL" \
npm_config_user_agent="npm/$(npm --version)" \
npx --yes --package "$GITHUB_WORKSPACE/packages/shadcn" \
shadcn "${args[@]}"
)
validate_non_pnpm_project \
"npm" \
"$project_path" \
"true" \
"$is_monorepo"
;;
yarn)
(
cd "$template_root"
SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \
REGISTRY_URL="$REGISTRY_URL" \
COREPACK_ENABLE_PROJECT_SPEC=0 \
npm_config_user_agent="yarn/$(COREPACK_ENABLE_PROJECT_SPEC=0 yarn --version)" \
yarn dlx --package "$GITHUB_WORKSPACE/packages/shadcn" \
shadcn "${args[@]}"
)
validate_non_pnpm_project \
"yarn" \
"$project_path" \
"false" \
"$is_monorepo"
;;
esac
echo "::endgroup::"
done
BASH
"$root_pnpm" exec start-server-and-test \
"$root_pnpm v4:dev" \
http://localhost:4000 \
"bash $validation_script"

View File

@@ -25,7 +25,46 @@ jobs:
name: Install pnpm
id: pnpm-install
with:
version: 9.0.6
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 Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
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
@@ -42,4 +81,4 @@ jobs:
- name: Install dependencies
run: pnpm install
- run: pnpm test
- run: pnpm --filter=@shadcn/react test

View File

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

10
.gitignore vendored
View File

@@ -15,6 +15,7 @@ build
# misc
.DS_Store
.eslintcache
*.pem
# debug
@@ -42,4 +43,13 @@ tsconfig.tsbuildinfo
.notes
.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/(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/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/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

@@ -1,6 +1,8 @@
"use client"
import Script from "next/script"
import { type RegistryItem } from "shadcn/schema"
import {
Command,
CommandDialog,
@@ -9,10 +11,8 @@ import {
CommandInput,
CommandItem,
CommandList,
} from "@/examples/base/ui/command"
import { type RegistryItem } from "shadcn/schema"
import { useActionMenu } from "@/app/(create)/hooks/use-action-menu"
} from "@/styles/base-nova/ui/command"
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/(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/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/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/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/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

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

View File

@@ -1,15 +1,16 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui/button"
import { cn } from "@/lib/utils"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
import { Button } from "@/styles/base-nova/ui/button"
import { usePresetCode } from "@/app/(app)/(create)/hooks/use-design-system"
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
const presetCode = usePresetCode()
const [hasCopied, setHasCopied] = React.useState(false)
const label = hasCopied ? "Copied" : `--preset ${presetCode}`
React.useEffect(() => {
if (hasCopied) {
@@ -32,12 +33,13 @@ export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
<Button
variant="outline"
onClick={handleCopy}
title={label}
className={cn(
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
className
)}
>
<span>{hasCopied ? "Copied" : `--preset ${presetCode}`}</span>
<span className="block min-w-0 truncate">{label}</span>
</Button>
)
}

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

@@ -0,0 +1,131 @@
"use client"
import * as React from "react"
import dynamic from "next/dynamic"
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,
CardFooter,
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"
// 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({
itemsByBase,
}: {
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
}) {
const [params] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const anchorRef = React.useRef<HTMLDivElement | null>(null)
const availableThemes = React.useMemo(
() => getThemesForBaseColor(params.baseColor),
[params.baseColor]
)
return (
<Card
className="dark top-24 right-12 isolate z-10 max-h-full min-h-0 w-full self-start rounded-2xl bg-card/90 backdrop-blur-xl md:w-(--customizer-width)"
ref={anchorRef}
size="sm"
>
<CardHeader className="hidden items-center justify-between gap-2 border-b group-data-reversed/layout:flex-row-reverse md:flex">
<MainMenu />
</CardHeader>
<CardContent className="no-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden md:overflow-y-auto">
<FieldGroup className="flex-row gap-2.5 py-px **:data-[slot=field-separator]:-mx-4 **:data-[slot=field-separator]:w-auto md:flex-col md:gap-3.25">
<StylePicker
styles={STYLES}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<FieldSeparator className="hidden md:block" />
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<ThemePicker
themes={availableThemes}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<ChartColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<FieldSeparator className="hidden md:block" />
<FontPicker
label="Heading"
param="fontHeading"
fonts={FONT_HEADING_OPTIONS}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<FontPicker
label="Font"
param="font"
fonts={FONTS}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<FieldSeparator className="hidden md:block" />
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
<FieldSeparator className="hidden md:block" />
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
</FieldGroup>
</CardContent>
<CardFooter className="flex min-w-0 gap-2 md:flex-col md:rounded-b-none md:**:[button,a]:w-full">
<CopyPreset className="min-w-0 flex-1 md:flex-none" />
<OpenPreset
className="max-w-20 min-w-0 flex-1 sm:max-w-none md:flex-none"
label={isMobile ? "Open" : "Open Preset"}
/>
<RandomButton className="max-w-20 min-w-0 flex-1 sm:max-w-none md:flex-none" />
<ActionMenu itemsByBase={itemsByBase} />
<ResetDialog />
</CardFooter>
<CardFooter className="-mt-3 hidden min-w-0 gap-2 md:flex md:flex-col md:**:[button,a]:w-full">
<ProjectForm />
</CardFooter>
</Card>
)
}

View File

@@ -5,17 +5,24 @@ import * as React from "react"
import {
buildRegistryTheme,
DEFAULT_CONFIG,
POINTER_CURSOR_SELECTOR,
type DesignSystemConfig,
} from "@/registry/config"
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
import { FONTS } from "@/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/(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
const POINTER_CURSOR_CSS = `@layer base {
${POINTER_CURSOR_SELECTOR} {
cursor: pointer;
}
}
`
type RegistryThemeCssVars = NonNullable<
ReturnType<typeof buildRegistryTheme>["cssVars"]
@@ -44,14 +51,17 @@ function buildCssRule(selector: string, cssVars?: Record<string, string>) {
return `${selector} {\n${declarations}\n}\n`
}
function buildThemeCssText(cssVars: RegistryThemeCssVars) {
function buildThemeCssText(cssVars: RegistryThemeCssVars, pointer: boolean) {
return [
buildCssRule(":root", {
...(cssVars.theme ?? {}),
...(cssVars.light ?? {}),
}),
buildCssRule(".dark", cssVars.dark),
].join("\n")
pointer ? POINTER_CURSOR_CSS : "",
]
.filter(Boolean)
.join("\n")
}
export function DesignSystemProvider({
@@ -64,18 +74,38 @@ export function DesignSystemProvider({
history: "replace", // …or push updates into the iframe history.
})
const [isReady, setIsReady] = React.useState(false)
const { style, theme, font, baseColor, menuAccent, menuColor, radius } =
searchParams
const {
style,
theme,
font,
fontHeading,
baseColor,
chartColor,
menuAccent,
menuColor,
pointer,
radius,
} = searchParams
const effectiveRadius = style === "lyra" ? "none" : radius
const selectedFont = React.useMemo(
() => FONTS.find((fontOption) => fontOption.value === font),
[font]
)
const selectedHeadingFont = React.useMemo(() => {
if (fontHeading === "inherit" || fontHeading === font) {
return selectedFont
}
return FONTS.find((fontOption) => fontOption.value === fontHeading)
}, [font, fontHeading, selectedFont])
const initialFontSansRef = React.useRef<string | null>(null)
const initialFontHeadingRef = React.useRef<string | null>(null)
React.useEffect(() => {
initialFontSansRef.current =
document.documentElement.style.getPropertyValue("--font-sans")
initialFontHeadingRef.current =
document.documentElement.style.getPropertyValue("--font-heading")
return () => {
removeManagedBodyClasses(document.body)
@@ -86,10 +116,18 @@ export function DesignSystemProvider({
"--font-sans",
initialFontSansRef.current
)
return
} else {
document.documentElement.style.removeProperty("--font-sans")
}
document.documentElement.style.removeProperty("--font-sans")
if (initialFontHeadingRef.current) {
document.documentElement.style.setProperty(
"--font-heading",
initialFontHeadingRef.current
)
} else {
document.documentElement.style.removeProperty("--font-heading")
}
}
}, [])
@@ -103,8 +141,13 @@ export function DesignSystemProvider({
useIframeMessageListener("design-system-params", handleDesignSystemMessage)
React.useEffect(() => {
if (style === "lyra" && radius !== "none") {
if (style === "lyra" || (style === "sera" && radius !== "none")) {
setSearchParams({ radius: "none" })
return
}
if (style === "rhea" && radius === "large") {
setSearchParams({ radius: "default" })
}
}, [style, radius, setSearchParams])
@@ -124,12 +167,29 @@ export function DesignSystemProvider({
// Always set --font-sans for the preview so the selected font is visible.
// The font type (sans/serif/mono) is metadata for the CLI updater.
if (selectedFont) {
const fontFamily = selectedFont.font.style.fontFamily
document.documentElement.style.setProperty("--font-sans", fontFamily)
document.documentElement.style.setProperty(
"--font-sans",
selectedFont.font.style.fontFamily
)
}
if (selectedHeadingFont) {
document.documentElement.style.setProperty(
"--font-heading",
selectedHeadingFont.font.style.fontFamily
)
}
setIsReady(true)
}, [style, theme, font, baseColor, selectedFont])
}, [
style,
theme,
font,
fontHeading,
baseColor,
selectedFont,
selectedHeadingFont,
])
const registryTheme = React.useMemo(() => {
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
@@ -140,12 +200,13 @@ export function DesignSystemProvider({
...DEFAULT_CONFIG,
baseColor,
theme,
chartColor,
menuAccent,
radius: effectiveRadius,
}
return buildRegistryTheme(config)
}, [baseColor, theme, menuAccent, effectiveRadius])
}, [baseColor, theme, chartColor, menuAccent, effectiveRadius])
// Use useLayoutEffect for synchronous CSS var updates.
React.useLayoutEffect(() => {
@@ -163,8 +224,8 @@ export function DesignSystemProvider({
document.head.appendChild(styleElement)
}
styleElement.textContent = buildThemeCssText(registryTheme.cssVars)
}, [registryTheme])
styleElement.textContent = buildThemeCssText(registryTheme.cssVars, pointer)
}, [registryTheme, pointer])
// Handle menu color inversion by adding/removing dark class to elements with cn-menu-target.
// useLayoutEffect to apply classes synchronously before paint, avoiding flash.

View File

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

View File

@@ -1,11 +1,11 @@
"use client"
import Script from "next/script"
import { Button } from "@/examples/base/ui/button"
import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useHistory } from "@/app/(create)/hooks/use-history"
import { Button } from "@/styles/base-nova/ui/button"
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/(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/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/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/(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

@@ -2,11 +2,16 @@
import * as React from "react"
import Link from "next/link"
import { ChevronRightIcon } from "lucide-react"
import { type RegistryItem } from "shadcn/schema"
import { cn } from "@/lib/utils"
import { type Base } from "@/registry/bases"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/examples/base/ui/collapsible"
} from "@/styles/base-nova/ui/collapsible"
import {
Sidebar,
SidebarContent,
@@ -15,14 +20,9 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/examples/base/ui/sidebar"
import { ChevronRightIcon } from "lucide-react"
import { type RegistryItem } from "shadcn/schema"
import { cn } from "@/lib/utils"
import { type Base } from "@/registry/bases"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(create)/lib/utils"
} from "@/styles/base-nova/ui/sidebar"
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

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

View File

@@ -1,11 +1,11 @@
"use client"
import * as React from "react"
import { type Button } from "@/examples/base/ui/button"
import { Menu09Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { type Button } from "@/styles/base-nova/ui/button"
import {
Picker,
PickerContent,
@@ -14,12 +14,13 @@ import {
PickerSeparator,
PickerShortcut,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useActionMenuTrigger } from "@/app/(create)/hooks/use-action-menu"
import { useHistory } from "@/app/(create)/hooks/use-history"
import { useRandom } from "@/app/(create)/hooks/use-random"
import { useReset } from "@/app/(create)/hooks/use-reset"
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
} 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/
@@ -27,6 +28,7 @@ export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
const [isMac, setIsMac] = React.useState(false)
const { canGoBack, canGoForward, goBack, goForward } = useHistory()
const { openActionMenu } = useActionMenuTrigger()
const { openPreset } = useOpenPresetTrigger()
const { randomize } = useRandom()
const { toggleTheme } = useThemeToggle()
const { setShowResetDialog } = useReset()
@@ -55,6 +57,9 @@ export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
Navigate...
<PickerShortcut>{isMac ? "⌘P" : "Ctrl+P"}</PickerShortcut>
</PickerItem>
<PickerItem onClick={openPreset}>
Open Preset... <PickerShortcut>O</PickerShortcut>
</PickerItem>
<PickerItem onClick={randomize}>
Shuffle <PickerShortcut>R</PickerShortcut>
</PickerItem>
@@ -73,7 +78,7 @@ export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
</PickerItem>
<PickerSeparator />
<PickerItem onClick={() => setShowResetDialog(true)}>
Reset
Reset <PickerShortcut>R</PickerShortcut>
</PickerItem>
</PickerGroup>
</PickerContent>

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/(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/(create)/components/picker"
} from "@/app/(app)/(create)/components/picker"
import {
isTranslucentMenuColor,
useDesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
} from "@/app/(app)/(create)/lib/search-params"
type ColorChoice = "default" | "inverted"
type SurfaceChoice = "solid" | "translucent"
@@ -104,7 +104,7 @@ export function MenuColorPicker({
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Menu</div>
<div className="text-sm font-medium text-foreground">
<div className="line-clamp-1 max-w-[80%] truncate text-sm font-medium text-foreground">
{currentMenu?.label}
</div>
</div>

View File

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

View File

@@ -0,0 +1,203 @@
"use client"
import * as React from "react"
import Script from "next/script"
import { cn } from "@/lib/utils"
import { useIsMobile } from "@/hooks/use-mobile"
import { Button } from "@/styles/base-nova/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/styles/base-nova/ui/dialog"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-nova/ui/drawer"
import { Field, FieldContent, FieldLabel } from "@/styles/base-nova/ui/field"
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"
const PRESET_EXAMPLE = "b2D0wqNxT"
const PRESET_TITLE = "Open Preset"
const PRESET_DESCRIPTION = "Paste a preset code to load a saved configuration."
export function OpenPreset({
className,
label = "Open Preset",
}: React.ComponentProps<typeof Button> & {
label?: string
}) {
const [input, setInput] = React.useState("")
const [, setParams] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const { open, setOpen } = useOpenPreset()
const nextPreset = React.useMemo(() => parsePresetInput(input), [input])
const isInvalid = input.trim().length > 0 && nextPreset === null
const handleOpenChange = React.useCallback(
(nextOpen: boolean) => {
setOpen(nextOpen)
if (!nextOpen) {
setInput("")
}
},
[setOpen]
)
const handleSubmit = React.useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (!nextPreset) {
return
}
setParams({ preset: nextPreset })
handleOpenChange(false)
},
[handleOpenChange, nextPreset, setParams]
)
const triggerClassName = cn(
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
className
)
const desktopTrigger = (
<Button variant="outline" className={triggerClassName} />
)
const fields = (
<Field data-invalid={isInvalid || undefined}>
<FieldLabel htmlFor="preset-code" className="sr-only">
Preset code
</FieldLabel>
<FieldContent>
<Input
id="preset-code"
value={input}
onChange={(event) => setInput(event.target.value)}
placeholder={`${PRESET_EXAMPLE} or --preset ${PRESET_EXAMPLE}`}
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
aria-invalid={isInvalid}
className="h-10 md:h-8"
/>
</FieldContent>
</Field>
)
if (isMobile) {
return (
<Drawer open={open} onOpenChange={handleOpenChange}>
<DrawerTrigger
render={<Button variant="outline" className={triggerClassName} />}
>
{label}
</DrawerTrigger>
<DrawerContent className="dark rounded-t-2xl!">
<DrawerHeader>
<DrawerTitle className="text-xl">{PRESET_TITLE}</DrawerTitle>
<DrawerDescription>{PRESET_DESCRIPTION}</DrawerDescription>
</DrawerHeader>
<form onSubmit={handleSubmit}>
<div className="px-4 py-2">{fields}</div>
<DrawerFooter>
<Button type="submit" className="h-10" disabled={!nextPreset}>
Open
</Button>
<DrawerClose
render={
<Button variant="outline" type="button" className="h-10" />
}
>
Cancel
</DrawerClose>
</DrawerFooter>
</form>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger render={desktopTrigger}>{label}</DialogTrigger>
<DialogContent className="dark">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>{PRESET_TITLE}</DialogTitle>
<DialogDescription>{PRESET_DESCRIPTION}</DialogDescription>
</DialogHeader>
<div className="py-4">{fields}</div>
<DialogFooter>
<DialogClose render={<Button variant="outline" type="button" />}>
Cancel
</DialogClose>
<Button type="submit" disabled={!nextPreset}>
Open
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
export function OpenPresetScript() {
return (
<Script
id="open-preset-listener"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
(function() {
// Forward O key.
document.addEventListener('keydown', function(e) {
if (e.key === 'o' && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return;
}
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: '${OPEN_PRESET_FORWARD_TYPE}',
key: e.key
}, '*');
}
}
});
})();
`,
}}
/>
)
}

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/(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} />
@@ -19,7 +19,7 @@ function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
<MenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
className={cn(
"relative w-40 shrink-0 touch-manipulation rounded-xl p-3 ring-1 ring-foreground/10 select-none hover:bg-muted focus-visible:ring-foreground/50 focus-visible:outline-none disabled:opacity-50 data-popup-open:bg-muted md:w-full md:rounded-lg md:px-2.5 md:py-2",
"relative w-36 shrink-0 touch-manipulation rounded-xl p-3 ring-1 ring-foreground/10 select-none hover:bg-muted focus-visible:ring-foreground/50 focus-visible:outline-none disabled:opacity-50 data-popup-open:bg-muted md:w-full md:rounded-lg md:px-2.5 md:py-2",
className
)}
{...props}

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/(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/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function PresetPicker({
presets,
@@ -31,8 +31,10 @@ export function PresetPicker({
preset.style === params.style &&
preset.baseColor === params.baseColor &&
preset.theme === params.theme &&
preset.chartColor === params.chartColor &&
preset.iconLibrary === params.iconLibrary &&
preset.font === params.font &&
preset.fontHeading === params.fontHeading &&
preset.menuAccent === params.menuAccent &&
preset.menuColor === params.menuColor &&
preset.radius === params.radius
@@ -43,8 +45,10 @@ export function PresetPicker({
params.style,
params.baseColor,
params.theme,
params.chartColor,
params.iconLibrary,
params.font,
params.fontHeading,
params.menuAccent,
params.menuColor,
params.radius,
@@ -67,8 +71,10 @@ export function PresetPicker({
style: preset.style,
baseColor: preset.baseColor,
theme: preset.theme,
chartColor: preset.chartColor,
iconLibrary: preset.iconLibrary,
font: preset.font,
fontHeading: preset.fontHeading,
menuAccent: preset.menuAccent,
menuColor: preset.menuColor,
radius: preset.radius,
@@ -106,13 +112,6 @@ export function PresetPicker({
closeOnClick={isMobile}
>
<div className="flex items-center gap-2">
{style?.icon && (
<div className="flex size-4 shrink-0 items-center justify-center">
{React.cloneElement(style.icon, {
className: "size-4",
})}
</div>
)}
{preset.description}
</div>
</PickerRadioItem>

View File

@@ -0,0 +1,37 @@
"use client"
import { Button } from "@/registry/new-york-v4/ui/button"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
const PREVIEW_ITEMS = [
{ label: "01", value: "preview-02" },
{ label: "02", value: "preview" },
]
export function PreviewSwitcher() {
const [params, setParams] = useDesignSystemSearchParams()
const isPreview =
params.item === "preview" || params.item.startsWith("preview-0")
if (!isPreview) {
return null
}
return (
<div className="dark absolute right-3 bottom-3 z-20 flex items-center gap-1 rounded-xl bg-card/90 p-1 shadow-xl backdrop-blur-xl">
{PREVIEW_ITEMS.map((item) => (
<Button
key={item.value}
variant="ghost"
size="sm"
data-active={params.item === item.value}
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({ item: item.value })}
>
{item.label}
</Button>
))}
</div>
)
}

View File

@@ -0,0 +1,168 @@
"use client"
import * as React from "react"
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"
import {
serializeDesignSystemSearchParams,
useDesignSystemSearchParams,
} from "@/app/(app)/(create)/lib/search-params"
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
export function Preview() {
const [params] = useDesignSystemSearchParams()
const iframeRef = React.useRef<HTMLIFrameElement>(null)
React.useEffect(() => {
const iframe = iframeRef.current
if (!iframe) {
return
}
const sendParams = () => {
sendToIframe(iframe, "design-system-params", params)
}
if (iframe.contentWindow) {
sendParams()
}
iframe.addEventListener("load", sendParams)
return () => {
iframe.removeEventListener("load", sendParams)
}
}, [params])
React.useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const iframeWindow = iframeRef.current?.contentWindow
if (
!iframeWindow ||
event.origin !== window.location.origin ||
event.source !== iframeWindow ||
!event.data ||
typeof event.data !== "object"
) {
return
}
const type = event.data.type
if (type === CMD_K_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "k",
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === RANDOMIZE_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "r",
bubbles: true,
cancelable: true,
})
)
} else if (type === OPEN_PRESET_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "o",
bubbles: true,
cancelable: true,
})
)
} else if (type === UNDO_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "z",
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === REDO_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "z",
shiftKey: true,
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
} else if (type === RESET_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: "R",
shiftKey: true,
bubbles: true,
cancelable: true,
})
)
} else if (type === DARK_MODE_FORWARD_TYPE) {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "d",
bubbles: true,
cancelable: true,
})
)
}
}
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
}, [])
const iframeSrc = React.useMemo(() => {
// The iframe src needs to include the serialized design system params
// for the initial load, but not be reactive to them as it would cause
// full-iframe reloads on every param change (flashes & loss of state).
// Further updates of the search params will be sent to the iframe
// via a postMessage channel, for it to sync its own history onto the host's.
return serializeDesignSystemSearchParams(
`/preview/${params.base}/${params.item}`,
params
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params.base, params.item])
return (
<div className="relative flex flex-1 flex-col justify-center overflow-hidden rounded-2xl ring ring-foreground/10 md:ring-muted dark:ring-foreground/10">
<div className="relative z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden">
<div className="absolute inset-0 bg-muted dark:bg-muted/30" />
<iframe
key={params.base + params.item}
ref={iframeRef}
src={iframeSrc}
className="z-10 size-full flex-1"
title="Preview"
/>
</div>
<CreateDevtools />
<PreviewSwitcher />
</div>
)
}

View File

@@ -0,0 +1,726 @@
"use client"
import * as React from "react"
import {
Copy01Icon,
Globe02Icon,
HandPointingRight04Icon,
Tick02Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import {
BASES,
buildThemeForPreset,
DEFAULT_CONFIG,
type BaseName,
type DesignSystemConfig,
} from "@/registry/config"
import { Button } from "@/styles/base-nova/ui/button"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "@/styles/base-nova/ui/dialog"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/styles/base-nova/ui/field"
import { RadioGroup, RadioGroupItem } from "@/styles/base-nova/ui/radio-group"
import { Switch } from "@/styles/base-nova/ui/switch"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/styles/base-nova/ui/tabs"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/styles/base-nova/ui/toggle-group"
import { usePresetCode } from "@/app/(app)/(create)/hooks/use-design-system"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(app)/(create)/lib/search-params"
import {
getFramework,
getTemplateValue,
NO_MONOREPO_FRAMEWORKS,
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>'
const ORIGIN = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000"
const IS_LOCAL_DEV = ORIGIN.includes("localhost")
const SHADCN_VERSION = process.env.NEXT_PUBLIC_RC ? "@rc" : "@latest"
const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"] as const
type PackageManager = (typeof PACKAGE_MANAGERS)[number]
const APPLY_MODES = [
{
value: "full",
title: "Full preset",
description:
"Everything from the preset, including components, theme, and fonts.",
flag: null,
label: "full preset",
},
{
value: "theme",
title: "Theme only",
description:
"Theme tokens only, like colors, radii, and shadows. Components stay as they are.",
flag: "--only theme",
label: "--only theme",
},
{
value: "font",
title: "Fonts only",
description:
"Only preset fonts for body and headings. Components stay as they are.",
flag: "--only font",
label: "--only font",
},
] as const
type ApplyMode = (typeof APPLY_MODES)[number]["value"]
type ProjectFormTab = "new-project" | "existing-project" | "theme"
type CopyTarget = "command" | "apply" | "theme"
type ThemeCssVars = NonNullable<
ReturnType<typeof buildThemeForPreset>["cssVars"]
>
function formatCssVarsRule(selector: string, cssVars?: Record<string, string>) {
const declarations = Object.entries(cssVars ?? {})
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
return `${selector} {\n${declarations}\n}`
}
function formatThemeCss(cssVars: ThemeCssVars) {
return [
formatCssVarsRule(":root", cssVars.light),
formatCssVarsRule(".dark", cssVars.dark),
].join("\n\n")
}
export function ProjectForm({
className,
}: React.ComponentProps<typeof Button>) {
const [open, setOpen] = React.useState(false)
const [params, setParams] = useDesignSystemSearchParams()
const presetCode = usePresetCode()
const [config, setConfig] = useConfig()
const [copiedTarget, setCopiedTarget] = React.useState<CopyTarget | null>(
null
)
const [applyMode, setApplyMode] = React.useState<ApplyMode>("full")
const [activeTab, setActiveTab] =
React.useState<ProjectFormTab>("new-project")
const packageManager = (config.packageManager || "pnpm") as PackageManager
const framework = React.useMemo(
() => getFramework(params.template ?? "next"),
[params.template]
)
const isMonorepo = React.useMemo(
() => params.template?.endsWith("-monorepo") ?? false,
[params.template]
)
const hasMonorepo = !NO_MONOREPO_FRAMEWORKS.includes(
framework as (typeof NO_MONOREPO_FRAMEWORKS)[number]
)
const commands = React.useMemo(() => {
const presetFlag = ` --preset ${presetCode}`
const baseFlag =
params.base !== DEFAULT_CONFIG.base ? ` --base ${params.base}` : ""
const templateFlag = ` --template ${framework}`
const monorepoFlag = isMonorepo ? " --monorepo" : ""
const rtlFlag = params.rtl ? " --rtl" : ""
const pointerFlag = params.pointer ? " --pointer" : ""
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}${pointerFlag}`
return IS_LOCAL_DEV
? {
pnpm: `shadcn init${flags}`,
npm: `shadcn init${flags}`,
yarn: `shadcn init${flags}`,
bun: `shadcn init${flags}`,
}
: {
pnpm: `pnpm dlx shadcn${SHADCN_VERSION} init${flags}`,
npm: `npx shadcn${SHADCN_VERSION} init${flags}`,
yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`,
bun: `bunx --bun shadcn${SHADCN_VERSION} init${flags}`,
}
}, [
framework,
isMonorepo,
params.base,
params.pointer,
params.rtl,
presetCode,
])
const command = commands[packageManager]
const applyCommands = React.useMemo(() => {
const presetFlag = ` --preset ${presetCode}`
const onlyFlag =
applyMode === "theme"
? " --only theme"
: applyMode === "font"
? " --only font"
: ""
const flags = `${presetFlag}${onlyFlag}`
return IS_LOCAL_DEV
? {
pnpm: `shadcn apply${flags}`,
npm: `shadcn apply${flags}`,
yarn: `shadcn apply${flags}`,
bun: `shadcn apply${flags}`,
}
: {
pnpm: `pnpm dlx shadcn${SHADCN_VERSION} apply${flags}`,
npm: `npx shadcn${SHADCN_VERSION} apply${flags}`,
yarn: `yarn dlx shadcn${SHADCN_VERSION} apply${flags}`,
bun: `bunx --bun shadcn${SHADCN_VERSION} apply${flags}`,
}
}, [applyMode, presetCode])
const applyCommand = applyCommands[packageManager]
const themeConfig = React.useMemo<DesignSystemConfig>(() => {
const isRadiusLocked = params.style === "lyra" || params.style === "sera"
return {
...DEFAULT_CONFIG,
base: params.base,
style: params.style,
baseColor: params.baseColor,
theme: params.theme,
chartColor: params.chartColor,
iconLibrary: params.iconLibrary,
font: params.font,
fontHeading: params.fontHeading,
menuAccent: params.menuAccent,
menuColor: params.menuColor,
radius: isRadiusLocked ? "none" : params.radius,
template: params.template,
rtl: params.rtl,
pointer: params.pointer,
}
}, [
params.base,
params.baseColor,
params.chartColor,
params.font,
params.fontHeading,
params.iconLibrary,
params.menuAccent,
params.menuColor,
params.pointer,
params.radius,
params.rtl,
params.style,
params.template,
params.theme,
])
const themeCss = React.useMemo(() => {
const theme = buildThemeForPreset(themeConfig)
if (!theme.cssVars) {
return ""
}
return formatThemeCss(theme.cssVars)
}, [themeConfig])
React.useEffect(() => {
if (copiedTarget) {
const timer = setTimeout(() => setCopiedTarget(null), 2000)
return () => clearTimeout(timer)
}
}, [copiedTarget])
const handleCopy = React.useCallback(() => {
const properties: Record<string, string> = {
command,
}
if (params.template) {
properties.template = params.template
}
copyToClipboardWithMeta(command, {
name: "copy_npm_command",
properties,
})
setCopiedTarget("command")
}, [command, params.template])
const handleCopyApply = React.useCallback(() => {
copyToClipboardWithMeta(applyCommand, {
name: "copy_apply_command",
properties: {
command: applyCommand,
applyMode,
},
})
setCopiedTarget("apply")
}, [applyCommand, applyMode])
const handleCopyTheme = React.useCallback(() => {
copyToClipboardWithMeta(themeCss, {
name: "copy_theme_code",
properties: {
preset: presetCode,
baseColor: params.baseColor,
theme: params.theme,
format: "css",
},
})
setCopiedTarget("theme")
}, [params.baseColor, params.theme, presetCode, themeCss])
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button className={cn(className)} />}>
Get Code
</DialogTrigger>
<DialogContent className="dark top-[64px] no-scrollbar flex max-h-[calc(100svh-2rem)] translate-y-0 flex-col rounded-2xl p-0 shadow-xl **:data-[slot=dialog-close]:top-4.5 **:data-[slot=dialog-close]:right-4 **:data-[slot=field-separator]:h-2 sm:max-w-md">
<div className="flex min-w-0 flex-1 flex-col gap-0 overflow-hidden rounded-2xl">
<DialogHeader className="border-b px-6 py-5">
<ToggleGroup
value={[activeTab]}
onValueChange={(values) =>
setActiveTab((values[0] as typeof activeTab) ?? "new-project")
}
aria-label="Project type"
spacing={2}
className="**:data-[slot=toggle-group-item]:data-pressed:bg-neutral-700/70"
>
<ToggleGroupItem value="new-project">New Project</ToggleGroupItem>
<ToggleGroupItem value="existing-project">
Existing Project
</ToggleGroupItem>
<ToggleGroupItem value="theme">Theme</ToggleGroupItem>
</ToggleGroup>
</DialogHeader>
{activeTab === "new-project" && (
<div className="no-scrollbar overflow-y-auto">
<FieldGroup className="px-6 py-4">
<Field className="gap-3">
<FieldLabel>Template</FieldLabel>
<TemplateGrid
template={params.template}
setParams={setParams}
/>
</Field>
<FieldSeparator className="-mx-6" />
<Field>
<FieldLabel>Base</FieldLabel>
<BaseGrid base={params.base} setParams={setParams} />
</Field>
<FieldSeparator className="-mx-6" />
<FieldSet>
<FieldLegend variant="label" className="sr-only">
Options
</FieldLegend>
<Field orientation="horizontal">
<FieldLabel htmlFor="pointer">
<HugeiconsIcon
icon={HandPointingRight04Icon}
className="size-4 -rotate-90"
/>
Use pointer on buttons
</FieldLabel>
<Switch
id="pointer"
checked={params.pointer}
onCheckedChange={(checked) =>
setParams({ pointer: checked === true })
}
/>
</Field>
<FieldSeparator className="-mx-6" />
<Field
orientation="horizontal"
data-disabled={hasMonorepo ? undefined : "true"}
>
<FieldLabel htmlFor="monorepo">
<span
className="size-4 text-neutral-100 [&_svg]:size-4 [&_svg]:fill-current"
dangerouslySetInnerHTML={{
__html: TURBOREPO_LOGO,
}}
/>
Create a monorepo
</FieldLabel>
<Switch
id="monorepo"
checked={params.template?.endsWith("-monorepo") ?? false}
disabled={!hasMonorepo}
onCheckedChange={(checked) => {
const framework = getFramework(
params.template ?? "next"
)
setParams({
template: getTemplateValue(
framework,
checked === true
) as typeof params.template,
})
}}
/>
</Field>
<FieldSeparator className="-mx-6" />
<Field orientation="horizontal">
<FieldLabel htmlFor="rtl">
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
Enable RTL support
</FieldLabel>
<Switch
id="rtl"
checked={params.rtl}
onCheckedChange={(checked) =>
setParams({ rtl: checked === true })
}
/>
</Field>
</FieldSet>
</FieldGroup>
<DialogFooter className="m-0 min-w-0 p-6">
<div className="flex w-full min-w-0 flex-col gap-3">
<Tabs
value={packageManager}
onValueChange={(value) => {
setConfig((prev) => ({
...prev,
packageManager: value as PackageManager,
}))
}}
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
>
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
<TabsList className="bg-transparent font-mono">
{PACKAGE_MANAGERS.map((manager) => {
return (
<TabsTrigger
key={manager}
value={manager}
className="py-0 leading-none data-[state=active]:shadow-none"
>
{manager}
</TabsTrigger>
)
})}
</TabsList>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto"
onClick={handleCopy}
>
{copiedTarget === "command" ? (
<HugeiconsIcon icon={Tick02Icon} />
) : (
<HugeiconsIcon icon={Copy01Icon} />
)}
<span className="sr-only">Copy command</span>
</Button>
</div>
{Object.entries(commands).map(([key, cmd]) => {
return (
<TabsContent key={key} value={key}>
<div className="relative overflow-hidden border-t bg-popover p-3">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">
{cmd}
</code>
</div>
</div>
</TabsContent>
)
})}
</Tabs>
<Button onClick={handleCopy} className="h-9 w-full">
{copiedTarget === "command" ? "Copied" : "Copy Command"}
</Button>
</div>
</DialogFooter>
</div>
)}
{activeTab === "existing-project" && (
<div className="no-scrollbar overflow-y-auto">
<FieldGroup className="px-6 py-4">
<FieldSet className="gap-3">
<FieldLegend variant="label">Apply Preset</FieldLegend>
<FieldDescription>
Pick which parts of the preset to apply.
</FieldDescription>
<ApplyModeGrid mode={applyMode} setMode={setApplyMode} />
</FieldSet>
</FieldGroup>
<DialogFooter className="m-0 min-w-0 p-6">
<div className="flex w-full min-w-0 flex-col gap-3">
<Tabs
value={packageManager}
onValueChange={(value) => {
setConfig((prev) => ({
...prev,
packageManager: value as PackageManager,
}))
}}
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
>
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
<TabsList className="bg-transparent font-mono">
{PACKAGE_MANAGERS.map((manager) => {
return (
<TabsTrigger
key={manager}
value={manager}
className="py-0 leading-none data-[state=active]:shadow-none"
>
{manager}
</TabsTrigger>
)
})}
</TabsList>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto"
onClick={handleCopyApply}
>
{copiedTarget === "apply" ? (
<HugeiconsIcon icon={Tick02Icon} />
) : (
<HugeiconsIcon icon={Copy01Icon} />
)}
<span className="sr-only">Copy command</span>
</Button>
</div>
{Object.entries(applyCommands).map(([key, cmd]) => {
return (
<TabsContent key={key} value={key}>
<div className="relative overflow-hidden border-t bg-popover p-3">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">
{cmd}
</code>
</div>
</div>
</TabsContent>
)
})}
</Tabs>
<Button onClick={handleCopyApply} className="h-9 w-full">
{copiedTarget === "apply" ? "Copied" : "Copy Command"}
</Button>
</div>
</DialogFooter>
</div>
)}
{activeTab === "theme" && (
<div className="no-scrollbar overflow-y-auto">
<FieldGroup className="min-w-0 px-6 py-4">
<FieldSet className="min-w-0 gap-3">
<FieldLegend variant="label">Theme Tokens</FieldLegend>
<FieldDescription>
Copy the CSS variables for this preset.
</FieldDescription>
<div className="w-full min-w-0 overflow-hidden rounded-xl border-0 ring-1 ring-border">
<div className="flex items-center gap-2 py-1 pr-1.5 pl-3">
<div className="min-w-0 truncate font-mono text-sm text-muted-foreground">
globals.css
</div>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto"
onClick={handleCopyTheme}
>
{copiedTarget === "theme" ? (
<HugeiconsIcon icon={Tick02Icon} />
) : (
<HugeiconsIcon icon={Copy01Icon} />
)}
<span className="sr-only">Copy theme</span>
</Button>
</div>
<div className="relative no-scrollbar max-h-[45svh] overflow-auto border-t bg-popover p-3">
<pre className="min-w-max font-mono leading-normal whitespace-pre">
<code>{themeCss}</code>
</pre>
</div>
</div>
</FieldSet>
</FieldGroup>
<DialogFooter className="m-0 min-w-0 p-6">
<Button onClick={handleCopyTheme} className="h-9 w-full">
{copiedTarget === "theme" ? "Copied" : "Copy Theme"}
</Button>
</DialogFooter>
</div>
)}
</div>
</DialogContent>
</Dialog>
)
}
const TemplateGrid = React.memo(function TemplateGrid({
template,
setParams,
}: {
template: DesignSystemSearchParams["template"]
setParams: ReturnType<typeof useDesignSystemSearchParams>[1]
}) {
const isMonorepo = template?.endsWith("-monorepo") ?? false
const framework = getFramework(template ?? "next")
const handleTemplateChange = React.useCallback(
(value: string) => {
setParams({
template: getTemplateValue(
value,
isMonorepo
) as DesignSystemSearchParams["template"],
})
},
[isMonorepo, setParams]
)
return (
<RadioGroup
value={framework}
onValueChange={handleTemplateChange}
className="grid grid-cols-2 gap-2"
>
{TEMPLATES.map((item) => (
<FieldLabel
key={item.value}
htmlFor={`template-${item.value}`}
className="block w-full"
>
<Field
orientation="horizontal"
className="w-full rounded-md transition-colors duration-150 hover:bg-neutral-700/45"
>
<FieldContent className="flex flex-row items-center gap-2 px-2.5 py-1.5">
<div
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
dangerouslySetInnerHTML={{
__html: item.logo,
}}
></div>
<FieldTitle>{item.title}</FieldTitle>
</FieldContent>
<RadioGroupItem
value={item.value}
id={`template-${item.value}`}
className="sr-only absolute"
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
)
})
const ApplyModeGrid = React.memo(function ApplyModeGrid({
mode,
setMode,
}: {
mode: ApplyMode
setMode: (mode: ApplyMode) => void
}) {
return (
<RadioGroup
value={mode}
onValueChange={(value) => setMode(value as ApplyMode)}
aria-label="Apply"
>
{APPLY_MODES.map((option) => (
<FieldLabel key={option.value} htmlFor={`apply-${option.value}`}>
<Field orientation="horizontal">
<RadioGroupItem value={option.value} id={`apply-${option.value}`} />
<FieldContent>
<FieldTitle>{option.title}</FieldTitle>
<FieldDescription>{option.description}</FieldDescription>
</FieldContent>
</Field>
</FieldLabel>
))}
</RadioGroup>
)
})
const BaseGrid = React.memo(function BaseGrid({
base,
setParams,
}: {
base: DesignSystemSearchParams["base"]
setParams: ReturnType<typeof useDesignSystemSearchParams>[1]
}) {
const handleBaseChange = React.useCallback(
(value: string) => {
setParams({ base: value as BaseName })
},
[setParams]
)
return (
<RadioGroup
value={base}
onValueChange={handleBaseChange}
aria-label="Base"
className="grid grid-cols-2 gap-2"
>
{BASES.map((item) => (
<FieldLabel
key={item.name}
htmlFor={`base-${item.name}`}
className="block w-full"
>
<Field
orientation="horizontal"
className="w-full rounded-md transition-colors duration-150 hover:bg-neutral-700/45"
>
<FieldContent className="flex flex-row items-center gap-2 px-2.5 py-1.5">
<div
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
dangerouslySetInnerHTML={{
__html: item.meta?.logo ?? "",
}}
/>
<FieldTitle>{item.title}</FieldTitle>
</FieldContent>
<RadioGroupItem
value={item.name}
id={`base-${item.name}`}
className="sr-only absolute"
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
)
})

View File

@@ -3,7 +3,7 @@
import * as React from "react"
import { RADII, type RadiusValue } from "@/registry/config"
import { LockButton } from "@/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/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
} from "@/app/(app)/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
export function RadiusPicker({
isMobile,
@@ -23,7 +23,7 @@ export function RadiusPicker({
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const isRadiusLocked = params.style === "lyra"
const isRadiusLocked = params.style === "lyra" || params.style === "sera"
const selectedRadiusName = isRadiusLocked ? "none" : params.radius
const currentRadius = RADII.find(
@@ -93,6 +93,7 @@ export function RadiusPicker({
key={radius.name}
value={radius.name}
closeOnClick={isMobile}
disabled={params.style === "rhea" && radius.name === "large"}
>
{radius.label}
</PickerRadioItem>

View File

@@ -1,12 +1,13 @@
"use client"
import Script from "next/script"
import { Button } from "@/examples/base/ui/button"
import { DiceFaces05Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { useRandom } from "@/app/(create)/hooks/use-random"
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"
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
@@ -27,7 +28,7 @@ export function RandomButton({
)}
{...props}
>
<span className="w-full text-center font-medium">Shuffle</span>
<span className="w-full truncate text-center font-medium">Shuffle</span>
</Button>
)
}
@@ -40,7 +41,7 @@ export function RandomizeScript() {
dangerouslySetInnerHTML={{
__html: `
(function() {
// Forward R key
// Forward r key (shuffle) and Shift+R (reset).
document.addEventListener('keydown', function(e) {
if ((e.key === 'r' || e.key === 'R') && !e.metaKey && !e.ctrlKey) {
if (
@@ -53,8 +54,11 @@ export function RandomizeScript() {
}
e.preventDefault();
if (window.parent && window.parent !== window) {
var type = e.shiftKey
? '${RESET_FORWARD_TYPE}'
: '${RANDOMIZE_FORWARD_TYPE}';
window.parent.postMessage({
type: '${RANDOMIZE_FORWARD_TYPE}',
type: type,
key: e.key
}, '*');
}

View File

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

View File

@@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import { Share03Icon, Tick02Icon } from "@hugeicons/core-free-icons"
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"
export function ShareButton() {
const [params] = useDesignSystemSearchParams()
const presetCode = usePresetCode()
const [hasCopied, setHasCopied] = React.useState(false)
const shareUrl = React.useMemo(() => {
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
const searchParams = new URLSearchParams({
preset: presetCode,
item: params.item,
})
if (params.pointer) {
searchParams.set("pointer", "true")
}
return `${origin}/create?${searchParams.toString()}`
}, [params.item, params.pointer, presetCode])
React.useEffect(() => {
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
const handleCopy = React.useCallback(() => {
copyToClipboardWithMeta(shareUrl, {
name: "copy_create_share_url",
properties: {
url: shareUrl,
},
})
setHasCopied(true)
}, [shareUrl])
return (
<Button variant="outline" className="hidden md:flex" onClick={handleCopy}>
{hasCopied ? (
<HugeiconsIcon
icon={Tick02Icon}
strokeWidth={2}
data-icon="inline-start"
/>
) : (
<HugeiconsIcon
icon={Share03Icon}
strokeWidth={2}
data-icon="inline-start"
/>
)}
Share
</Button>
)
}

View File

@@ -3,7 +3,7 @@
import * as React from "react"
import { type Style, type StyleName } from "@/registry/config"
import { LockButton } from "@/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/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/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/(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/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/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

@@ -0,0 +1,57 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { useIsMobile } from "@/hooks/use-mobile"
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"
export function V0Button({ className }: { className?: string }) {
const [params] = useDesignSystemSearchParams()
const isMobile = useIsMobile()
const isMounted = useMounted()
const url = React.useMemo(() => {
const searchParams = new URLSearchParams()
if (params.preset) {
searchParams.set("preset", params.preset)
}
searchParams.set("base", params.base)
return `${process.env.NEXT_PUBLIC_APP_URL}/init/v0?${searchParams.toString()}`
}, [params.preset, params.base])
const title = React.useMemo(() => {
return params.base && params.style
? `New ${params.base}-${params.style} project`
: "New Project"
}, [params.base, params.style])
if (!isMounted) {
return <Skeleton className="h-8 w-24 rounded-lg" />
}
return (
<Button
nativeButton={false}
role="link"
variant={isMobile ? "default" : "outline"}
className={cn("h-[31px] gap-1 rounded-lg", className)}
render={
<a
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${url}&title=${title}`}
target="_blank"
/>
}
>
<span>Open in</span>
<Icons.v0 className="size-5" data-icon="inline-end" />
</Button>
)
}

View File

@@ -1,7 +1,9 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui/button"
import { Icons } from "@/components/icons"
import { Button } from "@/styles/base-nova/ui/button"
import {
Dialog,
DialogClose,
@@ -10,9 +12,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/examples/base/ui/dialog"
import { Icons } from "@/components/icons"
} from "@/styles/base-nova/ui/dialog"
const STORAGE_KEY = "shadcn-create-welcome-dialog"

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

@@ -0,0 +1,74 @@
import { Suspense } from "react"
import { type Metadata } from "next"
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"
// Only shown on first visit (checks localStorage).
const WelcomeDialog = dynamic(() =>
import("@/app/(app)/(create)/components/welcome-dialog").then(
(m) => m.WelcomeDialog
)
)
export const metadata: Metadata = {
title: "New Project",
description:
"Customize everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.",
openGraph: {
title: "New Project",
description:
"Customize everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.",
type: "website",
url: absoluteUrl("/create"),
images: [
{
url: siteConfig.ogImage,
width: 1200,
height: 630,
alt: siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
title: "New Project",
description:
"Customize everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.",
images: [siteConfig.ogImage],
creator: "@shadcn",
},
}
export default function CreatePage() {
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"
>
<Preview />
<Suspense
fallback={
<Skeleton className="isolate min-h-[151px] w-full self-start rounded-2xl md:h-full md:max-h-full md:min-h-0 md:w-(--customizer-width)" />
}
>
<CustomizerLoader />
</Suspense>
</div>
<PresetHandler />
<WelcomeDialog />
</div>
)
}
async function CustomizerLoader() {
const itemsByBase = await getAllItems()
return <Customizer itemsByBase={itemsByBase} />
}

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/(create)/lib/search-params"
import { groupItemsByType } from "@/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

@@ -0,0 +1,11 @@
"use client"
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() {
const [params] = useDesignSystemSearchParams()
return getPresetCode(params)
}

View File

@@ -0,0 +1,185 @@
"use client"
import * as React from "react"
import { Suspense } from "react"
import { useSearchParams } from "next/navigation"
import {
useDesignSystemSearchParams,
type DesignSystemSearchParams,
} from "@/app/(app)/(create)/lib/search-params"
type HistoryContextValue = {
canGoBack: boolean
canGoForward: boolean
goBack: () => void
goForward: () => void
}
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,
}: {
onPresetChange: (preset: string) => void
}) {
const searchParams = useSearchParams()
const preset = searchParams.get("preset") ?? ""
React.useEffect(() => {
onPresetChange(preset)
}, [preset, onPresetChange])
return null
}
export function HistoryProvider({ children }: { children: React.ReactNode }) {
// 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("")
const entriesRef = React.useRef<string[]>([preset])
const indexRef = React.useRef(0)
const maxIndexRef = React.useRef(0)
const isNavigatingRef = React.useRef(false)
const [index, setIndex] = React.useState(0)
const [maxIndex, setMaxIndex] = React.useState(0)
const onPresetChange = React.useCallback((nextPreset: string) => {
setPreset(nextPreset)
}, [])
React.useEffect(() => {
if (isNavigatingRef.current) {
isNavigatingRef.current = false
return
}
if (preset === entriesRef.current[indexRef.current]) {
return
}
const nextEntries = entriesRef.current.slice(0, indexRef.current + 1)
nextEntries.push(preset)
entriesRef.current = nextEntries
const nextIndex = nextEntries.length - 1
indexRef.current = nextIndex
maxIndexRef.current = nextIndex
setIndex(nextIndex)
setMaxIndex(nextIndex)
}, [preset])
const canGoBack = index > 0
const canGoForward = index < maxIndex
const goBack = React.useCallback(() => {
if (indexRef.current <= 0) {
return
}
isNavigatingRef.current = true
const nextIndex = indexRef.current - 1
indexRef.current = nextIndex
setIndex(nextIndex)
// 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) {
return
}
isNavigatingRef.current = true
const nextIndex = indexRef.current + 1
indexRef.current = nextIndex
setIndex(nextIndex)
// 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) => {
if (!e.metaKey && !e.ctrlKey) {
return
}
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
const key = e.key.toLowerCase()
if ((key === "z" && e.shiftKey) || (key === "y" && e.ctrlKey)) {
e.preventDefault()
goForward()
return
}
if (key === "z") {
e.preventDefault()
goBack()
}
}
document.addEventListener("keydown", down)
return () => {
document.removeEventListener("keydown", down)
}
}, [goBack, goForward])
const value = React.useMemo(
() => ({ canGoBack, canGoForward, goBack, goForward }),
[canGoBack, canGoForward, goBack, goForward]
)
return (
<HistoryContext value={value}>
<Suspense>
<PresetSync onPresetChange={onPresetChange} />
</Suspense>
{children}
</HistoryContext>
)
}
export function useHistory() {
const context = React.useContext(HistoryContext)
if (!context) {
throw new Error("useHistory must be used within HistoryProvider")
}
return context
}

View File

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

View File

@@ -6,8 +6,10 @@ export type LockableParam =
| "style"
| "baseColor"
| "theme"
| "chartColor"
| "iconLibrary"
| "font"
| "fontHeading"
| "menuAccent"
| "menuColor"
| "radius"

View File

@@ -0,0 +1,81 @@
"use client"
import * as React from "react"
import useSWR from "swr"
const OPEN_PRESET_KEY = "create:open-preset-open"
export const OPEN_PRESET_FORWARD_TYPE = "open-preset-forward"
function isEditableTarget(target: EventTarget | null) {
return (
(target instanceof HTMLElement && target.isContentEditable) ||
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement
)
}
export function useOpenPreset() {
const { data: open = false, mutate: setOpenData } = useSWR<boolean>(
OPEN_PRESET_KEY,
{
fallbackData: false,
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
}
)
const handleOpenChange = React.useCallback(
(nextOpen: boolean) => {
void setOpenData(nextOpen, { revalidate: false })
},
[setOpenData]
)
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (
e.key === "o" &&
!e.shiftKey &&
!e.metaKey &&
!e.ctrlKey &&
!e.altKey
) {
if (isEditableTarget(e.target)) {
return
}
e.preventDefault()
void setOpenData(true, { revalidate: false })
}
}
document.addEventListener("keydown", down)
return () => {
document.removeEventListener("keydown", down)
}
}, [setOpenData])
return {
open,
setOpen: handleOpenChange,
}
}
export function useOpenPresetTrigger() {
const { mutate: setOpenData } = useSWR<boolean>(OPEN_PRESET_KEY, {
fallbackData: false,
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
})
const openPreset = React.useCallback(() => {
void setOpenData(true, { revalidate: false })
}, [setOpenData])
return {
openPreset,
}
}

View File

@@ -0,0 +1,249 @@
"use client"
import * as React from "react"
import { decodePreset } from "shadcn/preset"
import {
BASE_COLORS,
getThemesForBaseColor,
iconLibraries,
MENU_ACCENTS,
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 { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
import {
applyBias,
RANDOMIZE_BIASES,
type RandomizeContext,
} from "@/app/(app)/(create)/lib/randomize-biases"
import {
isTranslucentMenuColor,
useDesignSystemSearchParams,
} 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)]
}
export function useRandom() {
const { locks } = useLocks()
const [params, setParams] = useDesignSystemSearchParams()
const paramsRef = React.useRef(params)
React.useEffect(() => {
paramsRef.current = params
}, [params])
// 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
const context: RandomizeContext = {
style: selectedStyle,
}
const availableBaseColors = applyBias(
BASE_COLORS,
context,
RANDOMIZE_BIASES.baseColors
)
const baseColor = locks.has("baseColor")
? paramsRef.current.baseColor
: randomItem(availableBaseColors).name
context.baseColor = baseColor
const availableThemes = getThemesForBaseColor(baseColor)
const availableFonts = applyBias(FONTS, context, RANDOMIZE_BIASES.fonts)
const availableRadii = applyBias(RADII, context, RANDOMIZE_BIASES.radius)
const selectedTheme = locks.has("theme")
? paramsRef.current.theme
: randomItem(availableThemes).name
context.theme = selectedTheme
const availableChartColors = applyBias(
getThemesForBaseColor(baseColor),
context,
RANDOMIZE_BIASES.chartColors
)
const selectedChartColor = locks.has("chartColor")
? paramsRef.current.chartColor
: randomItem(availableChartColors).name
context.chartColor = selectedChartColor
const selectedFont = locks.has("font")
? paramsRef.current.font
: randomItem(availableFonts).value
context.font = selectedFont
// Pick heading font: ~70% inherit, ~30% distinct with cross-category contrast.
let selectedFontHeading: FontHeadingValue
if (locks.has("fontHeading")) {
selectedFontHeading = paramsRef.current.fontHeading
} else if (Math.random() < 0.7) {
selectedFontHeading = "inherit"
} else {
const bodyType = availableFonts.find(
(f) => f.value === selectedFont
)?.type
const contrastFonts = availableFonts.filter(
(f) => f.type !== bodyType && f.value !== selectedFont
)
selectedFontHeading = (
contrastFonts.length > 0
? randomItem(contrastFonts)
: randomItem(availableFonts)
).value as FontHeadingValue
}
const selectedRadius = locks.has("radius")
? paramsRef.current.radius
: randomItem(availableRadii).name
const selectedIconLibrary = locks.has("iconLibrary")
? paramsRef.current.iconLibrary
: randomItem(Object.values(iconLibraries)).name
const lockedMenuAccent = locks.has("menuAccent")
? paramsRef.current.menuAccent
: undefined
const availableMenuColors =
!locks.has("menuColor") && lockedMenuAccent === "bold"
? MENU_COLORS.filter((menuColor) => {
return !isTranslucentMenuColor(menuColor.value)
})
: MENU_COLORS
const selectedMenuColor = locks.has("menuColor")
? paramsRef.current.menuColor
: randomItem(availableMenuColors).value
const selectedMenuAccent =
locks.has("menuAccent") || isTranslucentMenuColor(selectedMenuColor)
? paramsRef.current.menuAccent === "bold" &&
isTranslucentMenuColor(selectedMenuColor)
? "subtle"
: paramsRef.current.menuAccent
: randomItem(MENU_ACCENTS).value
context.radius = selectedRadius
const nextParams = {
style: selectedStyle,
baseColor,
theme: selectedTheme,
chartColor: selectedChartColor,
iconLibrary: selectedIconLibrary,
font: selectedFont,
fontHeading: selectedFontHeading,
menuAccent: selectedMenuAccent,
menuColor: selectedMenuColor,
radius: selectedRadius,
}
// 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])
// 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
}, [randomize])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "r" && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
e.preventDefault()
randomizeRef.current()
}
}
document.addEventListener("keydown", down)
return () => {
document.removeEventListener("keydown", down)
}
}, [])
return { randomize, randomizeTrueRandom }
}

View File

@@ -0,0 +1,103 @@
"use client"
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"
const RESET_DIALOG_KEY = "create:reset-dialog-open"
export const RESET_FORWARD_TYPE = "reset-forward"
export function useReset() {
const [params, setParams] = useDesignSystemSearchParams()
const { data: showResetDialog = false, mutate: setShowResetDialogData } =
useSWR<boolean>(RESET_DIALOG_KEY, {
fallbackData: false,
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
})
const reset = React.useCallback(() => {
const preset =
PRESETS.find(
(preset) => preset.base === params.base && preset.style === params.style
) ?? DEFAULT_CONFIG
setParams({
base: params.base,
style: params.style,
baseColor: preset.baseColor,
theme: preset.theme,
chartColor: preset.chartColor,
iconLibrary: preset.iconLibrary,
font: preset.font,
fontHeading: preset.fontHeading,
menuAccent: preset.menuAccent,
menuColor: preset.menuColor,
radius: preset.radius,
template: DEFAULT_CONFIG.template,
item: params.item,
})
}, [setParams, params.base, params.style, params.item])
const handleShowResetDialogChange = React.useCallback(
(open: boolean) => {
void setShowResetDialogData(open, { revalidate: false })
},
[setShowResetDialogData]
)
const confirmReset = React.useCallback(() => {
reset()
void setShowResetDialogData(false, { revalidate: false })
}, [reset, setShowResetDialogData])
const showResetDialogRef = React.useRef(showResetDialog)
React.useEffect(() => {
showResetDialogRef.current = showResetDialog
}, [showResetDialog])
const confirmResetRef = React.useRef(confirmReset)
React.useEffect(() => {
confirmResetRef.current = confirmReset
}, [confirmReset])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "R" && e.shiftKey && !e.metaKey && !e.ctrlKey) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
e.preventDefault()
// If the dialog is already open, confirm the reset.
if (showResetDialogRef.current) {
confirmResetRef.current()
return
}
handleShowResetDialogChange(true)
}
}
document.addEventListener("keydown", down)
return () => {
document.removeEventListener("keydown", down)
}
}, [handleShowResetDialogChange])
return {
reset,
showResetDialog,
setShowResetDialog: handleShowResetDialogChange,
confirmReset,
}
}

View File

@@ -0,0 +1,29 @@
import { type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
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 {
const searchParams = request.nextUrl.searchParams
const result = parseDesignSystemConfig(searchParams)
if (!result.success) {
return new Response(result.error, { status: 400 })
}
track("create_app_manual", result.data)
const markdown = buildInstructions(result.data)
return new Response(markdown, {
headers: { "Content-Type": "text/markdown; charset=utf-8" },
})
} catch (error) {
return new Response(
error instanceof Error ? error.message : "An unknown error occurred",
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest"
import {
buildRegistryBase,
DEFAULT_CONFIG,
POINTER_CURSOR_SELECTOR,
} from "@/registry/config"
import { GET } from "./route"
function createRequest(search = "") {
const searchParams = new URLSearchParams(
Object.entries(DEFAULT_CONFIG).map(([key, value]) => [key, String(value)])
)
const url = new URL(`http://localhost:4000/init${search}`)
for (const [key, value] of url.searchParams) {
searchParams.set(key, value)
}
return {
nextUrl: new URL(`http://localhost:4000/init?${searchParams}`),
} as Parameters<typeof GET>[0]
}
describe("GET /init", () => {
it("returns the full registry base when only is omitted", async () => {
const response = await GET(createRequest())
const json = await response.json()
expect(response.status).toBe(200)
expect(json).toEqual(buildRegistryBase(DEFAULT_CONFIG))
expect(json.css["@layer base"][POINTER_CURSOR_SELECTOR]).toBeUndefined()
})
it("returns pointer cursor css when pointer is enabled", async () => {
const response = await GET(createRequest("?pointer=true"))
const json = await response.json()
expect(response.status).toBe(200)
expect(json.css["@layer base"][POINTER_CURSOR_SELECTOR]).toEqual({
cursor: "pointer",
})
})
it("returns a sparse registry base when only is provided", async () => {
const response = await GET(createRequest("?only=theme"))
const json = await response.json()
expect(response.status).toBe(200)
expect(json.type).toBe("registry:base")
expect(json.config).toEqual({
menuColor: "default",
menuAccent: "subtle",
tailwind: {
baseColor: "neutral",
},
})
expect(json.cssVars.light).toBeDefined()
expect(json.cssVars.light.radius).toBe("0.625rem")
expect(json.dependencies).toBeUndefined()
expect(json.registryDependencies).toBeUndefined()
})
it("rejects unsupported only values", async () => {
const response = await GET(createRequest("?only=icon"))
const json = await response.json()
expect(response.status).toBe(400)
expect(json.error).toBe(
"Invalid only value. Use one or more of: theme, font"
)
})
})

View File

@@ -0,0 +1,66 @@
import { NextResponse, type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import { isPresetCode } from "shadcn/preset"
import { registryItemSchema } from "shadcn/schema"
import {
buildPartialRegistryBase,
buildRegistryBase,
parseRegistryBaseParts,
} from "@/registry/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 {
const searchParams = request.nextUrl.searchParams
const result = parseDesignSystemConfig(searchParams)
if (!result.success) {
return NextResponse.json({ error: result.error }, { status: 400 })
}
const onlyResult = parseRegistryBaseParts(searchParams.get("only"))
if (!onlyResult.success) {
return NextResponse.json({ error: onlyResult.error }, { status: 400 })
}
const rawPreset = searchParams.get("preset")
const presetCode =
rawPreset && isPresetCode(rawPreset)
? rawPreset
: getPresetCode(result.data)
const registryBase = onlyResult.parts
? buildPartialRegistryBase(result.data, onlyResult.parts)
: buildRegistryBase(result.data)
const parseResult = registryItemSchema.safeParse(registryBase)
if (!parseResult.success) {
return NextResponse.json(
{
error: "Invalid registry base item",
details: parseResult.error.format(),
},
{ status: 500 }
)
}
if (searchParams.get("track") === "1") {
track("create_app", {
...result.data,
preset: presetCode,
})
}
return NextResponse.json(parseResult.data)
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "An unknown error occurred",
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,44 @@
import { after, NextResponse, type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import { isPresetCode } from "shadcn/preset"
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 {
const searchParams = request.nextUrl.searchParams
const result = parseDesignSystemConfig(searchParams)
if (!result.success) {
return NextResponse.json({ error: result.error }, { status: 400 })
}
const rawPreset = searchParams.get("preset")
const presetCode =
rawPreset && isPresetCode(rawPreset)
? rawPreset
: getPresetCode(result.data)
// Defer analytics to after response is sent.
after(() => {
track("create_open_in_v0", {
...result.data,
preset: presetCode,
})
})
const payload = await buildV0Payload(result.data)
return NextResponse.json(payload)
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "An unknown error occurred",
},
{ status: 500 }
)
}
}

View File

@@ -6,7 +6,7 @@ import { BASES, getThemesForBaseColor, type BaseName } from "@/registry/config"
import {
ALLOWED_ITEM_TYPES,
EXCLUDED_ITEMS,
} from "@/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,297 @@
import dedent from "dedent"
import { UI_COMPONENTS } from "@/lib/components"
import {
buildRegistryBase,
getBodyFont,
getHeadingFont,
getInheritedHeadingFontValue,
type DesignSystemConfig,
} from "@/registry/config"
// Builds step-by-step markdown instructions for manually setting up a project.
export function buildInstructions(config: DesignSystemConfig) {
const registryBase = buildRegistryBase(config)
const normalizedFontHeading =
config.fontHeading === config.font ? "inherit" : config.fontHeading
const dependencies = [
...(registryBase.dependencies ?? []),
"clsx",
"tailwind-merge",
]
const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
const darkVars = Object.entries(registryBase.cssVars?.dark ?? {})
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
const font = getBodyFont(config.font)
const headingFont =
normalizedFontHeading === "inherit"
? undefined
: getHeadingFont(normalizedFontHeading)
const sections = [
buildDependenciesSection(dependencies),
buildUtilsSection(),
buildCssSection(
lightVars,
darkVars,
normalizedFontHeading === "inherit"
? getInheritedHeadingFontValue(config.font)
: "var(--font-heading)"
),
buildFontSection(font, headingFont),
buildComponentsJsonSection(config),
buildAvailableComponentsSection(config),
config.rtl ? buildRtlSection(config) : null,
]
return sections.filter(Boolean).join("\n\n---\n\n")
}
function buildDependenciesSection(dependencies: string[]) {
const list = dependencies.map((dep) => `- ${dep}`).join("\n")
return dedent`
## Step 1: Dependencies
The following dependencies are required:
${list}
`
}
function buildUtilsSection() {
return dedent`
## Step 2: Create \`lib/utils.ts\`
\`\`\`ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
\`\`\`
`
}
function buildCssSection(
lightVars: string,
darkVars: string,
fontHeadingValue: string
) {
return dedent`
## Step 3: Set up CSS
Add the following to your global CSS file (e.g. \`app/globals.css\`):
\`\`\`css
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@theme inline {
--font-sans: var(--font-sans);
--font-heading: ${fontHeadingValue};
--font-mono: var(--font-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root {
${lightVars}
}
.dark {
${darkVars}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
\`\`\`
`
}
function buildFontSection(
font: ReturnType<typeof getBodyFont>,
headingFont: ReturnType<typeof getHeadingFont>
) {
if (!font) {
return null
}
const googleFontsUrl = `https://fonts.google.com/specimen/${font.font.import.replace(/_/g, "+")}`
const headingGoogleFontsUrl = headingFont
? `https://fonts.google.com/specimen/${headingFont.font.import.replace(/_/g, "+")}`
: null
const nextImports = headingFont
? `${font.font.import}, ${headingFont.font.import}`
: font.font.import
const nextDeclarations = [
`const fontSans = ${font.font.import}({`,
` subsets: ["latin"],`,
` variable: "${font.font.variable}",`,
`})`,
headingFont ? `const fontHeading = ${headingFont.font.import}({` : null,
headingFont ? ` subsets: ["latin"],` : null,
headingFont ? ` variable: "${headingFont.font.variable}",` : null,
headingFont ? `})` : null,
]
.filter(Boolean)
.join("\n")
const nextHtmlClassName = headingFont
? 'className={fontSans.variable + " " + fontHeading.variable}'
: `className={fontSans.variable}`
const otherFrameworkCss = [
":root {",
` ${font.font.variable}: ${font.font.family};`,
...(headingFont
? [` ${headingFont.font.variable}: ${headingFont.font.family};`]
: []),
"}",
].join("\n")
const headingSection = headingFont
? dedent`
This config also uses **${headingFont.title.replace(" (Heading)", "")}** for headings (\`${headingFont.font.variable}\`).
`
: ""
return dedent`
## Step 4: Set up the font
This config uses **${font.title}** (\`${font.font.variable}\`).
${headingSection}
### Next.js
\`\`\`tsx
import { ${nextImports} } from "next/font/google"
${nextDeclarations}
// Add the font variable classes to your <html> className.
// <html ${nextHtmlClassName}>
\`\`\`
### Other frameworks
Add the font${headingFont ? "s" : ""} from [Google Fonts](${googleFontsUrl})${headingGoogleFontsUrl ? ` and [Google Fonts](${headingGoogleFontsUrl})` : ""} and set the CSS variable${headingFont ? "s" : ""} to the font famil${headingFont ? "ies" : "y"}:
\`\`\`css
${otherFrameworkCss}
\`\`\`
`
}
function buildComponentsJsonSection(config: DesignSystemConfig) {
const componentsJson = {
$schema: "https://ui.shadcn.com/schema.json",
style: `${config.base}-${config.style}`,
tailwind: {
css: "app/globals.css",
baseColor: config.baseColor,
},
iconLibrary: config.iconLibrary,
aliases: {
components: "@/components",
utils: "@/lib/utils",
ui: "@/components/ui",
lib: "@/lib",
hooks: "@/hooks",
},
}
return dedent`
## Step 5: Create \`components.json\`
Add a \`components.json\` file to the root of your project:
\`\`\`json
${JSON.stringify(componentsJson, null, 2)}
\`\`\`
`
}
function buildAvailableComponentsSection(config: DesignSystemConfig) {
const list = UI_COMPONENTS.join(", ")
const style = `${config.base}-${config.style}`
return dedent`
## Available Components
${list}
To fetch the source for a component, use:
\`https://ui.shadcn.com/r/styles/${style}/<component>.json\`
For documentation and examples, visit:
\`https://ui.shadcn.com/docs/components/${config.base}/<component>\`
`
}
function buildRtlSection(config: DesignSystemConfig) {
const template =
config.template === "next-monorepo" ? "next" : (config.template ?? "next")
return dedent`
## RTL Support
Add \`dir="rtl"\` to your root \`<html>\` element:
\`\`\`html
<html dir="rtl">
\`\`\`
For full RTL setup including the \`DirectionProvider\`, see the [RTL documentation](https://ui.shadcn.com/docs/rtl/${template}).
`
}

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

@@ -0,0 +1,77 @@
import { FONT_DEFINITIONS, type FontName } from "@/lib/font-definitions"
type CreateFont = {
family: string
import: string
previewVariable: string
style: {
fontFamily: string
}
variable: string
}
function createFontOption(name: FontName) {
const definition = FONT_DEFINITIONS.find((font) => font.name === name)
if (!definition) {
throw new Error(`Unknown font definition: ${name}`)
}
return {
name: definition.title,
value: definition.name,
font: {
family: definition.family,
import: definition.import,
previewVariable: definition.previewVariable,
style: {
fontFamily: `var(${definition.previewVariable}), ${definition.family}`,
},
variable: definition.registryVariable,
} satisfies CreateFont,
type: definition.type,
} as const
}
export const FONTS = [
createFontOption("geist"),
createFontOption("inter"),
createFontOption("noto-sans"),
createFontOption("nunito-sans"),
createFontOption("figtree"),
createFontOption("roboto"),
createFontOption("raleway"),
createFontOption("dm-sans"),
createFontOption("public-sans"),
createFontOption("outfit"),
createFontOption("oxanium"),
createFontOption("manrope"),
createFontOption("space-grotesk"),
createFontOption("montserrat"),
createFontOption("ibm-plex-sans"),
createFontOption("source-sans-3"),
createFontOption("instrument-sans"),
createFontOption("geist-mono"),
createFontOption("jetbrains-mono"),
createFontOption("noto-serif"),
createFontOption("roboto-slab"),
createFontOption("merriweather"),
createFontOption("lora"),
createFontOption("playfair-display"),
createFontOption("eb-garamond"),
createFontOption("instrument-serif"),
] as const
export type Font = (typeof FONTS)[number]
export const FONT_HEADING_OPTIONS = [
{
name: "Inherit",
value: "inherit",
font: null,
type: "default",
},
...FONTS,
] as const
export type FontHeadingOption = (typeof FONT_HEADING_OPTIONS)[number]

View File

@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest"
import { parseDesignSystemConfig } from "./parse-config"
describe("parseDesignSystemConfig", () => {
it("defaults pointer to false when omitted", () => {
const result = parseDesignSystemConfig(
new URLSearchParams(
"base=base&style=sera&baseColor=taupe&theme=taupe&iconLibrary=lucide&font=noto-sans&rtl=false&menuAccent=subtle&menuColor=default&radius=default&fontHeading=playfair-display&template=vite&track=1"
)
)
expect(result.success).toBe(true)
if (!result.success) {
throw new Error(result.error)
}
expect(result.data.pointer).toBe(false)
})
it("parses pointer=true", () => {
const result = parseDesignSystemConfig(
new URLSearchParams(
"base=base&style=sera&baseColor=taupe&theme=taupe&iconLibrary=lucide&font=noto-sans&rtl=false&pointer=true&menuAccent=subtle&menuColor=default&radius=default&fontHeading=playfair-display&template=vite&track=1"
)
)
expect(result.success).toBe(true)
if (!result.success) {
throw new Error(result.error)
}
expect(result.data.pointer).toBe(true)
})
it("defaults missing chartColor from the selected theme", () => {
const result = parseDesignSystemConfig(
new URLSearchParams(
"base=base&style=sera&baseColor=taupe&theme=taupe&iconLibrary=lucide&font=noto-sans&rtl=false&menuAccent=subtle&menuColor=default&radius=default&fontHeading=playfair-display&template=vite&track=1"
)
)
expect(result.success).toBe(true)
if (!result.success) {
throw new Error(result.error)
}
expect(result.data.chartColor).toBe("taupe")
})
it("honors explicit fontHeading and chartColor overrides when a preset is present", () => {
const result = parseDesignSystemConfig(
new URLSearchParams(
"preset=a0&fontHeading=playfair-display&chartColor=emerald"
)
)
expect(result.success).toBe(true)
if (!result.success) {
throw new Error(result.error)
}
expect(result.data.fontHeading).toBe("playfair-display")
expect(result.data.chartColor).toBe("emerald")
})
it("keeps pointer outside preset decoding", () => {
const result = parseDesignSystemConfig(
new URLSearchParams("preset=a0&pointer=true")
)
expect(result.success).toBe(true)
if (!result.success) {
throw new Error(result.error)
}
expect(result.data.pointer).toBe(true)
})
})

View File

@@ -4,6 +4,7 @@ import {
designSystemConfigSchema,
type DesignSystemConfig,
} from "@/registry/config"
import { resolvePresetOverrides } from "@/app/(app)/(create)/lib/preset-query"
// Parses design system config from URL search params.
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
@@ -15,11 +16,14 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
if (!decoded) {
return { success: false as const, error: "Invalid preset code" }
}
const presetOverrides = resolvePresetOverrides(searchParams, decoded)
configInput = {
...decoded,
base: searchParams.get("base") ?? "radix",
...presetOverrides,
base: searchParams.get("base") ?? "base",
template: searchParams.get("template") ?? undefined,
rtl: searchParams.get("rtl") === "true",
pointer: searchParams.get("pointer") === "true",
}
} else {
configInput = {
@@ -28,12 +32,15 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
iconLibrary: searchParams.get("iconLibrary"),
baseColor: searchParams.get("baseColor"),
theme: searchParams.get("theme"),
chartColor: searchParams.get("chartColor") ?? undefined,
font: searchParams.get("font"),
fontHeading: searchParams.get("fontHeading") ?? undefined,
menuAccent: searchParams.get("menuAccent"),
menuColor: searchParams.get("menuColor"),
radius: searchParams.get("radius"),
template: searchParams.get("template") ?? undefined,
rtl: searchParams.get("rtl") === "true",
pointer: searchParams.get("pointer") === "true",
}
}

View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest"
import { parsePresetInput } from "./parse-preset-input"
describe("parsePresetInput", () => {
it("accepts a raw preset code", () => {
expect(parsePresetInput("b0")).toBe("b0")
})
it("accepts a --preset flag", () => {
expect(parsePresetInput(" --preset b0 ")).toBe("b0")
})
it("rejects invalid preset input", () => {
expect(parsePresetInput("open sesame")).toBeNull()
})
})

View File

@@ -0,0 +1,15 @@
import { isPresetCode } from "shadcn/preset"
const PRESET_FLAG_PATTERN = /^--preset\b\s+(.+)$/i
export function parsePresetInput(value: string) {
const input = value.trim()
if (!input) {
return null
}
const preset = input.match(PRESET_FLAG_PATTERN)?.[1]?.trim() ?? input
return isPresetCode(preset) ? preset : null
}

View File

@@ -0,0 +1,34 @@
import { encodePreset, type PresetConfig } from "shadcn/preset"
import { type DesignSystemConfig } from "@/registry/config"
type PresetCodeConfig = Pick<
DesignSystemConfig,
| "style"
| "baseColor"
| "theme"
| "chartColor"
| "iconLibrary"
| "font"
| "fontHeading"
| "radius"
| "menuAccent"
| "menuColor"
>
export function getPresetCode(config: PresetCodeConfig) {
const presetConfig: Partial<PresetConfig> = {
style: config.style as PresetConfig["style"],
baseColor: config.baseColor as PresetConfig["baseColor"],
theme: config.theme as PresetConfig["theme"],
chartColor: config.chartColor as PresetConfig["chartColor"],
iconLibrary: config.iconLibrary as PresetConfig["iconLibrary"],
font: config.font as PresetConfig["font"],
fontHeading: config.fontHeading as PresetConfig["fontHeading"],
radius: config.radius as PresetConfig["radius"],
menuAccent: config.menuAccent as PresetConfig["menuAccent"],
menuColor: config.menuColor as PresetConfig["menuColor"],
}
return encodePreset(presetConfig)
}

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest"
import { resolvePresetOverrides } from "./preset-query"
describe("resolvePresetOverrides", () => {
it("prefers explicit fontHeading and chartColor query params", () => {
const overrides = resolvePresetOverrides(
new URLSearchParams("fontHeading=playfair-display&chartColor=emerald"),
{
theme: "neutral",
chartColor: "blue",
fontHeading: "inherit",
}
)
expect(overrides).toEqual({
fontHeading: "playfair-display",
chartColor: "emerald",
})
})
it("falls back to decoded preset values when no overrides are present", () => {
const overrides = resolvePresetOverrides(new URLSearchParams(), {
theme: "neutral",
chartColor: "blue",
fontHeading: "inherit",
})
expect(overrides).toEqual({
fontHeading: "inherit",
chartColor: "blue",
})
})
})

View File

@@ -0,0 +1,30 @@
import { V1_CHART_COLOR_MAP, type PresetConfig } from "shadcn/preset"
import { type ChartColorName, type FontHeadingValue } from "@/registry/config"
type SearchParamsLike = Pick<URLSearchParams, "get" | "has">
export function resolvePresetOverrides(
searchParams: SearchParamsLike,
decoded: Pick<PresetConfig, "theme" | "chartColor" | "fontHeading">
) {
const hasFontHeadingOverride = searchParams.has("fontHeading")
const hasChartColorOverride = searchParams.has("chartColor")
const fontHeading = hasFontHeadingOverride
? ((searchParams.get("fontHeading") ??
decoded.fontHeading) as FontHeadingValue)
: decoded.fontHeading
const chartColor = hasChartColorOverride
? ((searchParams.get("chartColor") ??
decoded.chartColor ??
V1_CHART_COLOR_MAP[decoded.theme] ??
decoded.theme) as ChartColorName)
: (decoded.chartColor ?? V1_CHART_COLOR_MAP[decoded.theme] ?? decoded.theme)
return {
fontHeading,
chartColor,
}
}

View File

@@ -0,0 +1,113 @@
import type {
BaseColor,
BaseColorName,
Radius,
StyleName,
Theme,
ThemeName,
} from "@/registry/config"
import { type FONTS } from "./fonts"
export type RandomizeContext = {
style?: StyleName
baseColor?: BaseColorName
theme?: ThemeName
chartColor?: string
iconLibrary?: string
font?: string
menuAccent?: string
menuColor?: string
radius?: string
}
export type BiasFilter<T> = (
items: readonly T[],
context: RandomizeContext
) => readonly T[]
export type RandomizeBiases = {
baseColors?: BiasFilter<BaseColor>
chartColors?: BiasFilter<Theme>
fonts?: BiasFilter<(typeof FONTS)[number]>
radius?: BiasFilter<Radius>
}
// Theme → chart color pairings for randomization.
const CHART_COLOR_PAIRINGS: Record<string, string[]> = {
red: ["teal", "sky"],
orange: ["teal", "blue"],
amber: ["cyan", "indigo"],
yellow: ["sky", "violet"],
lime: ["indigo", "pink"],
green: ["purple", "rose"],
emerald: ["purple", "red"],
teal: ["fuchsia", "red"],
cyan: ["rose", "amber"],
sky: ["red", "yellow"],
blue: ["orange", "yellow"],
indigo: ["amber", "yellow"],
violet: ["yellow", "lime"],
purple: ["green", "lime"],
fuchsia: ["lime", "teal"],
pink: ["green", "cyan"],
rose: ["emerald", "sky"],
}
/**
* Configuration for randomization biases.
* Add biases here to influence random selection based on context.
*/
export const RANDOMIZE_BIASES: RandomizeBiases = {
baseColors: (baseColors) => {
return baseColors.filter((c) => c.name !== "gray")
},
fonts: (fonts, context) => {
// When style is lyra, only use mono fonts.
if (context.style === "lyra") {
return fonts.filter((font) => font.value === "jetbrains-mono")
}
return fonts
},
radius: (radii, context) => {
// When style is lyra, always use "none" radius.
if (context.style === "lyra") {
return radii.filter((radius) => radius.name === "none")
}
// Rhea does not support the "large" radius.
if (context.style === "rhea") {
return radii.filter((radius) => radius.name !== "large")
}
return radii
},
chartColors: (chartColors, context) => {
// When theme has a pairing, restrict chart colors to the paired values.
const pairing = context.theme ? CHART_COLOR_PAIRINGS[context.theme] : null
if (pairing) {
const filtered = chartColors.filter((c) => pairing.includes(c.name))
if (filtered.length > 0) {
return filtered
}
}
return chartColors
},
}
/**
* Applies biases to a list of items based on the current context.
*/
export function applyBias<T>(
items: readonly T[],
context: RandomizeContext,
biasFilter?: BiasFilter<T>
): readonly T[] {
if (!biasFilter) {
return items
}
return biasFilter(items, context)
}

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

@@ -0,0 +1,343 @@
import * as React from "react"
import { useSearchParams } from "next/navigation"
import { useQueryStates } from "nuqs"
import {
createLoader,
createSerializer,
parseAsBoolean,
parseAsInteger,
parseAsString,
parseAsStringLiteral,
type inferParserType,
type Options,
} from "nuqs/server"
import { decodePreset, isPresetCode } from "shadcn/preset"
import {
BASE_COLORS,
BASES,
DEFAULT_CONFIG,
getThemesForBaseColor,
iconLibraries,
MENU_ACCENTS,
MENU_COLORS,
RADII,
STYLES,
THEMES,
type BaseColorName,
type BaseName,
type ChartColorName,
type FontHeadingValue,
type FontValue,
type IconLibraryName,
type MenuAccentValue,
type MenuColorValue,
type RadiusValue,
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"
const designSystemSearchParams = {
preset: parseAsString.withDefault("b0"),
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
DEFAULT_CONFIG.base
),
item: parseAsString.withDefault("preview-02").withOptions({ shallow: true }),
iconLibrary: parseAsStringLiteral<IconLibraryName>(
Object.values(iconLibraries).map((i) => i.name)
).withDefault(DEFAULT_CONFIG.iconLibrary),
style: parseAsStringLiteral<StyleName>(STYLES.map((s) => s.name)).withDefault(
DEFAULT_CONFIG.style
),
theme: parseAsStringLiteral<ThemeName>(THEMES.map((t) => t.name)).withDefault(
DEFAULT_CONFIG.theme
),
chartColor: parseAsStringLiteral<ChartColorName>(
THEMES.map((t) => t.name)
).withDefault(DEFAULT_CONFIG.chartColor ?? "neutral"),
font: parseAsStringLiteral<FontValue>(FONTS.map((f) => f.value)).withDefault(
DEFAULT_CONFIG.font
),
fontHeading: parseAsStringLiteral<FontHeadingValue>([
"inherit",
...FONTS.map((f) => f.value),
]).withDefault(DEFAULT_CONFIG.fontHeading),
baseColor: parseAsStringLiteral<BaseColorName>(
BASE_COLORS.map((b) => b.name)
).withDefault(DEFAULT_CONFIG.baseColor),
menuAccent: parseAsStringLiteral<MenuAccentValue>(
MENU_ACCENTS.map((a) => a.value)
).withDefault(DEFAULT_CONFIG.menuAccent),
menuColor: parseAsStringLiteral<MenuColorValue>(
MENU_COLORS.map((m) => m.value)
).withDefault(DEFAULT_CONFIG.menuColor),
radius: parseAsStringLiteral<RadiusValue>(
RADII.map((r) => r.name)
).withDefault("default"),
template: parseAsStringLiteral([
"next",
"next-monorepo",
"start",
"start-monorepo",
"react-router",
"react-router-monorepo",
"vite",
"vite-monorepo",
"astro",
"astro-monorepo",
"laravel",
] as const).withDefault("next"),
rtl: parseAsBoolean.withDefault(false),
pointer: parseAsBoolean.withDefault(false),
size: parseAsInteger.withDefault(100),
custom: parseAsBoolean.withDefault(false),
}
// Design system param keys that get encoded into the preset code.
const DESIGN_SYSTEM_KEYS = [
"style",
"baseColor",
"theme",
"chartColor",
"iconLibrary",
"font",
"fontHeading",
"radius",
"menuAccent",
"menuColor",
] as const
function normalizeFontHeading(
font: FontValue,
fontHeading: FontHeadingValue
): FontHeadingValue {
// Persist "same as body" as an explicit inherit sentinel so the body font
// can change later without freezing headings to a concrete previous value.
return fontHeading === font ? "inherit" : fontHeading
}
// Non-design-system keys that get passed through as-is.
// `base` is not encoded in preset codes — it's an architectural choice, not visual.
const NON_DESIGN_SYSTEM_KEYS = [
"base",
"item",
"preset",
"template",
"rtl",
"pointer",
"size",
"custom",
] as const
// Shared across hook instances so only one mount effect encodes the initial URL.
let hasSyncedPresetToUrl = false
export const loadDesignSystemSearchParams = createLoader(
designSystemSearchParams
)
export const serializeDesignSystemSearchParams = createSerializer(
designSystemSearchParams
)
export type DesignSystemSearchParams = inferParserType<
typeof designSystemSearchParams
>
export function isTranslucentMenuColor(
menuColor?: MenuColorValue | null
): menuColor is "default-translucent" | "inverted-translucent" {
return (
menuColor === "default-translucent" || menuColor === "inverted-translucent"
)
}
function normalizePartialDesignSystemParams(
params: Partial<DesignSystemSearchParams>
): Partial<DesignSystemSearchParams> {
if (
params.menuAccent === "bold" &&
isTranslucentMenuColor(params.menuColor ?? undefined)
) {
return {
...params,
menuAccent: "subtle",
}
}
return params
}
function normalizeDesignSystemParams(
params: DesignSystemSearchParams
): DesignSystemSearchParams {
let result = {
...params,
fontHeading: normalizeFontHeading(params.font, params.fontHeading),
}
// Validate theme and chartColor against baseColor.
if (result.baseColor) {
const available = getThemesForBaseColor(result.baseColor)
const themeValid = available.some((t) => t.name === result.theme)
const chartColorValid = available.some((t) => t.name === result.chartColor)
if (!themeValid || !chartColorValid) {
const fallback = (available[0]?.name ?? result.baseColor) as ThemeName
result = {
...result,
...(!themeValid && { theme: fallback }),
...(!chartColorValid && { chartColor: fallback as ChartColorName }),
}
}
}
if (
result.menuAccent === "bold" &&
isTranslucentMenuColor(result.menuColor)
) {
return {
...result,
menuAccent: "subtle",
}
}
return result
}
// If preset param exists, decode it and overlay on raw params.
// V1 presets don't encode chartColor — fall back to the colored
// theme that base-color themes originally borrowed charts from.
type SearchParamsLike = Pick<URLSearchParams, "get" | "has">
function resolvePresetParams(
rawParams: DesignSystemSearchParams,
searchParams: SearchParamsLike
) {
if (rawParams.preset && isPresetCode(rawParams.preset)) {
const decoded = decodePreset(rawParams.preset)
if (decoded) {
const presetOverrides = resolvePresetOverrides(searchParams, decoded)
return normalizeDesignSystemParams({
...decoded,
...presetOverrides,
base: rawParams.base,
item: rawParams.item,
preset: rawParams.preset,
template: rawParams.template,
rtl: rawParams.rtl,
pointer: rawParams.pointer,
size: rawParams.size,
custom: rawParams.custom,
})
}
}
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: true,
history: "push",
...options,
})
const params = React.useMemo(
() => resolvePresetParams(rawParams, searchParams),
[rawParams, searchParams]
)
// Use ref so setParams callback stays stable across renders.
const paramsRef = React.useRef(params)
React.useEffect(() => {
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(
(
updates:
| Partial<DesignSystemSearchParams>
| ((
old: DesignSystemSearchParams
) => Partial<DesignSystemSearchParams>),
setOptions?: Options
) => {
const resolvedUpdates = normalizePartialDesignSystemParams(
typeof updates === "function" ? updates(paramsRef.current) : updates
)
const hasDesignSystemUpdate = DESIGN_SYSTEM_KEYS.some(
(key) => key in resolvedUpdates
)
if (!hasDesignSystemUpdate) {
// No design system change, pass through directly.
return rawSetParams(resolvedUpdates as RawSetParamsInput, setOptions)
}
// Merge current decoded values with updates.
const merged = normalizeDesignSystemParams({
...paramsRef.current,
...resolvedUpdates,
})
return rawSetParams(
buildPresetUrlUpdate(merged, resolvedUpdates) as RawSetParamsInput,
setOptions
)
},
[rawSetParams]
)
return [params, setParams] as const
}

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

@@ -7,7 +7,7 @@ export const TEMPLATES = [
{
value: "vite",
title: "Vite",
logo: '<svg xmlns="http://www.w3.org/2000/svg" width="410" height="404" fill="none" viewBox="0 0 410 404"><path fill="var(--foreground)" d="m399.641 59.525-183.998 329.02c-3.799 6.793-13.559 6.833-17.415.073L10.582 59.556C6.38 52.19 12.68 43.266 21.028 44.76l184.195 32.923c1.175.21 2.378.208 3.553-.006l180.343-32.87c8.32-1.517 14.649 7.337 10.522 14.719"/><path fill="var(--background)" d="M292.965 1.574 156.801 28.255a5 5 0 0 0-4.03 4.611l-8.376 141.464c-.197 3.332 2.863 5.918 6.115 5.168l37.91-8.749c3.547-.818 6.752 2.306 6.023 5.873l-11.263 55.153c-.758 3.712 2.727 6.886 6.352 5.785l23.415-7.114c3.63-1.102 7.118 2.081 6.35 5.796l-17.899 86.633c-1.12 5.419 6.088 8.374 9.094 3.728l2.008-3.103 110.954-221.428c1.858-3.707-1.346-7.935-5.418-7.15l-39.022 7.532c-3.667.707-6.787-2.708-5.752-6.296l25.469-88.291c1.036-3.594-2.095-7.012-5.766-6.293"/></svg>',
logo: '<svg xmlns="http://www.w3.org/2000/svg" width="410" height="404" fill="none" viewBox="0 0 410 404"><path fill="var(--foreground)" d="m399.641 59.525-183.998 329.02c-3.799 6.793-13.559 6.833-17.415.073L10.582 59.556C6.38 52.19 12.68 43.266 21.028 44.76l184.195 32.923c1.175.21 2.378.208 3.553-.006l180.343-32.87c8.32-1.517 14.649 7.337 10.522 14.719"/><path fill="var(--color-neutral-800)" d="M292.965 1.574 156.801 28.255a5 5 0 0 0-4.03 4.611l-8.376 141.464c-.197 3.332 2.863 5.918 6.115 5.168l37.91-8.749c3.547-.818 6.752 2.306 6.023 5.873l-11.263 55.153c-.758 3.712 2.727 6.886 6.352 5.785l23.415-7.114c3.63-1.102 7.118 2.081 6.35 5.796l-17.899 86.633c-1.12 5.419 6.088 8.374 9.094 3.728l2.008-3.103 110.954-221.428c1.858-3.707-1.346-7.935-5.418-7.15l-39.022 7.532c-3.667.707-6.787-2.708-5.752-6.296l25.469-88.291c1.036-3.594-2.095-7.012-5.766-6.293"/></svg>',
},
{
value: "start",

View File

@@ -0,0 +1,115 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { DEFAULT_CONFIG } from "@/registry/config"
import { buildV0Payload } from "@/app/(app)/(create)/lib/v0"
vi.mock("shadcn/schema", async () => {
return await vi.importActual("shadcn/schema")
})
vi.mock("shadcn/utils", async () => {
const actual = (await vi.importActual("shadcn/utils")) as {
transformFont: unknown
}
return {
transformFont: actual.transformFont,
transformIcons: async ({ sourceFile }: { sourceFile: unknown }) =>
sourceFile,
transformMenu: async ({ sourceFile }: { sourceFile: unknown }) =>
sourceFile,
transformRender: async ({ sourceFile }: { sourceFile: unknown }) =>
sourceFile,
}
})
vi.mock("@/registry/bases/__index__", () => ({
Index: {
base: {
card: {
name: "card",
type: "registry:ui",
},
},
radix: {
card: {
name: "card",
type: "registry:ui",
},
},
},
}))
describe("buildV0Payload", () => {
beforeEach(() => {
process.env.NEXT_PUBLIC_APP_URL = "http://example.test"
vi.stubGlobal(
"fetch",
vi.fn(async (input: string | URL | Request) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url
const name = url.split("/").pop()?.replace(".json", "") ?? "component"
return new Response(
JSON.stringify({
name,
type: "registry:ui",
files: [
{
path: `registry/base-nova/ui/${name}.tsx`,
type: "registry:ui",
content: `import * as React from "react"\n\nexport function Component() {\n return <div className="cn-font-heading text-xl" />\n}\n`,
},
],
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
)
})
)
})
afterEach(() => {
vi.unstubAllGlobals()
delete process.env.NEXT_PUBLIC_APP_URL
})
it("rewrites cn-font-heading to font-heading when heading inherits the body font", async () => {
const payload = await buildV0Payload({
...DEFAULT_CONFIG,
item: undefined,
fontHeading: "inherit",
})
const cardFile = payload.files?.find(
(file) => file.target === "components/ui/card.tsx"
)
expect(cardFile?.content).toContain("font-heading")
expect(cardFile?.content).not.toContain("cn-font-heading")
})
it("rewrites cn-font-heading to font-heading when a distinct heading font is selected", async () => {
const payload = await buildV0Payload({
...DEFAULT_CONFIG,
item: undefined,
fontHeading: "playfair-display",
})
const cardFile = payload.files?.find(
(file) => file.target === "components/ui/card.tsx"
)
expect(cardFile?.content).toContain("font-heading")
expect(cardFile?.content).not.toContain("cn-font-heading")
})
})

View File

@@ -5,19 +5,28 @@ import {
type configSchema,
type RegistryItem,
} from "shadcn/schema"
import { transformIcons, transformMenu, transformRender } from "shadcn/utils"
import { Project, ScriptKind } from "ts-morph"
import {
transformFont,
transformIcons,
transformMenu,
transformRender,
} from "shadcn/utils"
import { Project, ScriptKind, type SourceFile } from "ts-morph"
import { z } from "zod"
import {
buildRegistryBase,
fonts,
getBodyFont,
getHeadingFont,
getInheritedHeadingFontValue,
type DesignSystemConfig,
} from "@/registry/config"
const { Index } = await import("@/registry/bases/__index__")
const THEME_INLINE = `--font-sans: var(--font-sans);
function buildThemeInline(fontHeadingValue: string) {
return `--font-sans: var(--font-sans);
--font-heading: ${fontHeadingValue};
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--color-background: var(--background);
@@ -59,6 +68,7 @@ const THEME_INLINE = `--font-sans: var(--font-sans);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);`
}
// Static file — parsed once at module level.
const themeProviderFile = registryItemFileSchema.parse({
@@ -148,7 +158,20 @@ const ALIASES = {
hooks: "@/hooks",
} as const
const transformers = [transformIcons, transformMenu, transformRender]
type V0Transformer = (opts: {
filename: string
raw: string
sourceFile: SourceFile
config: z.infer<typeof configSchema>
supportedFontMarkers?: string[]
}) => Promise<unknown>
const transformers: V0Transformer[] = [
transformIcons as V0Transformer,
transformMenu as V0Transformer,
transformRender as V0Transformer,
transformFont as V0Transformer,
]
function getStyle(designSystemConfig: DesignSystemConfig) {
return `${designSystemConfig.base}-${designSystemConfig.style}`
@@ -156,10 +179,18 @@ function getStyle(designSystemConfig: DesignSystemConfig) {
export async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
const registryBase = buildRegistryBase(designSystemConfig)
const normalizedFontHeading =
designSystemConfig.fontHeading === designSystemConfig.font
? "inherit"
: designSystemConfig.fontHeading
// Only buildComponentFiles is async — run sync builders directly.
const globalsCss = buildGlobalsCss(registryBase)
const layoutFile = buildLayoutFile(designSystemConfig)
const globalsCss = buildGlobalsCss(
registryBase,
designSystemConfig.font,
normalizedFontHeading
)
const layoutFile = buildLayoutFile(designSystemConfig, normalizedFontHeading)
const componentFiles = await buildComponentFiles(designSystemConfig)
const dependencies = [...(registryBase.dependencies ?? []), "next-themes"]
@@ -181,7 +212,11 @@ export async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
})
}
function buildGlobalsCss(registryBase: RegistryItem) {
function buildGlobalsCss(
registryBase: RegistryItem,
font: DesignSystemConfig["font"],
fontHeading: DesignSystemConfig["fontHeading"]
) {
const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
@@ -194,11 +229,15 @@ function buildGlobalsCss(registryBase: RegistryItem) {
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@custom-variant dark (&:is(.dark *));
@theme inline {
${THEME_INLINE}
}
@theme inline {
${buildThemeInline(
fontHeading === "inherit"
? getInheritedHeadingFontValue(font)
: "var(--font-heading)"
)}
}
:root {
${lightVars}
@@ -332,18 +371,23 @@ function buildPackageJson(dependencies: string[]) {
})
}
function buildLayoutFile(designSystemConfig: DesignSystemConfig) {
const font = fonts.find(
(font) => font.name === `font-${designSystemConfig.font}`
)
function buildLayoutFile(
designSystemConfig: DesignSystemConfig,
fontHeading: DesignSystemConfig["fontHeading"]
) {
const font = getBodyFont(designSystemConfig.font)
if (!font) {
throw new Error(`Font "${designSystemConfig.font}" not found`)
}
const headingFont =
fontHeading === "inherit" ? undefined : getHeadingFont(fontHeading)
// Derive const name from the font's CSS variable (e.g. --font-sans → fontSans).
const constName = font.font.variable
.replace(/^--/, "")
.replace(/-./g, (m) => m[1].toUpperCase())
const headingConstName = "fontHeading"
// Add font-serif or font-mono class to body when needed. Sans is the default.
const fontClass =
@@ -354,14 +398,26 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) {
: ""
const bodyClassName = fontClass ? `antialiased ${fontClass}` : "antialiased"
const imports = headingFont
? Array.from(new Set([font.font.import, headingFont.font.import])).join(
", "
)
: font.font.import
const headingDeclaration = headingFont
? `\nconst ${headingConstName} = ${headingFont.font.import}({subsets:['latin'],variable:'${headingFont.font.variable}'});\n`
: ""
const htmlClassName = headingFont
? `{${constName}.variable + " " + ${headingConstName}.variable}`
: `{${constName}.variable}`
const content = dedent`
import type { Metadata } from "next";
import { ${font.font.import} } from "next/font/google";
import { ${imports} } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
const ${constName} = ${font.font.import}({subsets:['latin'],variable:'${font.font.variable}'});
${headingDeclaration}
export const metadata: Metadata = {
title: "Create Next App",
@@ -374,7 +430,7 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) {
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning className={${constName}.variable}>
<html lang="en" suppressHydrationWarning className=${htmlClassName}>
<body
className="${bodyClassName}"
>
@@ -401,13 +457,14 @@ async function buildComponentFiles(designSystemConfig: DesignSystemConfig) {
// Build config once for all components.
const config = buildTransformConfig(designSystemConfig)
// Fetch UI components and the item component in parallel.
const [registryItemFiles, itemComponentFile] = await Promise.all([
// Fetch UI components, the demo, and the item component in parallel.
const [registryItemFiles, demoFile, itemComponentFile] = await Promise.all([
Promise.all(
allItemsForBase.map((name) =>
getRegistryItemFile(name, designSystemConfig, config)
)
),
getRegistryItemFile("demo", designSystemConfig, config),
designSystemConfig.item
? getRegistryItemFile(designSystemConfig.item, designSystemConfig, config)
: null,
@@ -415,29 +472,24 @@ async function buildComponentFiles(designSystemConfig: DesignSystemConfig) {
const files = [...registryItemFiles.filter(Boolean)]
// Include the demo component.
if (demoFile) {
files.push({
...demoFile,
target: "components/demo.tsx",
type: "registry:component",
})
}
const pageFile = {
path: "app/page.tsx",
type: "registry:page",
target: "app/page.tsx",
content: dedent`
import { Button } from "@/components/ui/button"
import { Demo } from "@/components/demo"
export default function Page() {
return (
<div className="flex min-h-svh p-6">
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
<div>
<h1 className="font-medium">Project ready!</h1>
<p>You may now add components and start building.</p>
<p>We&apos;ve already added the button component for you.</p>
<Button className="mt-2">Button</Button>
</div>
<div className="font-mono text-xs text-muted-foreground">
(Press <kbd>d</kbd> to toggle dark mode)
</div>
</div>
</div>
)
return <Demo />
}
`,
}
@@ -557,6 +609,7 @@ async function transformFileContent(
raw: content,
sourceFile,
config,
supportedFontMarkers: ["cn-font-heading"],
})
}

View File

@@ -0,0 +1,94 @@
import {
AlertCircleIcon,
ArrowRight01Icon,
SquareLock02Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import { Field, FieldGroup, FieldLabel } from "@/styles/base-rhea/ui/field"
import { Input } from "@/styles/base-rhea/ui/input"
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/styles/base-rhea/ui/item"
export function AccountAccess() {
return (
<Card>
<CardHeader>
<CardTitle>Account Access</CardTitle>
<CardDescription>
Update your credentials or re-authenticate.
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup>
<Field>
<FieldLabel htmlFor="email-address">Email Address</FieldLabel>
<Input
id="email-address"
type="email"
placeholder="artist@studio.inc"
/>
</Field>
<Field>
<div className="flex items-center justify-between">
<FieldLabel htmlFor="current-password">
Current Password
</FieldLabel>
<a
href="#"
className="text-xs font-medium tracking-wider text-muted-foreground uppercase hover:text-foreground"
>
Forgot?
</a>
</div>
<Input
id="current-password"
type="password"
placeholder="••••••••••••••••••••••••"
/>
</Field>
</FieldGroup>
</CardContent>
<CardFooter className="flex-col gap-4">
<Button className="w-full">
<HugeiconsIcon icon={SquareLock02Icon} strokeWidth={2} />
Update Security
</Button>
<Item variant="muted" render={<a href="#" />}>
<ItemMedia variant="icon">
<HugeiconsIcon
icon={AlertCircleIcon}
className="text-destructive"
strokeWidth={2}
/>
</ItemMedia>
<ItemContent>
<ItemTitle>Danger Zone</ItemTitle>
<ItemDescription className="line-clamp-1">
Archive account and remove catalog
</ItemDescription>
</ItemContent>
<HugeiconsIcon
icon={ArrowRight01Icon}
className="size-4"
strokeWidth={2}
/>
</Item>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,46 @@
import { Badge } from "@/styles/base-rhea/ui/badge"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Card,
CardAction,
CardDescription,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
const areaPath = "M0 52L18 40L36 46L54 70L72 50L100 49V86H0Z"
const strokePath = "M0 52L18 40L36 46L54 70L72 50L100 49"
export function AnalyticsCard() {
return (
<Card className="mx-auto w-full max-w-sm data-[size=sm]:pb-0" size="sm">
<CardHeader>
<CardTitle>Analytics</CardTitle>
<CardDescription>
418.2K Visitors <Badge>+10%</Badge>
</CardDescription>
<CardAction>
<Button variant="outline" size="sm">
View Analytics
</Button>
</CardAction>
</CardHeader>
<svg
viewBox="0 0 100 86"
preserveAspectRatio="none"
className="aspect-[1/0.35] w-full text-chart-1"
role="img"
aria-label="Visitor trend"
>
<path d={areaPath} fill="currentColor" opacity="0.28" />
<path
d={strokePath}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
vectorEffect="non-scaling-stroke"
/>
</svg>
</Card>
)
}

View File

@@ -0,0 +1,75 @@
import { Badge } from "@/styles/base-rhea/ui/badge"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import { Item, ItemContent } from "@/styles/base-rhea/ui/item"
import { Separator } from "@/styles/base-rhea/ui/separator"
const netRoyalties = 1248.75
const processingFee = 37.46
const totalClaimable = netRoyalties - processingFee
const formatCurrency = (amount: number) =>
amount.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
export function ClaimableBalance() {
return (
<Card>
<CardHeader>
<CardDescription>Claimable Balance</CardDescription>
<CardTitle className="text-4xl tabular-nums">
${formatCurrency(totalClaimable)}
</CardTitle>
<Badge variant="outline">
<span className="size-2 rounded-full bg-yellow-500" />
Pending Setup
</Badge>
</CardHeader>
<CardContent className="flex flex-1 flex-col justify-end">
<Item variant="muted" className="flex-col items-stretch">
<ItemContent className="gap-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
Net Royalties
</span>
<span className="text-sm font-medium tabular-nums">
${formatCurrency(netRoyalties)}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
Processing Fee
</span>
<span className="text-sm font-medium tabular-nums">
-${formatCurrency(processingFee)}
</span>
</div>
<Separator />
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
Total Ready to Claim
</span>
<span className="text-sm font-semibold tabular-nums">
${formatCurrency(totalClaimable)} USD
</span>
</div>
</ItemContent>
</Item>
</CardContent>
<CardFooter>
<CardDescription>
Once your bank is connected, balances over $10.00 are automatically
eligible for monthly distribution on the 15th of each month.
</CardDescription>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,88 @@
import { Badge } from "@/styles/base-rhea/ui/badge"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import { Item, ItemContent, ItemDescription } from "@/styles/base-rhea/ui/item"
const chartData = [
{ month: "Dec", amount: 800 },
{ month: "Jan", amount: 1100 },
{ month: "Feb", amount: 900 },
{ month: "Mar", amount: 1300 },
{ month: "Apr", amount: 750 },
]
export function ContributionHistory() {
const maxAmount = Math.max(...chartData.map((item) => item.amount))
return (
<Card>
<CardHeader>
<CardTitle>Contribution History</CardTitle>
<CardDescription>Last 6 months of activity</CardDescription>
</CardHeader>
<CardContent>
<div
className="flex h-[200px] w-full items-end gap-3"
role="img"
aria-label="Last 6 months of contribution activity"
>
{chartData.map((item, index) => (
<div
key={item.month}
className="flex h-full flex-1 flex-col justify-end gap-2"
>
<div
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">
{item.month}
</span>
</div>
))}
</div>
</CardContent>
<CardContent>
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
<Item variant="muted" className="flex-col items-stretch">
<ItemContent className="gap-1">
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
Upcoming
</ItemDescription>
<span className="cn-font-heading text-base font-semibold">
May 2024
</span>
<span className="text-sm text-muted-foreground">Scheduled</span>
</ItemContent>
</Item>
<Item
variant="muted"
className="hidden flex-col items-stretch xl:flex"
>
<ItemContent className="gap-1">
<ItemDescription className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
Savings Plan
</ItemDescription>
<span className="cn-font-heading text-base font-semibold">
Accelerated
</span>
<span className="text-sm text-muted-foreground">Recurring</span>
</ItemContent>
</Item>
</div>
</CardContent>
<CardFooter>
<Button className="w-full">View Full Report</Button>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,116 @@
import { Cancel01Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemTitle,
} from "@/styles/base-rhea/ui/item"
const HOLDINGS = [
{
name: "Vanguard",
shares: "450 Shares",
amount: "$1,842.10",
data: [
{ q: "Q1", value: 380 },
{ q: "Q2", value: 420 },
{ q: "Q3", value: 390 },
{ q: "Q4", value: 652 },
],
},
{
name: "S&P 500 VOO",
shares: "112 Shares",
amount: "$928.40",
data: [
{ q: "Q1", value: 180 },
{ q: "Q2", value: 210 },
{ q: "Q3", value: 320 },
{ q: "Q4", value: 218 },
],
},
{
name: "Apple AAPL",
shares: "85 Shares",
amount: "$340.00",
data: [
{ q: "Q1", value: 60 },
{ q: "Q2", value: 70 },
{ q: "Q3", value: 120 },
{ q: "Q4", value: 90 },
],
},
{
name: "Realty Income",
shares: "320 Shares",
amount: "$1,139.50",
data: [
{ q: "Q1", value: 240 },
{ q: "Q2", value: 260 },
{ q: "Q3", value: 280 },
{ q: "Q4", value: 360 },
],
},
]
export function DividendIncome() {
return (
<Card>
<CardHeader>
<CardTitle>Q2 Dividend Income</CardTitle>
<CardDescription>
Quarterly dividend payouts across your portfolio holdings.
</CardDescription>
<CardAction>
<Button
variant="ghost"
size="icon-sm"
className="bg-muted"
aria-label="Dismiss dividend income"
>
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
</Button>
</CardAction>
</CardHeader>
<CardContent>
<ItemGroup>
{HOLDINGS.map((holding) => (
<Item key={holding.name} role="listitem" variant="muted">
<ItemContent>
<ItemTitle>{holding.name}</ItemTitle>
<ItemDescription>{holding.shares}</ItemDescription>
</ItemContent>
<div
className="hidden h-8 w-24 items-end gap-1 md:flex"
role="img"
aria-label={`${holding.name} quarterly dividends`}
>
{holding.data.map((item) => (
<div
key={item.q}
className="min-h-1 flex-1 rounded-t-sm bg-chart-2"
style={{
height: `${(item.value / Math.max(...holding.data.map((point) => point.value))) * 100}%`,
}}
/>
))}
</div>
</Item>
))}
</ItemGroup>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,37 @@
import { Add01Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { Button } from "@/styles/base-rhea/ui/button"
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/styles/base-rhea/ui/empty"
export function EmptyDistributeTrack() {
return (
<Card>
<CardContent>
<Empty className="p-4">
<EmptyMedia variant="icon">
<HugeiconsIcon icon={Add01Icon} strokeWidth={2} />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>Distribute Track</EmptyTitle>
<EmptyDescription>
Upload your first master to start reaching listeners on Spotify,
Apple Music, and more.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>Create Release</Button>
</EmptyContent>
</Empty>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,122 @@
import { MessageScrollerDemo } from "@/examples/radix/message-scroller-demo"
import { AccountAccess } from "./account-access"
import { AnalyticsCard } from "./analytics-card"
import { ClaimableBalance } from "./claimable-balance"
import { ContributionHistory } from "./contribution-history"
import { DividendIncome } from "./dividend-income"
import { EmptyDistributeTrack } from "./empty-distribute-track"
import { NewMilestone } from "./new-milestone"
import { NotificationSettings } from "./notification-settings"
import { Payments } from "./payments"
import { PayoutThreshold } from "./payout-threshold"
import { PowerUsage } from "./power-usage"
import { QrConnect } from "./qr-connect"
import { SavingsTargets } from "./savings-targets"
import { SidebarNav } from "./sidebar-nav"
import { AccountAccess as SkeletonAccountAccess } from "./skeleton/account-access"
import { AnalyticsCard as SkeletonAnalyticsCard } from "./skeleton/analytics-card"
import { ClaimableBalance as SkeletonClaimableBalance } from "./skeleton/claimable-balance"
import { ContributionHistory as SkeletonContributionHistory } from "./skeleton/contribution-history"
import { DividendIncome as SkeletonDividendIncome } from "./skeleton/dividend-income"
import { EmptyDistributeTrack as SkeletonEmptyDistributeTrack } from "./skeleton/empty-distribute-track"
import { NewMilestone as SkeletonNewMilestone } from "./skeleton/new-milestone"
import { NotificationSettings as SkeletonNotificationSettings } from "./skeleton/notification-settings"
import { Payments as SkeletonPayments } from "./skeleton/payments"
import { PayoutThreshold as SkeletonPayoutThreshold } from "./skeleton/payout-threshold"
import { PowerUsage as SkeletonPowerUsage } from "./skeleton/power-usage"
import { QrConnect as SkeletonQrConnect } from "./skeleton/qr-connect"
import { SavingsTargets as SkeletonSavingsTargets } from "./skeleton/savings-targets"
import { TransferFunds as SkeletonTransferFunds } from "./skeleton/transfer-funds"
import { UIElements as SkeletonUIElements } from "./skeleton/ui-elements"
import { TransferFunds } from "./transfer-funds"
import { UIElements } from "./ui-elements"
function CardsSkeletonRails() {
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-12 z-10 hidden min-[2200px]:block [&_[data-slot=skeleton]:nth-child(even)]:hidden"
>
<div className="absolute top-0 left-[calc(50%-950px-var(--rail-width)-var(--gap))] grid w-(--rail-width) grid-cols-[repeat(2,var(--rail-column))] gap-(--gap) opacity-50 [--rail-column:20rem] [--rail-width:calc(var(--rail-column)*2+var(--gap))]">
<div className="flex flex-col gap-(--gap)">
<SkeletonContributionHistory />
<SkeletonClaimableBalance />
<SkeletonDividendIncome />
<SkeletonPayoutThreshold />
</div>
<div className="flex flex-col gap-(--gap)">
<SkeletonUIElements />
<SkeletonSavingsTargets />
<SkeletonNewMilestone />
<SkeletonPayoutThreshold />
<SkeletonAccountAccess />
</div>
</div>
<div className="absolute top-0 right-[calc(50%-950px-var(--rail-width)-var(--gap))] grid w-(--rail-width) grid-cols-[repeat(2,var(--rail-column))] gap-(--gap) opacity-50 [--rail-column:20rem] [--rail-width:calc(var(--rail-column)*2+var(--gap))]">
<div className="flex flex-col gap-(--gap)">
<SkeletonNewMilestone />
<SkeletonPayoutThreshold />
<SkeletonAccountAccess />
<SkeletonQrConnect />
<SkeletonTransferFunds />
<SkeletonPayments />
<SkeletonEmptyDistributeTrack />
</div>
<div className="flex flex-col gap-(--gap)">
<SkeletonQrConnect />
<SkeletonTransferFunds />
<SkeletonPayments />
<SkeletonEmptyDistributeTrack />
<SkeletonAnalyticsCard />
<SkeletonNotificationSettings />
<SkeletonPowerUsage />
</div>
</div>
</div>
)
}
export function CardsDemo() {
return (
<div
data-slot="demo"
className="theme-neutral relative flex w-full max-w-none flex-col gap-(--gap) overflow-hidden bg-muted p-12 pb-0! [--gap:--spacing(8)] 3xl:[--gap:--spacing(8)] min-[1900px]:p-12 min-[1900px]:[--gap:--spacing(10)]! lg:p-6 lg:[--gap:--spacing(6)] dark:bg-background"
>
<CardsSkeletonRails />
<div className="relative z-10 mx-auto grid gap-(--gap) **:data-[slot=card]:w-full min-[1400px]:grid-cols-4! min-[1900px]:grid-cols-5! md:max-w-3xl md:grid-cols-2 lg:max-w-none lg:grid-cols-3 xl:max-w-[1600px] 2xl:max-w-[1900px]">
<div className="flex flex-col items-start gap-(--gap)">
<UIElements />
<SidebarNav />
<SavingsTargets />
</div>
<div className="hidden flex-col gap-(--gap) lg:flex">
<ContributionHistory />
<ClaimableBalance />
<DividendIncome />
</div>
<div className="hidden flex-col gap-(--gap) min-[1400px]:flex">
<NewMilestone />
<PayoutThreshold />
<AccountAccess />
</div>
<div className="hidden flex-col gap-(--gap) md:flex">
<QrConnect />
<div className="**:[.text-center.text-xs]:hidden">
<MessageScrollerDemo />
</div>
{/* <TransferFunds /> */}
<Payments />
</div>
<div className="hidden flex-col gap-(--gap) min-[1900px]:flex">
<EmptyDistributeTrack />
<AnalyticsCard />
<NotificationSettings />
<PowerUsage />
</div>
</div>
<div className="absolute inset-x-0 top-0 z-1 h-120 bg-linear-to-b from-background via-muted to-transparent dark:hidden" />
<div className="absolute inset-x-0 bottom-0 z-20 h-48 bg-linear-to-t from-background via-muted/80 to-transparent lg:h-80 xl:h-64 dark:via-background/80" />
</div>
)
}

View File

@@ -0,0 +1,52 @@
import { Button } from "@/styles/base-rhea/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import { Field, FieldGroup, FieldLabel } from "@/styles/base-rhea/ui/field"
import { Input } from "@/styles/base-rhea/ui/input"
export function NewMilestone() {
return (
<Card>
<CardHeader>
<CardTitle>Set a new milestone</CardTitle>
<CardDescription>
Define your financial target and we&apos;ll help you pace your
savings.
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup>
<Field>
<FieldLabel htmlFor="goal-name">Goal Name</FieldLabel>
<Input
id="goal-name"
placeholder="e.g. New Car, Home Downpayment"
/>
</Field>
<div className="grid grid-cols-2 gap-3">
<Field>
<FieldLabel htmlFor="target-amount">Target Amount</FieldLabel>
<Input id="target-amount" defaultValue="$15,000" />
</Field>
<Field>
<FieldLabel htmlFor="target-date">Target Date</FieldLabel>
<Input id="target-date" defaultValue="Dec 2025" />
</Field>
</div>
</FieldGroup>
</CardContent>
<CardFooter className="flex-col gap-2">
<Button className="w-full">Create Goal</Button>
<Button variant="outline" className="w-full">
Cancel
</Button>
</CardFooter>
</Card>
)
}

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