Compare commits

...

94 Commits

Author SHA1 Message Date
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
1642 changed files with 156382 additions and 37962 deletions

View File

@@ -1,5 +0,0 @@
---
"shadcn": patch
---
fix failing version derivation test

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

View File

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

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

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
@@ -73,25 +33,20 @@ jobs:
node <<'EOF'
const fs = require("node:fs")
const files = [
"apps/v4/public/r/registries.json",
"apps/v4/registry/directory.json",
]
const file = "apps/v4/registry/directory.json"
const reservedNamespaces = new Set(
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
)
function readNames(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
function readNames() {
return JSON.parse(fs.readFileSync(file, "utf8")).map(
(entry) => entry.name
)
}
const violations = files.flatMap((filePath) => {
return readNames(filePath)
.filter((name) => reservedNamespaces.has(name))
.map((name) => `${filePath}: ${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:")
@@ -108,7 +63,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

8
.gitignore vendored
View File

@@ -43,5 +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

@@ -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-blue 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>
)
}

View File

@@ -0,0 +1,76 @@
import { Button } from "@/styles/base-rhea/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import { Checkbox } from "@/styles/base-rhea/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/styles/base-rhea/ui/field"
const NOTIFICATIONS = [
{
id: "transactions",
label: "Transaction alerts",
description: "Deposits, withdrawals, and transfers.",
defaultChecked: true,
},
{
id: "security",
label: "Security alerts",
description: "Login attempts and account changes.",
defaultChecked: true,
},
{
id: "goals",
label: "Goal milestones",
description: "Updates at 25%, 50%, 75%, and 100%.",
defaultChecked: false,
},
{
id: "market",
label: "Market updates",
description: "Daily portfolio summary and price alerts.",
defaultChecked: false,
},
]
export function NotificationSettings() {
return (
<Card>
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>
Choose which email and push alerts you want to receive.
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup>
{NOTIFICATIONS.map((n) => (
<Field key={n.id} orientation="horizontal">
<Checkbox
id={`notify-${n.id}`}
defaultChecked={n.defaultChecked}
/>
<FieldContent>
<FieldLabel htmlFor={`notify-${n.id}`}>{n.label}</FieldLabel>
<FieldDescription>{n.description}</FieldDescription>
</FieldContent>
</Field>
))}
</FieldGroup>
</CardContent>
<CardFooter>
<Button className="w-full">Save Preferences</Button>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,139 @@
import {
ArrowRight01Icon,
Calendar03Icon,
MoreHorizontalCircle01Icon,
RefreshIcon,
Settings01Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/styles/base-rhea/ui/breadcrumb"
import { Button } from "@/styles/base-rhea/ui/button"
import { Card, CardContent, CardHeader } from "@/styles/base-rhea/ui/card"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/styles/base-rhea/ui/dropdown-menu"
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/styles/base-rhea/ui/item"
export function Payments() {
return (
<Card>
<CardHeader className="flex flex-col gap-3">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
size="icon-sm"
variant="ghost"
aria-label="Account options"
/>
}
>
<HugeiconsIcon
icon={MoreHorizontalCircle01Icon}
strokeWidth={2}
/>
<span className="sr-only">Account options</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuGroup>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Statements</DropdownMenuItem>
<DropdownMenuItem>Documents</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Payments</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</CardHeader>
<CardContent>
<ItemGroup>
<div role="listitem" className="w-full">
<Item variant="muted" render={<a href="#" />}>
<ItemMedia variant="icon">
<HugeiconsIcon icon={Settings01Icon} strokeWidth={2} />
</ItemMedia>
<ItemContent>
<ItemTitle>Change transfer limit</ItemTitle>
<ItemDescription>
Adjust how much you can send from your balance.
</ItemDescription>
</ItemContent>
<HugeiconsIcon
icon={ArrowRight01Icon}
className="size-4 shrink-0 text-muted-foreground"
strokeWidth={2}
/>
</Item>
</div>
<div role="listitem" className="w-full">
<Item variant="muted" render={<a href="#" />}>
<ItemMedia variant="icon">
<HugeiconsIcon icon={Calendar03Icon} strokeWidth={2} />
</ItemMedia>
<ItemContent>
<ItemTitle>Scheduled transfers</ItemTitle>
<ItemDescription>
Set up a transfer to send at a later date.
</ItemDescription>
</ItemContent>
<HugeiconsIcon
icon={ArrowRight01Icon}
className="size-4 shrink-0 text-muted-foreground"
strokeWidth={2}
/>
</Item>
</div>
<div role="listitem" className="w-full">
<Item variant="muted" render={<a href="#" />}>
<ItemMedia variant="icon">
<HugeiconsIcon icon={RefreshIcon} strokeWidth={2} />
</ItemMedia>
<ItemContent>
<ItemTitle>Recurring card payments</ItemTitle>
<ItemDescription>
Manage your repeated card transactions.
</ItemDescription>
</ItemContent>
<HugeiconsIcon
icon={ArrowRight01Icon}
className="size-4 shrink-0 text-muted-foreground"
strokeWidth={2}
/>
</Item>
</div>
</ItemGroup>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,112 @@
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,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/styles/base-rhea/ui/field"
import { Progress } from "@/styles/base-rhea/ui/progress"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/styles/base-rhea/ui/select"
import { Textarea } from "@/styles/base-rhea/ui/textarea"
const CURRENCIES = [
{ label: "USD — United States Dollar", value: "usd" },
{ label: "EUR — Euro", value: "eur" },
{ label: "GBP — British Pound", value: "gbp" },
{ label: "JPY — Japanese Yen", value: "jpy" },
]
export function PayoutThreshold() {
return (
<Card>
<CardHeader>
<CardTitle>Payout Threshold</CardTitle>
<CardDescription>
Set the minimum balance required before a payout is triggered.
</CardDescription>
<CardAction>
<Button
variant="ghost"
size="icon-sm"
className="bg-muted"
aria-label="Dismiss payout threshold"
>
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
</Button>
</CardAction>
</CardHeader>
<CardContent>
<FieldGroup>
<Field>
<FieldLabel htmlFor="preferred-currency">
Preferred Currency
</FieldLabel>
<Select items={CURRENCIES} defaultValue="usd">
<SelectTrigger id="preferred-currency" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{CURRENCIES.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<div className="flex items-baseline justify-between">
<FieldLabel id="min-payout-label">
Minimum Payout Amount
</FieldLabel>
<span className="text-2xl font-semibold tabular-nums">
$2500.00
</span>
</div>
<Progress
value={25}
aria-labelledby="min-payout-label"
aria-valuetext="$2,500 of $10,000"
/>
<div className="flex items-center justify-between">
<FieldDescription>$50 (MIN)</FieldDescription>
<FieldDescription>$10,000 (MAX)</FieldDescription>
</div>
</Field>
<Field>
<FieldLabel htmlFor="payout-notes">Notes</FieldLabel>
<Textarea
id="payout-notes"
placeholder="Add any notes for this payout configuration..."
className="min-h-[100px]"
/>
</Field>
</FieldGroup>
</CardContent>
<CardFooter>
<Button className="w-full">Save Threshold</Button>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,67 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import { Separator } from "@/styles/base-rhea/ui/separator"
const chartData = [
{ hour: "6a", usage: 1.2 },
{ hour: "8a", usage: 2.8 },
{ hour: "10a", usage: 3.1 },
{ hour: "12p", usage: 2.4 },
{ hour: "2p", usage: 3.4 },
{ hour: "4p", usage: 2.9 },
{ hour: "6p", usage: 3.8 },
{ hour: "8p", usage: 3.2 },
]
export function PowerUsage() {
const maxUsage = Math.max(...chartData.map((item) => item.usage))
return (
<Card>
<CardHeader>
<CardTitle>Power Usage</CardTitle>
<CardDescription>Whole Home</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div
className="flex h-[140px] w-full items-end gap-2"
role="img"
aria-label="Power usage by hour"
>
{chartData.map((item) => (
<div
key={item.hour}
className="flex h-full flex-1 flex-col justify-end gap-1.5"
>
<div
className="min-h-2 rounded-t bg-chart-2"
style={{ height: `${(item.usage / maxUsage) * 100}%` }}
/>
<span className="text-center text-xs text-muted-foreground">
{item.hour}
</span>
</div>
))}
</div>
<Separator />
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-0.5">
<span className="text-sm text-muted-foreground">
Currently Using
</span>
<span className="text-lg font-semibold tabular-nums">3.4 kW</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-sm text-muted-foreground">Solar Gen</span>
<span className="text-lg font-semibold tabular-nums">+1.2 kW</span>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,64 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
const qrCells = [
"111111100101101111111",
"100000101001001000001",
"101110101111101011101",
"101110100100001011101",
"101110101010101011101",
"100000100111001000001",
"111111101010101111111",
"000000001101000000000",
"101011111001111010110",
"010100001110010101001",
"111010111011101111010",
"001101000101000010101",
"110111101111010111011",
"000000001001010001010",
"111111101101111101001",
"100000100010001001111",
"101110101011101110100",
"101110100110100010011",
"101110101000111101110",
"100000101101000011001",
"111111101011101101111",
]
export function QrConnect() {
return (
<Card>
<CardContent className="flex justify-center pt-6">
<div className="rounded-xl border bg-white p-4">
<svg
viewBox="0 0 21 21"
className="size-40 text-black"
role="img"
aria-label="Connect device QR code"
shapeRendering="crispEdges"
>
<rect width="21" height="21" fill="white" />
{qrCells.map((row, y) =>
[...row].map((cell, x) =>
cell === "1" ? (
<rect key={`${x}-${y}`} x={x} y={y} width="1" height="1" />
) : null
)
)}
</svg>
</div>
</CardContent>
<CardHeader className="text-center">
<CardTitle>Scan to connect your mobile device</CardTitle>
<CardDescription className="text-balance">
Open the Ledger mobile app and scan this code to link your device.
</CardDescription>
</CardHeader>
</Card>
)
}

View File

@@ -0,0 +1,81 @@
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import {
Item,
ItemContent,
ItemDescription,
ItemFooter,
ItemGroup,
} from "@/styles/base-rhea/ui/item"
import { Progress } from "@/styles/base-rhea/ui/progress"
export function SavingsTargets() {
return (
<Card>
<CardHeader>
<CardTitle>Savings Targets</CardTitle>
<CardDescription>
Active milestones for 2024 across your portfolio. Monitor how close
you are to each savings goal.
</CardDescription>
</CardHeader>
<CardContent>
<ItemGroup className="gap-3">
<Item
role="listitem"
variant="muted"
className="flex-col items-stretch"
>
<ItemContent className="gap-3">
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
Retirement
</ItemDescription>
<span className="text-3xl font-semibold tabular-nums">
$420,000
</span>
<Progress value={65} aria-label="Retirement savings progress" />
</ItemContent>
<ItemFooter>
<span className="text-sm text-muted-foreground">
65% achieved
</span>
<span className="text-sm font-medium tabular-nums">$273,000</span>
</ItemFooter>
</Item>
<Item
role="listitem"
variant="muted"
className="flex-col items-stretch"
>
<ItemContent className="gap-3">
<ItemDescription className="cn-font-heading text-xs font-medium tracking-wider text-muted-foreground uppercase">
Real Estate
</ItemDescription>
<span className="text-3xl font-semibold tabular-nums">
$85,000
</span>
<Progress value={32} aria-label="Real estate savings progress" />
</ItemContent>
<ItemFooter>
<span className="text-sm text-muted-foreground">
32% achieved
</span>
<span className="text-sm font-medium tabular-nums">$27,200</span>
</ItemFooter>
</Item>
</ItemGroup>
</CardContent>
<CardFooter>
<CardDescription className="text-center">
You have not met your targets for this year.
</CardDescription>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,218 @@
import * as React from "react"
import {
ActivityIcon,
Analytics01Icon,
AnalyticsUpIcon,
ArrowDataTransferHorizontalIcon,
BankIcon,
BookOpen02Icon,
Calendar03Icon,
ChartBarLineIcon,
CreditCardIcon,
File02Icon,
Globe02Icon,
HelpCircleIcon,
Message01Icon,
Notification03Icon,
PaintBoardIcon,
PieChartIcon,
ShieldIcon,
Target02Icon,
UserIcon,
Wallet01Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { Card } from "@/styles/base-rhea/ui/card"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
} from "@/styles/base-rhea/ui/sidebar"
function SidebarSection({
label,
children,
className,
}: {
label: string
children: React.ReactNode
className?: string
}) {
return (
<Card className={cn("w-full overflow-hidden rounded-3xl py-0", className)}>
<SidebarProvider className="min-h-0">
<Sidebar collapsible="none" className="w-full bg-transparent">
<SidebarContent className="gap-0 overflow-hidden">
<SidebarGroup>
<SidebarGroupLabel>{label}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu className="gap-1">{children}</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</SidebarProvider>
</Card>
)
}
export function SidebarNav() {
return (
<div className="grid w-full grid-cols-2 gap-4 xl:gap-6">
<SidebarSection
label="Overview"
className="xl:col-start-1 xl:row-start-2"
>
<SidebarMenuItem>
<SidebarMenuButton isActive>
<HugeiconsIcon icon={Analytics01Icon} strokeWidth={2} />
Analytics
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon
icon={ArrowDataTransferHorizontalIcon}
strokeWidth={2}
/>
Transactions
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={AnalyticsUpIcon} strokeWidth={2} />
Investments
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={BankIcon} strokeWidth={2} />
Accounts
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={PieChartIcon} strokeWidth={2} />
Spending
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarSection>
<SidebarSection
label="Planning"
className="xl:col-start-1 xl:row-start-1"
>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={File02Icon} strokeWidth={2} />
Documents
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={Wallet01Icon} strokeWidth={2} />
Budget
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={ChartBarLineIcon} strokeWidth={2} />
Reports
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={Target02Icon} strokeWidth={2} />
Goals
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={Calendar03Icon} strokeWidth={2} />
Calendar
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarSection>
<SidebarSection
label="Support"
className="flex xl:col-start-2 xl:row-start-1"
>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={HelpCircleIcon} strokeWidth={2} />
Help Center
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={BookOpen02Icon} strokeWidth={2} />
Docs
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={Message01Icon} strokeWidth={2} />
Contact Us
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={ActivityIcon} strokeWidth={2} />
Status
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={Globe02Icon} strokeWidth={2} />
Community
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarSection>
<SidebarSection
label="Account"
className="flex xl:col-start-2 xl:row-start-2"
>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={UserIcon} strokeWidth={2} />
Profile
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton isActive>
<HugeiconsIcon icon={CreditCardIcon} strokeWidth={2} />
Billing
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={Notification03Icon} strokeWidth={2} />
Notifications
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={ShieldIcon} strokeWidth={2} />
Security
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<HugeiconsIcon icon={PaintBoardIcon} strokeWidth={2} />
Appearance
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarSection>
</div>
)
}

View File

@@ -0,0 +1,35 @@
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
export function AccountAccess() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-36 rounded-md" />
<Skeleton className="h-4 w-64 rounded-md" />
</CardHeader>
<CardContent className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-24 rounded-md" />
<Skeleton className="h-9 w-full rounded-lg" />
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Skeleton className="h-3 w-32 rounded-md" />
<Skeleton className="h-3 w-12 rounded-md" />
</div>
<Skeleton className="h-9 w-full rounded-lg" />
</div>
</CardContent>
<CardFooter className="flex-col gap-4">
<Skeleton className="h-9 w-full rounded-lg" />
<Skeleton className="h-14 w-full rounded-xl" />
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,17 @@
import { Card, CardAction, CardHeader } from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
export function AnalyticsCard() {
return (
<Card className="mx-auto w-full max-w-sm data-[size=sm]:pb-0" size="sm">
<CardHeader className="gap-2">
<Skeleton className="h-5 w-24 rounded-md" />
<Skeleton className="h-4 w-40 rounded-md" />
<CardAction>
<Skeleton className="h-7 w-28 rounded-lg" />
</CardAction>
</CardHeader>
<Skeleton className="mx-6 mb-6 aspect-[1/0.35] w-auto rounded-lg" />
</Card>
)
}

View File

@@ -0,0 +1,41 @@
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
export function ClaimableBalance() {
return (
<Card>
<CardHeader className="gap-3">
<Skeleton className="h-4 w-36 rounded-md" />
<Skeleton className="h-12 w-56 rounded-lg" />
<Skeleton className="h-6 w-32 rounded-full" />
</CardHeader>
<CardContent className="flex flex-1 flex-col justify-end">
<div className="flex flex-col gap-3 rounded-xl bg-muted p-4">
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-4 w-20 rounded-md bg-muted-foreground/15" />
</div>
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-32 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-4 w-16 rounded-md bg-muted-foreground/15" />
</div>
<Skeleton className="h-px w-full rounded-none bg-muted-foreground/15" />
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-36 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-4 w-24 rounded-md bg-muted-foreground/15" />
</div>
</div>
</CardContent>
<CardFooter className="flex-col gap-2">
<Skeleton className="h-3 w-full rounded-md" />
<Skeleton className="h-3 w-11/12 rounded-md" />
<Skeleton className="h-3 w-3/4 rounded-md" />
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,53 @@
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
const bars = [60, 80, 65, 95, 50, 100]
export function ContributionHistory() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-44 rounded-md" />
<Skeleton className="h-4 w-52 rounded-md" />
</CardHeader>
<CardContent>
<div className="flex h-[200px] w-full items-end gap-3">
{bars.map((height, i) => (
<div
key={i}
className="flex h-full flex-1 flex-col justify-end gap-2"
>
<Skeleton
className="w-full rounded-t-md rounded-b-none"
style={{ height: `${height}%` }}
/>
<Skeleton className="mx-auto h-3 w-6 rounded-md" />
</div>
))}
</div>
</CardContent>
<CardContent>
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
<div className="flex flex-col gap-2 rounded-xl bg-muted p-4">
<Skeleton className="h-3 w-20 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-5 w-28 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
</div>
<div className="hidden flex-col gap-2 rounded-xl bg-muted p-4 xl:flex">
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-5 w-32 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-3 w-28 rounded-md bg-muted-foreground/15" />
</div>
</div>
</CardContent>
<CardFooter>
<Skeleton className="h-9 w-full rounded-lg" />
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,49 @@
import {
Card,
CardAction,
CardContent,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
const rows = [0, 1, 2, 3]
const miniBars = [40, 60, 80, 50]
export function DividendIncome() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-48 rounded-md" />
<Skeleton className="h-4 w-64 rounded-md" />
<CardAction>
<Skeleton className="size-8 rounded-md" />
</CardAction>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2">
{rows.map((row) => (
<div
key={row}
className="flex items-center gap-3 rounded-xl bg-muted p-3"
>
<div className="flex flex-1 flex-col gap-2">
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-3 w-20 rounded-md bg-muted-foreground/15" />
</div>
<div className="hidden h-8 w-24 items-end gap-1 md:flex">
{miniBars.map((h, i) => (
<Skeleton
key={i}
className="flex-1 rounded-t-sm rounded-b-none bg-muted-foreground/15"
style={{ height: `${h}%` }}
/>
))}
</div>
<Skeleton className="hidden h-4 w-16 rounded-md bg-muted-foreground/15 md:block" />
</div>
))}
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,20 @@
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
export function EmptyDistributeTrack() {
return (
<Card>
<CardContent>
<div className="flex flex-col items-center gap-4 p-4">
<Skeleton className="size-12 rounded-xl" />
<div className="flex flex-col items-center gap-2">
<Skeleton className="h-5 w-40 rounded-md" />
<Skeleton className="h-3 w-64 rounded-md" />
<Skeleton className="h-3 w-48 rounded-md" />
</div>
<Skeleton className="h-9 w-32 rounded-lg" />
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,56 @@
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 { TransferFunds } from "./transfer-funds"
import { UIElements } from "./ui-elements"
export function CardsSkeletonDemo() {
return (
<div
data-slot="demo"
className="theme-neutral relative flex w-full max-w-none flex-col gap-(--gap) bg-muted p-12 pb-0! [--gap:--spacing(8)] 3xl:[--gap:--spacing(8)] min-[1900px]:[--gap:--spacing(10)]! lg:p-8 lg:[--gap:--spacing(6)] xl:p-12 dark:bg-muted/30"
>
<div className="relative z-10 mx-auto grid gap-(--gap) **:data-[slot=card]:w-full 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] xl:grid-cols-4 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) 3xl:flex!">
<NewMilestone />
<PayoutThreshold />
<AccountAccess />
</div>
<div className="hidden flex-col gap-(--gap) md:flex">
<QrConnect />
<TransferFunds />
<Payments />
</div>
<div className="hidden flex-col gap-(--gap) xl:flex">
<EmptyDistributeTrack />
<AnalyticsCard />
<NotificationSettings />
<PowerUsage />
</div>
</div>
<div className="absolute inset-x-0 top-0 z-1 h-80 bg-linear-to-b from-background via-muted to-transparent dark:via-muted/30" />
<div className="absolute inset-x-0 bottom-0 z-20 h-80 bg-linear-to-t from-background via-muted to-transparent dark:via-muted/30" />
</div>
)
}

View File

@@ -0,0 +1,38 @@
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
export function NewMilestone() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-44 rounded-md" />
<Skeleton className="h-4 w-72 rounded-md" />
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-20 rounded-md" />
<Skeleton className="h-9 w-full rounded-lg" />
</div>
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-24 rounded-md" />
<Skeleton className="h-9 w-full rounded-lg" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-20 rounded-md" />
<Skeleton className="h-9 w-full rounded-lg" />
</div>
</div>
</CardContent>
<CardFooter className="flex-col gap-2">
<Skeleton className="h-9 w-full rounded-lg" />
<Skeleton className="h-9 w-full rounded-lg" />
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,34 @@
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
const rows = [0, 1, 2, 3]
export function NotificationSettings() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-32 rounded-md" />
<Skeleton className="h-4 w-64 rounded-md" />
</CardHeader>
<CardContent className="flex flex-col gap-4">
{rows.map((row) => (
<div key={row} className="flex items-start gap-3">
<Skeleton className="size-4 rounded-sm" />
<div className="flex flex-1 flex-col gap-2">
<Skeleton className="h-4 w-40 rounded-md" />
<Skeleton className="h-3 w-56 rounded-md" />
</div>
</div>
))}
</CardContent>
<CardFooter>
<Skeleton className="h-9 w-full rounded-lg" />
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,37 @@
import { Card, CardContent, CardHeader } from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
const rows = [0, 1, 2]
export function Payments() {
return (
<Card>
<CardHeader className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-12 rounded-md" />
<Skeleton className="size-1.5 rounded-full" />
<Skeleton className="size-7 rounded-md" />
<Skeleton className="size-1.5 rounded-full" />
<Skeleton className="h-4 w-20 rounded-md" />
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2">
{rows.map((row) => (
<div
key={row}
className="flex items-center gap-3 rounded-xl bg-muted p-3"
>
<Skeleton className="size-9 rounded-lg bg-muted-foreground/15" />
<div className="flex flex-1 flex-col gap-2">
<Skeleton className="h-4 w-40 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-3 w-56 rounded-md bg-muted-foreground/15" />
</div>
<Skeleton className="size-4 rounded-md bg-muted-foreground/15" />
</div>
))}
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,43 @@
import {
Card,
CardAction,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
export function PayoutThreshold() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-44 rounded-md" />
<Skeleton className="h-4 w-72 rounded-md" />
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-32 rounded-md" />
<Skeleton className="h-9 w-full rounded-lg" />
</div>
<div className="flex flex-col gap-3">
<div className="flex items-baseline justify-between">
<Skeleton className="h-3 w-40 rounded-md" />
<Skeleton className="h-7 w-24 rounded-md" />
</div>
<Skeleton className="h-2 w-full rounded-full" />
<div className="flex items-center justify-between">
<Skeleton className="h-3 w-16 rounded-md" />
<Skeleton className="h-3 w-20 rounded-md" />
</div>
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-16 rounded-md" />
<Skeleton className="h-[100px] w-full rounded-lg" />
</div>
</CardContent>
<CardFooter>
<Skeleton className="h-9 w-full rounded-lg" />
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,54 @@
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
const bars = [30, 70, 80, 60, 90, 75, 100, 85]
export function PowerUsage() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-32 rounded-md" />
<Skeleton className="h-4 w-24 rounded-md" />
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex h-[140px] w-full items-end gap-2">
{bars.map((height, i) => (
<div
key={i}
className="flex h-full flex-1 flex-col justify-end gap-1.5"
>
<Skeleton
className="w-full rounded-t rounded-b-none"
style={{ height: `${height}%` }}
/>
<Skeleton className="mx-auto h-3 w-5 rounded-md" />
</div>
))}
</div>
<Skeleton className="h-px w-full rounded-none" />
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<Skeleton className="h-3 w-28 rounded-md" />
<Skeleton className="h-5 w-20 rounded-md" />
</div>
<div className="flex flex-col gap-1.5">
<Skeleton className="h-3 w-20 rounded-md" />
<Skeleton className="h-5 w-24 rounded-md" />
</div>
</div>
</CardContent>
<CardFooter className="flex-col items-start gap-2">
<Skeleton className="h-3 w-24 rounded-md" />
<div className="flex w-full items-center gap-2">
<Skeleton className="h-2 flex-1 rounded-full" />
<Skeleton className="h-3 w-10 rounded-md" />
</div>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,17 @@
import { Card, CardContent, CardHeader } from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
export function QrConnect() {
return (
<Card>
<CardContent className="flex justify-center pt-6">
<Skeleton className="size-44 rounded-xl" />
</CardContent>
<CardHeader className="items-center gap-2 text-center">
<Skeleton className="h-5 w-56 rounded-md" />
<Skeleton className="h-4 w-64 rounded-md" />
<Skeleton className="h-4 w-48 rounded-md" />
</CardHeader>
</Card>
)
}

View File

@@ -0,0 +1,44 @@
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
const rows = [0, 1]
export function SavingsTargets() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-36 rounded-md" />
<div className="flex flex-col gap-1.5">
<Skeleton className="h-4 w-full max-w-64 rounded-md" />
<Skeleton className="h-4 w-48 rounded-md" />
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-3">
{rows.map((row) => (
<div
key={row}
className="flex flex-col gap-3 rounded-xl bg-muted p-4"
>
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-8 w-36 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-2 w-full rounded-full bg-muted-foreground/15" />
<div className="flex items-center justify-between">
<Skeleton className="h-3 w-24 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-3 w-20 rounded-md bg-muted-foreground/15" />
</div>
</div>
))}
</div>
</CardContent>
<CardFooter className="justify-center">
<Skeleton className="h-3 w-56 rounded-md" />
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,39 @@
import { Card } from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
const groupA = [0, 1, 2, 3, 4]
const groupB = [0, 1, 2, 3, 4]
function NavSkeleton({ groups }: { groups: number[][] }) {
return (
<div className="flex flex-col gap-1 p-2">
{groups.map((items, gi) => (
<div key={gi} className="flex flex-col gap-1 px-2 py-1.5">
<Skeleton className="mb-1 h-3 w-20 rounded-md" />
{items.map((item) => (
<div key={item} className="flex items-center gap-2 px-2 py-2">
<Skeleton className="size-4 rounded-md" />
<Skeleton className="h-3 w-24 rounded-md" />
</div>
))}
{gi < groups.length - 1 && (
<Skeleton className="my-1 h-px w-full rounded-none" />
)}
</div>
))}
</div>
)
}
export function SidebarNav() {
return (
<div className="grid w-full items-start gap-4 xl:grid-cols-2 xl:gap-6">
<Card className="w-full overflow-hidden rounded-3xl py-0">
<NavSkeleton groups={[groupA, groupB]} />
</Card>
<Card className="hidden w-full overflow-hidden rounded-3xl py-0 xl:flex">
<NavSkeleton groups={[groupA, groupB]} />
</Card>
</div>
)
}

View File

@@ -0,0 +1,55 @@
import {
Card,
CardAction,
CardContent,
CardFooter,
CardHeader,
} from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
export function TransferFunds() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-36 rounded-md" />
<Skeleton className="h-4 w-64 rounded-md" />
<CardAction>
<Skeleton className="size-8 rounded-md" />
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-32 rounded-md" />
<Skeleton className="h-9 w-full rounded-lg" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-24 rounded-md" />
<Skeleton className="h-9 w-full rounded-lg" />
</div>
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-20 rounded-md" />
<Skeleton className="h-9 w-full rounded-lg" />
</div>
<div className="flex flex-col gap-3 rounded-xl bg-muted p-4">
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-4 w-24 rounded-md bg-muted-foreground/15" />
</div>
<Skeleton className="h-px w-full rounded-none bg-muted-foreground/15" />
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-28 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-4 w-12 rounded-md bg-muted-foreground/15" />
</div>
<Skeleton className="h-px w-full rounded-none bg-muted-foreground/15" />
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-24 rounded-md bg-muted-foreground/15" />
<Skeleton className="h-4 w-20 rounded-md bg-muted-foreground/15" />
</div>
</div>
</CardContent>
<CardFooter>
<Skeleton className="h-9 w-full rounded-lg" />
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,45 @@
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
import { Skeleton } from "@/styles/base-rhea/ui/skeleton"
export function UIElements() {
return (
<Card className="w-full">
<CardContent className="flex flex-col gap-6">
<Skeleton className="h-8 w-full rounded-2xl" />
<div className="flex flex-wrap gap-2">
<Skeleton className="h-9 w-20 rounded-lg" />
<Skeleton className="h-9 w-24 rounded-lg" />
<Skeleton className="h-9 w-20 rounded-lg" />
</div>
<div className="flex flex-col gap-3">
<Skeleton className="h-9 w-full rounded-lg" />
<Skeleton className="h-20 w-full rounded-lg" />
</div>
<div className="flex items-center gap-2">
<div className="flex gap-2">
<Skeleton className="h-5 w-12 rounded-full" />
<Skeleton className="h-5 w-16 rounded-full" />
<Skeleton className="hidden h-5 w-14 rounded-full 4xl:block" />
</div>
<div className="ml-auto flex gap-3">
<Skeleton className="size-4 rounded-full" />
<Skeleton className="size-4 rounded-full" />
</div>
<div className="flex gap-3">
<Skeleton className="size-4 rounded-sm" />
<Skeleton className="hidden size-4 rounded-sm 4xl:block" />
</div>
<Skeleton className="ml-auto h-5 w-9 rounded-full 4xl:hidden" />
</div>
<div className="flex items-center gap-4">
<Skeleton className="h-9 w-24 rounded-lg" />
<div className="flex">
<Skeleton className="h-9 w-28 rounded-l-lg rounded-r-none" />
<Skeleton className="ml-px h-9 w-9 rounded-l-none rounded-r-lg" />
</div>
<Skeleton className="ml-auto hidden h-5 w-9 rounded-full 4xl:block" />
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,139 @@
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,
CardFooter,
CardHeader,
CardTitle,
} from "@/styles/base-rhea/ui/card"
import { Field, FieldGroup, FieldLabel } from "@/styles/base-rhea/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/styles/base-rhea/ui/input-group"
import { Item, ItemContent } from "@/styles/base-rhea/ui/item"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/styles/base-rhea/ui/select"
import { Separator } from "@/styles/base-rhea/ui/separator"
const FROM_ACCOUNTS = [
{ label: "Main Checking (··8402) — $12,450.00", value: "checking" },
{ label: "Business (··7731) — $8,920.00", value: "business" },
]
const TO_ACCOUNTS = [
{ label: "High Yield Savings (··1192) — $42,100.00", value: "savings" },
{ label: "Investment (··3349) — $18,200.00", value: "investment" },
]
export function TransferFunds() {
return (
<Card>
<CardHeader>
<CardTitle>Transfer Funds</CardTitle>
<CardDescription>
Move money between your connected accounts.
</CardDescription>
<CardAction>
<Button
variant="ghost"
size="icon-sm"
className="bg-muted"
aria-label="Dismiss transfer funds"
>
<HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
</Button>
</CardAction>
</CardHeader>
<CardContent>
<FieldGroup>
<Field>
<FieldLabel htmlFor="transfer-amount">
Amount to Transfer
</FieldLabel>
<InputGroup>
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupInput id="transfer-amount" defaultValue="1,200.00" />
</InputGroup>
</Field>
<Field>
<FieldLabel htmlFor="from-account">From Account</FieldLabel>
<Select items={FROM_ACCOUNTS} defaultValue="checking">
<SelectTrigger id="from-account" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{FROM_ACCOUNTS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="to-account">To Account</FieldLabel>
<Select items={TO_ACCOUNTS} defaultValue="savings">
<SelectTrigger id="to-account" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{TO_ACCOUNTS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
<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">
Estimated arrival
</span>
<span className="text-sm font-medium">Today, Apr 14</span>
</div>
<Separator />
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
Transaction fee
</span>
<span className="text-sm font-medium tabular-nums">$0.00</span>
</div>
<Separator />
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Total amount</span>
<span className="text-sm font-semibold tabular-nums">
$1,200.00
</span>
</div>
</ItemContent>
</Item>
</FieldGroup>
</CardContent>
<CardFooter>
<Button className="w-full">Confirm Transfer</Button>
</CardFooter>
</Card>
)
}

View File

@@ -0,0 +1,176 @@
"use client"
import {
ArrowRight02Icon,
ArrowUp01Icon,
Search01Icon,
} from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/styles/base-rhea/ui/alert-dialog"
import { Badge } from "@/styles/base-rhea/ui/badge"
import { Button } from "@/styles/base-rhea/ui/button"
import { ButtonGroup } from "@/styles/base-rhea/ui/button-group"
import { Card, CardContent } from "@/styles/base-rhea/ui/card"
import { Checkbox } from "@/styles/base-rhea/ui/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/styles/base-rhea/ui/dropdown-menu"
import { Field, FieldGroup } from "@/styles/base-rhea/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/styles/base-rhea/ui/input-group"
import { RadioGroup, RadioGroupItem } from "@/styles/base-rhea/ui/radio-group"
import { Switch } from "@/styles/base-rhea/ui/switch"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/styles/base-rhea/ui/tabs"
import { Textarea } from "@/styles/base-rhea/ui/textarea"
export function UIElements() {
return (
<Card className="w-full">
<CardContent className="flex flex-col gap-6">
<div className="flex gap-2">
<Button>
Button{" "}
<HugeiconsIcon
icon={ArrowRight02Icon}
strokeWidth={2}
data-icon="inline-end"
/>
</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
</div>
<FieldGroup>
<Field>
<InputGroup>
<InputGroupInput placeholder="Name" />
<InputGroupAddon align="inline-end">
<InputGroupText>
<HugeiconsIcon icon={Search01Icon} strokeWidth={2} />
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</Field>
<Field className="flex-1">
<Textarea placeholder="Message" className="resize-none" />
</Field>
</FieldGroup>
<div className="flex items-center gap-2">
<div className="flex gap-2">
<Badge>Badge</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline" className="hidden 4xl:flex">
Outline
</Badge>
</div>
<RadioGroup
defaultValue="apple"
className="ml-auto flex w-fit gap-3"
aria-label="Fruit preference"
>
<RadioGroupItem value="apple" aria-label="Apple" />
<RadioGroupItem value="banana" aria-label="Banana" />
</RadioGroup>
<div className="flex gap-3">
<Checkbox defaultChecked aria-label="Enable email alerts" />
<Checkbox
className="hidden 4xl:flex"
aria-label="Enable push alerts"
/>
</div>
<Switch
defaultChecked
className="flex 4xl:hidden"
aria-label="Enable compact notifications"
/>
</div>
<div className="flex items-center gap-4">
<AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" />}>
<span className="hidden md:flex style-sera:md:hidden">
Alert Dialog
</span>
<span className="flex md:hidden style-sera:md:flex">Dialog</span>
</AlertDialogTrigger>
<AlertDialogContent size="sm" className="theme-blue">
<AlertDialogHeader>
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
<AlertDialogDescription>
Do you want to allow the USB accessory to connect to this
device and your data?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Don&apos;t allow</AlertDialogCancel>
<AlertDialogAction>Allow</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ButtonGroup className="ml-auto">
<Button variant="outline">
<span className="style-sera:hidden">Button Group</span>
<span className="hidden style-sera:block">Group</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
size="icon"
aria-label="Open quick actions"
/>
}
>
<HugeiconsIcon icon={ArrowUp01Icon} strokeWidth={2} />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="top" className="w-40">
<DropdownMenuGroup>
<DropdownMenuLabel>Quick Actions</DropdownMenuLabel>
<DropdownMenuItem>Mute Conversation</DropdownMenuItem>
<DropdownMenuItem>Mark as Read</DropdownMenuItem>
<DropdownMenuItem>Block User</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
Delete Conversation
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
<Switch
defaultChecked
className="hidden 4xl:flex"
aria-label="Enable advanced setting"
/>
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,135 +0,0 @@
"use client"
import * as React from "react"
import { IconMinus, IconPlus } from "@tabler/icons-react"
import { Button } from "@/styles/radix-nova/ui/button"
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/styles/radix-nova/ui/field"
import { Input } from "@/styles/radix-nova/ui/input"
import { RadioGroup, RadioGroupItem } from "@/styles/radix-nova/ui/radio-group"
import { Switch } from "@/styles/radix-nova/ui/switch"
export function AppearanceSettings() {
const [gpuCount, setGpuCount] = React.useState(8)
const handleGpuAdjustment = React.useCallback((adjustment: number) => {
setGpuCount((prevCount) =>
Math.max(1, Math.min(99, prevCount + adjustment))
)
}, [])
const handleGpuInputChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value, 10)
if (!isNaN(value) && value >= 1 && value <= 99) {
setGpuCount(value)
}
},
[]
)
return (
<FieldSet>
<FieldGroup>
<FieldSet>
<FieldLegend>Compute Environment</FieldLegend>
<FieldDescription>
Select the compute environment for your cluster.
</FieldDescription>
<RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="kubernetes-r2h">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>
Run GPU workloads on a K8s configured cluster. This is the
default.
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="kubernetes"
id="kubernetes-r2h"
aria-label="Kubernetes"
/>
</Field>
</FieldLabel>
<FieldLabel htmlFor="vm-z4k">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Virtual Machine</FieldTitle>
<FieldDescription>
Access a VM configured cluster to run workloads. (Coming
soon)
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="vm"
id="vm-z4k"
aria-label="Virtual Machine"
/>
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="number-of-gpus-f6l">Number of GPUs</FieldLabel>
<FieldDescription>You can add more later.</FieldDescription>
</FieldContent>
<ButtonGroup>
<Input
id="number-of-gpus-f6l"
value={gpuCount}
onChange={handleGpuInputChange}
size={3}
className="h-7 w-14! font-mono"
maxLength={3}
/>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label="Decrement"
onClick={() => handleGpuAdjustment(-1)}
disabled={gpuCount <= 1}
>
<IconMinus />
</Button>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label="Increment"
onClick={() => handleGpuAdjustment(1)}
disabled={gpuCount >= 99}
>
<IconPlus />
</Button>
</ButtonGroup>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
<FieldDescription>
Allow the wallpaper to be tinted.
</FieldDescription>
</FieldContent>
<Switch id="tinting" defaultChecked />
</Field>
</FieldGroup>
</FieldSet>
)
}

View File

@@ -1,120 +0,0 @@
"use client"
import * as React from "react"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import { Button } from "@/styles/radix-nova/ui/button"
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/styles/radix-nova/ui/dropdown-menu"
export function ButtonGroupDemo() {
const [label, setLabel] = React.useState("personal")
return (
<ButtonGroup>
<ButtonGroup className="hidden sm:flex">
<Button variant="outline" size="icon-sm" aria-label="Go Back">
<ArrowLeftIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
Archive
</Button>
<Button variant="outline" size="sm">
Report
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
Snooze
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon-sm" aria-label="More Options">
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />
Mark as Read
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
Archive
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<ClockIcon />
Snooze
</DropdownMenuItem>
<DropdownMenuItem>
<CalendarPlusIcon />
Add to Calendar
</DropdownMenuItem>
<DropdownMenuItem>
<ListFilterIcon />
Add to List
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<TagIcon />
Label As...
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={label}
onValueChange={setLabel}
>
<DropdownMenuRadioItem value="personal">
Personal
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="work">
Work
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="other">
Other
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
Trash
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -1,58 +0,0 @@
"use client"
import * as React from "react"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { Button } from "@/styles/radix-nova/ui/button"
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/styles/radix-nova/ui/input-group"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/styles/radix-nova/ui/tooltip"
export function ButtonGroupInputGroup() {
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
return (
<ButtonGroup className="[--radius:9999rem]">
<ButtonGroup>
<Button variant="outline" size="icon" aria-label="Add">
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup className="flex-1">
<InputGroup>
<InputGroupInput
placeholder={
voiceEnabled ? "Record and send audio..." : "Send a message..."
}
disabled={voiceEnabled}
/>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
onClick={() => setVoiceEnabled(!voiceEnabled)}
data-active={voiceEnabled}
className="data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
aria-pressed={voiceEnabled}
size="icon-xs"
aria-label="Voice Mode"
>
<AudioLinesIcon />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>Voice Mode</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -1,32 +0,0 @@
"use client"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
import { Button } from "@/styles/radix-nova/ui/button"
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
export function ButtonGroupNested() {
return (
<ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
1
</Button>
<Button variant="outline" size="sm">
2
</Button>
<Button variant="outline" size="sm">
3
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon-sm" aria-label="Previous">
<ArrowLeftIcon />
</Button>
<Button variant="outline" size="icon-sm" aria-label="Next">
<ArrowRightIcon />
</Button>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -1,45 +0,0 @@
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { Button } from "@/styles/radix-nova/ui/button"
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/styles/radix-nova/ui/popover"
import { Separator } from "@/styles/radix-nova/ui/separator"
import { Textarea } from "@/styles/radix-nova/ui/textarea"
export function ButtonGroupPopover() {
return (
<ButtonGroup>
<Button variant="outline" size="sm">
<BotIcon /> Copilot
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="icon-sm" aria-label="Open Popover">
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="gap-0 rounded-xl p-0 text-sm">
<div className="px-4 py-3">
<div className="text-sm font-medium">Agent Tasks</div>
</div>
<Separator />
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
<Textarea
placeholder="Describe your task in natural language."
className="mb-4 resize-none"
/>
<p className="font-medium">Start a new task with Copilot</p>
<p className="text-muted-foreground">
Describe your task in natural language. Copilot will work in the
background and open a pull request for your review.
</p>
</div>
</PopoverContent>
</Popover>
</ButtonGroup>
)
}

View File

@@ -1,58 +0,0 @@
import { PlusIcon } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/styles/radix-nova/ui/avatar"
import { Button } from "@/styles/radix-nova/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/styles/radix-nova/ui/empty"
export function EmptyAvatarGroup() {
return (
<Empty className="flex-none border py-10">
<EmptyHeader>
<EmptyMedia>
<AvatarGroup className="grayscale">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</AvatarGroup>
</EmptyMedia>
<EmptyTitle>No Team Members</EmptyTitle>
<EmptyDescription>
Invite your team to collaborate on this project.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">
<PlusIcon />
Invite Members
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -1,43 +0,0 @@
import { SearchIcon } from "lucide-react"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/registry/new-york-v4/ui/empty"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/registry/new-york-v4/ui/input-group"
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
export function EmptyInputGroup() {
return (
<Empty>
<EmptyHeader>
<EmptyTitle>404 - Not Found</EmptyTitle>
<EmptyDescription>
The page you&apos;re looking for doesn&apos;t exist. Try searching for
what you need below.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<InputGroup className="w-3/4">
<InputGroupInput placeholder="Try searching for pages..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>/</Kbd>
</InputGroupAddon>
</InputGroup>
<EmptyDescription>
Need help? <a href="#">Contact support</a>
</EmptyDescription>
</EmptyContent>
</Empty>
)
}

View File

@@ -1,15 +0,0 @@
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
export function FieldCheckbox() {
return (
<FieldLabel htmlFor="checkbox-demo">
<Field orientation="horizontal">
<Checkbox id="checkbox-demo" defaultChecked />
<FieldLabel htmlFor="checkbox-demo" className="line-clamp-1">
I agree to the terms and conditions
</FieldLabel>
</Field>
</FieldLabel>
)
}

View File

@@ -1,62 +0,0 @@
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSet,
FieldTitle,
} from "@/registry/new-york-v4/ui/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
export function FieldChoiceCard() {
return (
<div className="w-full max-w-md">
<FieldGroup>
<FieldSet>
<FieldLabel htmlFor="compute-environment-p8w">
Compute Environment
</FieldLabel>
<FieldDescription>
Select the compute environment for your cluster.
</FieldDescription>
<RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="kubernetes-r2h">
<Field orientation="horizontal">
<RadioGroupItem
value="kubernetes"
id="kubernetes-r2h"
aria-label="Kubernetes"
/>
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>
Run GPU workloads on a K8s configured cluster.
</FieldDescription>
</FieldContent>
</Field>
</FieldLabel>
<FieldLabel htmlFor="vm-z4k">
<Field orientation="horizontal">
<RadioGroupItem
value="vm"
id="vm-z4k"
aria-label="Virtual Machine"
/>
<FieldContent>
<FieldTitle>Virtual Machine</FieldTitle>
<FieldDescription>
Access a VM configured cluster to run workloads.
</FieldDescription>
</FieldContent>
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
</FieldGroup>
</div>
)
}

View File

@@ -1,158 +0,0 @@
import { Button } from "@/styles/radix-nova/ui/button"
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/styles/radix-nova/ui/field"
import { Input } from "@/styles/radix-nova/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/styles/radix-nova/ui/select"
import { Textarea } from "@/styles/radix-nova/ui/textarea"
export function FieldDemo() {
return (
<div className="w-full max-w-md rounded-xl border p-6">
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>Payment Method</FieldLegend>
<FieldDescription>
All transactions are secure and encrypted
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="checkout-7j9-card-name-43j">
Name on Card
</FieldLabel>
<Input
id="checkout-7j9-card-name-43j"
placeholder="John Doe"
required
/>
</Field>
<div className="grid grid-cols-3 gap-4">
<Field className="col-span-2">
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">
Card Number
</FieldLabel>
<Input
id="checkout-7j9-card-number-uw1"
placeholder="1234 5678 9012 3456"
required
/>
<FieldDescription>
Enter your 16-digit number.
</FieldDescription>
</Field>
<Field className="col-span-1">
<FieldLabel htmlFor="checkout-7j9-cvv">CVV</FieldLabel>
<Input id="checkout-7j9-cvv" placeholder="123" required />
</Field>
</div>
<div className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="checkout-7j9-exp-month-ts6">
Month
</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-7j9-exp-month-ts6">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="01">01</SelectItem>
<SelectItem value="02">02</SelectItem>
<SelectItem value="03">03</SelectItem>
<SelectItem value="04">04</SelectItem>
<SelectItem value="05">05</SelectItem>
<SelectItem value="06">06</SelectItem>
<SelectItem value="07">07</SelectItem>
<SelectItem value="08">08</SelectItem>
<SelectItem value="09">09</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">
Year
</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-7j9-exp-year-f59">
<SelectValue placeholder="YYYY" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
<SelectItem value="2027">2027</SelectItem>
<SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
</div>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Billing Address</FieldLegend>
<FieldDescription>
The billing address associated with your payment method
</FieldDescription>
<FieldGroup>
<Field orientation="horizontal">
<Checkbox
id="checkout-7j9-same-as-shipping-wgm"
defaultChecked
/>
<FieldLabel
htmlFor="checkout-7j9-same-as-shipping-wgm"
className="font-normal"
>
Same as shipping address
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="checkout-7j9-optional-comments">
Comments
</FieldLabel>
<Textarea
id="checkout-7j9-optional-comments"
placeholder="Add any additional comments"
/>
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button type="submit">Submit</Button>
<Button variant="outline" type="button">
Cancel
</Button>
</Field>
</FieldGroup>
</form>
</div>
)
}

View File

@@ -1,72 +0,0 @@
import { Card, CardContent } from "@/styles/radix-nova/ui/card"
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/styles/radix-nova/ui/field"
const options = [
{
label: "Social Media",
value: "social-media",
},
{
label: "Search Engine",
value: "search-engine",
},
{
label: "Referral",
value: "referral",
},
{
label: "Other",
value: "other",
},
]
export function FieldHear() {
return (
<Card className="py-4 shadow-none">
<CardContent className="px-4">
<form>
<FieldGroup>
<FieldSet className="gap-4">
<FieldLegend>How did you hear about us?</FieldLegend>
<FieldDescription className="line-clamp-1">
Select the option that best describes how you heard about us.
</FieldDescription>
<FieldGroup className="flex flex-row flex-wrap gap-2 [--radius:9999rem]">
{options.map((option) => (
<FieldLabel
htmlFor={option.value}
key={option.value}
className="w-fit!"
>
<Field
orientation="horizontal"
className="gap-1.5 overflow-hidden px-3! py-1.5! transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:px-2!"
>
<Checkbox
value={option.value}
id={option.value}
defaultChecked={option.value === "social-media"}
className="-ml-6 -translate-x-1 rounded-full transition-all duration-100 ease-linear data-[state=checked]:ml-0 data-[state=checked]:translate-x-0"
/>
<FieldTitle>{option.label}</FieldTitle>
</Field>
</FieldLabel>
))}
</FieldGroup>
</FieldSet>
</FieldGroup>
</form>
</CardContent>
</Card>
)
}

View File

@@ -1,35 +0,0 @@
"use client"
import { useState } from "react"
import {
Field,
FieldDescription,
FieldTitle,
} from "@/styles/radix-nova/ui/field"
import { Slider } from "@/styles/radix-nova/ui/slider"
export function FieldSlider() {
const [value, setValue] = useState([200, 800])
return (
<div className="w-full max-w-md">
<Field>
<FieldTitle>Price Range</FieldTitle>
<FieldDescription>
Set your budget range ($
<span className="font-medium tabular-nums">{value[0]}</span> -{" "}
<span className="font-medium tabular-nums">{value[1]}</span>).
</FieldDescription>
<Slider
value={value}
onValueChange={setValue}
max={1000}
min={0}
step={10}
className="mt-2 w-full"
aria-label="Price Range"
/>
</Field>
</div>
)
}

View File

@@ -1,52 +0,0 @@
import { FieldSeparator } from "@/styles/radix-nova/ui/field"
import { AppearanceSettings } from "./appearance-settings"
import { ButtonGroupDemo } from "./button-group-demo"
import { ButtonGroupInputGroup } from "./button-group-input-group"
import { ButtonGroupNested } from "./button-group-nested"
import { ButtonGroupPopover } from "./button-group-popover"
import { EmptyAvatarGroup } from "./empty-avatar-group"
import { FieldCheckbox } from "./field-checkbox"
import { FieldDemo } from "./field-demo"
import { FieldHear } from "./field-hear"
import { FieldSlider } from "./field-slider"
import { InputGroupButtonExample } from "./input-group-button"
import { InputGroupDemo } from "./input-group-demo"
import { ItemDemo } from "./item-demo"
import { NotionPromptForm } from "./notion-prompt-form"
import { SpinnerBadge } from "./spinner-badge"
import { SpinnerEmpty } from "./spinner-empty"
export function RootComponents() {
return (
<div className="mx-auto grid gap-8 py-1 theme-container md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<FieldDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<EmptyAvatarGroup />
<SpinnerBadge />
<ButtonGroupInputGroup />
<FieldSlider />
<InputGroupDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<InputGroupButtonExample />
<ItemDemo />
<FieldSeparator className="my-4">Appearance Settings</FieldSeparator>
<AppearanceSettings />
</div>
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">
<NotionPromptForm />
<ButtonGroupDemo />
<FieldCheckbox />
<div className="flex justify-between gap-4">
<ButtonGroupNested />
<ButtonGroupPopover />
</div>
<FieldHear />
<SpinnerEmpty />
</div>
</div>
)
}

View File

@@ -1,68 +0,0 @@
"use client"
import * as React from "react"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/styles/radix-nova/ui/input-group"
import { Label } from "@/styles/radix-nova/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/styles/radix-nova/ui/popover"
export function InputGroupButtonExample() {
const [isFavorite, setIsFavorite] = React.useState(false)
return (
<div className="grid w-full max-w-sm gap-6">
<Label htmlFor="input-secure-19" className="sr-only">
Input Secure
</Label>
<InputGroup className="[--radius:9999px]">
<InputGroupInput id="input-secure-19" className="pl-0.5!" />
<Popover>
<PopoverTrigger asChild>
<InputGroupAddon>
<InputGroupButton
variant="secondary"
size="icon-xs"
aria-label="Info"
>
<IconInfoCircle />
</InputGroupButton>
</InputGroupAddon>
</PopoverTrigger>
<PopoverContent
align="start"
alignOffset={10}
className="flex flex-col gap-1 rounded-xl text-sm"
>
<p className="font-medium">Your connection is not secure.</p>
<p>You should not enter any sensitive information on this site.</p>
</PopoverContent>
</Popover>
<InputGroupAddon className="pl-1! text-muted-foreground">
https://
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton
onClick={() => setIsFavorite(!isFavorite)}
size="icon-xs"
aria-label="Favorite"
>
<IconStar
data-favorite={isFavorite}
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
/>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -1,98 +0,0 @@
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/styles/radix-nova/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/styles/radix-nova/ui/input-group"
import { Separator } from "@/styles/radix-nova/ui/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/styles/radix-nova/ui/tooltip"
export function InputGroupDemo() {
return (
<div className="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="example.com" className="pl-1!" />
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
className="rounded-full"
size="icon-xs"
aria-label="Info"
>
<IconInfoCircle />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>This is content in a tooltip.</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder="Ask, Search or Chat..." />
<InputGroupAddon align="block-end">
<InputGroupButton
variant="outline"
className="rounded-full"
size="icon-xs"
aria-label="Add"
>
<IconPlus />
</InputGroupButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<InputGroupButton variant="ghost">Auto</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText className="ml-auto">52% used</InputGroupText>
<Separator orientation="vertical" className="h-4!" />
<InputGroupButton
variant="default"
className="rounded-full"
size="icon-xs"
>
<ArrowUpIcon />
<span className="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="@shadcn" />
<InputGroupAddon align="inline-end">
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
<IconCheck className="size-3 text-background" />
</div>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -1,46 +0,0 @@
import {
IconBrandJavascript,
IconCopy,
IconCornerDownLeft,
IconRefresh,
} from "@tabler/icons-react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupTextarea,
} from "@/registry/new-york-v4/ui/input-group"
export function InputGroupTextareaExample() {
return (
<div className="grid w-full max-w-md gap-4">
<InputGroup>
<InputGroupTextarea
id="textarea-code-32"
placeholder="console.log('Hello, world!');"
className="min-h-[180px]"
/>
<InputGroupAddon align="block-end" className="border-t">
<InputGroupText>Line 1, Column 1</InputGroupText>
<InputGroupButton size="sm" className="ml-auto" variant="default">
Run <IconCornerDownLeft />
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon align="block-start" className="border-b">
<InputGroupText className="font-mono font-medium">
<IconBrandJavascript />
script.js
</InputGroupText>
<InputGroupButton className="ml-auto">
<IconRefresh />
</InputGroupButton>
<InputGroupButton variant="ghost">
<IconCopy />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -1,78 +0,0 @@
import { Plus } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/registry/new-york-v4/ui/item"
export function ItemAvatar() {
return (
<div className="flex w-full max-w-lg flex-col gap-6">
<Item variant="outline" className="hidden">
<ItemMedia>
<Avatar className="size-10">
<AvatarImage src="https://github.com/maxleiter.png" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>Max Leiter</ItemTitle>
<ItemDescription>Last seen 5 months ago</ItemDescription>
</ItemContent>
<ItemActions>
<Button
size="icon-sm"
variant="outline"
className="rounded-full"
aria-label="Invite"
>
<Plus />
</Button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemMedia>
<div className="flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background *:data-[slot=avatar]:grayscale">
<Avatar className="hidden sm:flex">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className="hidden sm:flex">
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
</ItemMedia>
<ItemContent>
<ItemTitle>No Team Members</ItemTitle>
<ItemDescription>Invite your team to collaborate.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">
Invite
</Button>
</ItemActions>
</Item>
</div>
)
}

View File

@@ -1,42 +0,0 @@
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { Button } from "@/styles/radix-nova/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/styles/radix-nova/ui/item"
export function ItemDemo() {
return (
<div className="flex w-full max-w-md flex-col gap-6">
<Item variant="outline">
<ItemContent>
<ItemTitle>Two-factor authentication</ItemTitle>
<ItemDescription className="text-pretty xl:hidden 2xl:block">
Verify via email or phone number.
</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm">Enable</Button>
</ItemActions>
</Item>
<Item variant="outline" size="sm" asChild>
<a href="#">
<ItemMedia>
<BadgeCheckIcon className="size-5" />
</ItemMedia>
<ItemContent>
<ItemTitle>Your profile has been verified.</ItemTitle>
</ItemContent>
<ItemActions>
<ChevronRightIcon className="size-4" />
</ItemActions>
</a>
</Item>
</div>
)
}

View File

@@ -1,453 +0,0 @@
"use client"
import { useMemo, useState } from "react"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/styles/radix-nova/ui/avatar"
import { Badge } from "@/styles/radix-nova/ui/badge"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/styles/radix-nova/ui/command"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/styles/radix-nova/ui/dropdown-menu"
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/styles/radix-nova/ui/input-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/styles/radix-nova/ui/popover"
import { Switch } from "@/styles/radix-nova/ui/switch"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/styles/radix-nova/ui/tooltip"
const SAMPLE_DATA = {
mentionable: [
{
type: "page",
title: "Meeting Notes",
image: "📝",
},
{
type: "page",
title: "Project Dashboard",
image: "📊",
},
{
type: "page",
title: "Ideas & Brainstorming",
image: "💡",
},
{
type: "page",
title: "Calendar & Events",
image: "📅",
},
{
type: "page",
title: "Documentation",
image: "📚",
},
{
type: "page",
title: "Goals & Objectives",
image: "🎯",
},
{
type: "page",
title: "Budget Planning",
image: "💰",
},
{
type: "page",
title: "Team Directory",
image: "👥",
},
{
type: "page",
title: "Technical Specs",
image: "🔧",
},
{
type: "page",
title: "Analytics Report",
image: "📈",
},
{
type: "user",
title: "shadcn",
image: "https://github.com/shadcn.png",
workspace: "Workspace",
},
{
type: "user",
title: "maxleiter",
image: "https://github.com/maxleiter.png",
workspace: "Workspace",
},
{
type: "user",
title: "evilrabbit",
image: "https://github.com/evilrabbit.png",
workspace: "Workspace",
},
],
models: [
{
name: "Auto",
},
{
name: "Agent Mode",
badge: "Beta",
},
{
name: "Plan Mode",
},
],
}
function MentionableIcon({
item,
}: {
item: (typeof SAMPLE_DATA.mentionable)[0]
}) {
return item.type === "page" ? (
<span className="flex size-4 items-center justify-center">
{item.image}
</span>
) : (
<Avatar className="size-4">
<AvatarImage src={item.image} />
<AvatarFallback>{item.title[0]}</AvatarFallback>
</Avatar>
)
}
export function NotionPromptForm() {
const [mentions, setMentions] = useState<string[]>([])
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
const [selectedModel, setSelectedModel] = useState<
(typeof SAMPLE_DATA.models)[0]
>(SAMPLE_DATA.models[0])
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
const grouped = useMemo(() => {
return SAMPLE_DATA.mentionable.reduce(
(acc, item) => {
const isAvailable = !mentions.includes(item.title)
if (isAvailable) {
if (!acc[item.type]) {
acc[item.type] = []
}
acc[item.type].push(item)
}
return acc
},
{} as Record<string, typeof SAMPLE_DATA.mentionable>
)
}, [mentions])
const hasMentions = mentions.length > 0
return (
<form>
<Field>
<FieldLabel htmlFor="notion-prompt" className="sr-only">
Prompt
</FieldLabel>
<InputGroup className="rounded-xl">
<InputGroupTextarea
id="notion-prompt"
placeholder="Ask, search, or make anything..."
/>
<InputGroupAddon align="block-start" className="pt-3">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
>
<Tooltip>
<TooltipTrigger
asChild
onFocusCapture={(e) => e.stopPropagation()}
>
<PopoverTrigger asChild>
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="transition-transform"
>
<IconAt /> {!hasMentions && "Add context"}
</InputGroupButton>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Mention a person, page, or date</TooltipContent>
</Tooltip>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput placeholder="Search pages..." />
<CommandList>
<CommandEmpty>No pages found</CommandEmpty>
{Object.entries(grouped).map(([type, items]) => (
<CommandGroup
key={type}
heading={type === "page" ? "Pages" : "Users"}
>
{items.map((item) => (
<CommandItem
key={item.title}
value={item.title}
onSelect={(currentValue) => {
setMentions((prev) => [...prev, currentValue])
setMentionPopoverOpen(false)
}}
className="rounded-lg"
>
<MentionableIcon item={item} />
{item.title}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="-m-1.5 no-scrollbar flex gap-1 overflow-y-auto p-1.5">
{mentions.map((mention) => {
const item = SAMPLE_DATA.mentionable.find(
(item) => item.title === mention
)
if (!item) {
return null
}
return (
<InputGroupButton
key={mention}
size="sm"
variant="secondary"
className="rounded-full pl-2!"
onClick={() => {
setMentions((prev) => prev.filter((m) => m !== mention))
}}
>
<MentionableIcon item={item} />
{item.title}
<IconX />
</InputGroupButton>
)
})}
</div>
</InputGroupAddon>
<InputGroupAddon align="block-end" className="gap-1">
<Tooltip>
<TooltipTrigger asChild>
<InputGroupButton
size="icon-sm"
className="rounded-full"
aria-label="Attach file"
>
<IconPaperclip />
</InputGroupButton>
</TooltipTrigger>
<TooltipContent>Attach file</TooltipContent>
</Tooltip>
<DropdownMenu
open={modelPopoverOpen}
onOpenChange={setModelPopoverOpen}
>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
{selectedModel.name}
</InputGroupButton>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>Select AI model</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="top"
align="start"
className="min-w-48"
>
<DropdownMenuGroup>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Select Agent Mode
</DropdownMenuLabel>
{SAMPLE_DATA.models.map((model) => (
<DropdownMenuCheckboxItem
key={model.name}
checked={model.name === selectedModel.name}
onCheckedChange={(checked) => {
if (checked) {
setSelectedModel(model)
}
}}
className="pl-2 *:[span:first-child]:right-2 *:[span:first-child]:left-auto"
>
{model.name}
{model.badge && (
<Badge
variant="secondary"
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
>
{model.badge}
</Badge>
)}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu open={scopeMenuOpen} onOpenChange={setScopeMenuOpen}>
<DropdownMenuTrigger asChild>
<InputGroupButton size="sm" className="rounded-full">
<IconWorld /> All Sources
</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="end" className="w-72">
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="web-search">
<IconWorld /> Web Search{" "}
<Switch
id="web-search"
className="ml-auto"
defaultChecked
/>
</label>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
asChild
onSelect={(e) => e.preventDefault()}
>
<label htmlFor="apps">
<IconApps /> Apps and Integrations
<Switch id="apps" className="ml-auto" defaultChecked />
</label>
</DropdownMenuItem>
<DropdownMenuItem>
<IconCircleDashedPlus /> All Sources I can access
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Avatar className="size-4">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
shadcn
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-72 p-0 [--radius:1rem]">
<Command>
<CommandInput
placeholder="Find or use knowledge in..."
autoFocus
/>
<CommandList>
<CommandEmpty>No knowledge found</CommandEmpty>
<CommandGroup>
{SAMPLE_DATA.mentionable
.filter((item) => item.type === "user")
.map((user) => (
<CommandItem
key={user.title}
value={user.title}
onSelect={() => {
// Handle user selection here
console.log("Selected user:", user.title)
}}
>
<Avatar className="size-4">
<AvatarImage src={user.image} />
<AvatarFallback>
{user.title[0]}
</AvatarFallback>
</Avatar>
{user.title}{" "}
<span className="text-muted-foreground">
- {user.workspace}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem>
<IconBook /> Help Center
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconPlus /> Connect Apps
</DropdownMenuItem>
<DropdownMenuLabel className="text-xs text-muted-foreground">
We&apos;ll only search in the sources selected here.
</DropdownMenuLabel>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupButton
aria-label="Send"
className="ml-auto rounded-full"
variant="default"
size="icon-sm"
>
<IconArrowUp />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</form>
)
}

View File

@@ -1,21 +0,0 @@
import { Badge } from "@/styles/radix-nova/ui/badge"
import { Spinner } from "@/styles/radix-nova/ui/spinner"
export function SpinnerBadge() {
return (
<div className="flex items-center gap-2">
<Badge>
<Spinner />
Syncing
</Badge>
<Badge variant="secondary">
<Spinner />
Updating
</Badge>
<Badge variant="outline">
<Spinner />
Loading
</Badge>
</div>
)
}

View File

@@ -1,31 +0,0 @@
import { Button } from "@/styles/radix-nova/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/styles/radix-nova/ui/empty"
import { Spinner } from "@/styles/radix-nova/ui/spinner"
export function SpinnerEmpty() {
return (
<Empty className="w-full border md:p-6">
<EmptyHeader>
<EmptyMedia variant="icon">
<Spinner />
</EmptyMedia>
<EmptyTitle>Processing your request</EmptyTitle>
<EmptyDescription>
Please wait while we process your request. Do not refresh the page.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm">
Cancel
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -1,6 +1,7 @@
import { type Metadata } from "next"
import Image from "next/image"
import Link from "next/link"
import { IconArrowRight } from "@tabler/icons-react"
import { Announcement } from "@/components/announcement"
import {
@@ -9,9 +10,9 @@ import {
PageHeaderDescription,
PageHeaderHeading,
} from "@/components/page-header"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Button } from "@/styles/radix-nova/ui/button"
import { RootComponents } from "./components"
import { CardsDemo } from "./cards"
const title = "The Foundation for your Design System"
const description =
@@ -47,41 +48,40 @@ export const metadata: Metadata = {
export default function IndexPage() {
return (
<div className="flex flex-1 flex-col">
<PageHeader>
<PageHeader className="md:**:[.container]:pb-8 lg:**:[.container]:pb-12">
<Announcement />
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
<PageHeaderDescription>{description}</PageHeaderDescription>
<PageActions>
<Button asChild size="sm" className="h-[31px] rounded-lg">
<Link href="/create">New Project</Link>
</Button>
<Button asChild size="sm" variant="ghost" className="rounded-lg">
<Link href="/docs/components">View Components</Link>
<Button asChild className="h-[31px] rounded-lg">
<Link href="/create?preset=b27GcrRo">
Build Your Own <IconArrowRight data-icon="inline-end" />
</Link>
</Button>
</PageActions>
</PageHeader>
<div className="container-wrapper flex-1 pb-6">
<div className="container overflow-hidden">
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
<div className="container-wrapper flex-1 p-0">
<div className="container overflow-hidden md:px-0 lg:max-w-none">
<section className="-mx-4 w-[140vw] overflow-hidden md:hidden">
<Image
src="/r/styles/new-york-v4/dashboard-01-light.png"
width={1400}
height={875}
src="/images/full-light.png"
width={2560}
height={2764}
alt="Dashboard"
className="block dark:hidden"
className="block h-auto w-full dark:hidden"
priority
/>
<Image
src="/r/styles/new-york-v4/dashboard-01-dark.png"
width={1400}
height={875}
src="/images/full-dark.png"
width={2560}
height={2764}
alt="Dashboard"
className="hidden dark:block"
className="hidden h-auto w-full dark:block"
priority
/>
</section>
<section className="hidden theme-container md:block">
<RootComponents />
<section className="hidden md:block">
<CardsDemo />
</section>
</div>
</div>

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

@@ -6,6 +6,7 @@ import { type RegistryItem } from "shadcn/schema"
import { useIsMobile } from "@/hooks/use-mobile"
import { getThemesForBaseColor, STYLES } from "@/registry/config"
import { Button } from "@/styles/base-nova/ui/button"
import {
Card,
CardContent,
@@ -32,11 +33,22 @@ import { ThemePicker } from "@/app/(app)/create/components/theme-picker"
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/create/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
// Only visible when user clicks "Create Project".
const ProjectForm = dynamic(() =>
import("@/app/(app)/create/components/project-form").then(
(m) => m.ProjectForm
)
// Only visible when user clicks "Create Project". Rendered client-only to
// avoid a useId hydration mismatch on the Base UI dialog trigger. The loading
// placeholder mirrors the trigger button exactly so there is no layout shift.
const ProjectForm = dynamic(
() =>
import("@/app/(app)/create/components/project-form").then(
(m) => m.ProjectForm
),
{
ssr: false,
loading: () => (
<Button disabled aria-hidden>
Get Code
</Button>
),
}
)
export function Customizer({
@@ -55,7 +67,7 @@ export function Customizer({
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 shadow-xl backdrop-blur-xl md:w-(--customizer-width)"
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"
>

View File

@@ -143,6 +143,11 @@ export function DesignSystemProvider({
React.useEffect(() => {
if (style === "lyra" || (style === "sera" && radius !== "none")) {
setSearchParams({ radius: "none" })
return
}
if (style === "rhea" && radius === "large") {
setSearchParams({ radius: "default" })
}
}, [style, radius, setSearchParams])

View File

@@ -3,6 +3,7 @@
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,
@@ -160,6 +161,7 @@ export function Preview() {
title="Preview"
/>
</div>
<CreateDevtools />
<PreviewSwitcher />
</div>
)

View File

@@ -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

@@ -24,15 +24,10 @@ const LocksContext = React.createContext<LocksContextValue | null>(null)
export function LocksProvider({ children }: { children: React.ReactNode }) {
const [locks, setLocks] = React.useState<Set<LockableParam>>(new Set())
const locksRef = React.useRef(locks)
React.useEffect(() => {
locksRef.current = locks
}, [locks])
// Stable callback — reads from ref so it doesn't change on every lock toggle.
const isLocked = React.useCallback(
(param: LockableParam) => locksRef.current.has(param),
[]
(param: LockableParam) => locks.has(param),
[locks]
)
const toggleLock = React.useCallback((param: LockableParam) => {

View File

@@ -42,7 +42,8 @@ export async function getBaseComponent(name: string, base: BaseName) {
return null
}
return index[name].component
const { Components } = await import("@/registry/bases/__components__")
return Components[base]?.[name] ?? null
}
export async function getAllItems() {

View File

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

View File

@@ -1,195 +1,14 @@
import {
DM_Sans,
EB_Garamond,
Figtree,
Geist,
Geist_Mono,
IBM_Plex_Sans,
Instrument_Sans,
Instrument_Serif,
Inter,
JetBrains_Mono,
Lora,
Manrope,
Merriweather,
Montserrat,
Noto_Sans,
Noto_Serif,
Nunito_Sans,
Outfit,
Oxanium,
Playfair_Display,
Public_Sans,
Raleway,
Roboto,
Roboto_Slab,
Source_Sans_3,
Space_Grotesk,
} from "next/font/google"
import { FONT_DEFINITIONS, type FontName } from "@/lib/font-definitions"
type PreviewFont = ReturnType<typeof Inter>
const geistSans = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
})
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
})
const notoSans = Noto_Sans({
subsets: ["latin"],
variable: "--font-noto-sans",
})
const nunitoSans = Nunito_Sans({
subsets: ["latin"],
variable: "--font-nunito-sans",
})
const figtree = Figtree({
subsets: ["latin"],
variable: "--font-figtree",
})
const roboto = Roboto({
subsets: ["latin"],
variable: "--font-roboto",
})
const raleway = Raleway({
subsets: ["latin"],
variable: "--font-raleway",
})
const dmSans = DM_Sans({
subsets: ["latin"],
variable: "--font-dm-sans",
})
const publicSans = Public_Sans({
subsets: ["latin"],
variable: "--font-public-sans",
})
const outfit = Outfit({
subsets: ["latin"],
variable: "--font-outfit",
})
const oxanium = Oxanium({
subsets: ["latin"],
variable: "--font-oxanium",
})
const manrope = Manrope({
subsets: ["latin"],
variable: "--font-manrope",
})
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
variable: "--font-space-grotesk",
})
const montserrat = Montserrat({
subsets: ["latin"],
variable: "--font-montserrat",
})
const ibmPlexSans = IBM_Plex_Sans({
subsets: ["latin"],
variable: "--font-ibm-plex-sans",
})
const sourceSans3 = Source_Sans_3({
subsets: ["latin"],
variable: "--font-source-sans-3",
})
const instrumentSans = Instrument_Sans({
subsets: ["latin"],
variable: "--font-instrument-sans",
})
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains-mono",
})
const geistMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-geist-mono",
})
const notoSerif = Noto_Serif({
subsets: ["latin"],
variable: "--font-noto-serif",
})
const robotoSlab = Roboto_Slab({
subsets: ["latin"],
variable: "--font-roboto-slab",
})
const merriweather = Merriweather({
subsets: ["latin"],
variable: "--font-merriweather",
})
const lora = Lora({
subsets: ["latin"],
variable: "--font-lora",
})
const playfairDisplay = Playfair_Display({
subsets: ["latin"],
variable: "--font-playfair-display",
})
const ebGaramond = EB_Garamond({
subsets: ["latin"],
variable: "--font-eb-garamond",
})
const instrumentSerif = Instrument_Serif({
subsets: ["latin"],
weight: "400",
variable: "--font-instrument-serif",
})
const PREVIEW_FONTS = {
geist: geistSans,
inter,
"noto-sans": notoSans,
"nunito-sans": nunitoSans,
figtree,
roboto,
raleway,
"dm-sans": dmSans,
"public-sans": publicSans,
outfit,
oxanium,
manrope,
"space-grotesk": spaceGrotesk,
montserrat,
"ibm-plex-sans": ibmPlexSans,
"source-sans-3": sourceSans3,
"instrument-sans": instrumentSans,
"jetbrains-mono": jetbrainsMono,
"geist-mono": geistMono,
"noto-serif": notoSerif,
"roboto-slab": robotoSlab,
merriweather,
lora,
"playfair-display": playfairDisplay,
"eb-garamond": ebGaramond,
"instrument-serif": instrumentSerif,
} satisfies Record<FontName, PreviewFont>
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)
@@ -201,7 +20,15 @@ function createFontOption(name: FontName) {
return {
name: definition.title,
value: definition.name,
font: PREVIEW_FONTS[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
}

View File

@@ -76,6 +76,11 @@ export const RANDOMIZE_BIASES: RandomizeBiases = {
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) => {

View File

@@ -93,7 +93,7 @@ export default async function Page(props: {
>
<div className="flex min-w-0 flex-1 flex-col">
<div className="h-(--top-spacing) shrink-0" />
<div className="mx-auto flex w-full max-w-[40rem] min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
<div className="mx-auto flex w-full max-w-160 min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-foreground md:px-0 lg:py-8 dark:text-foreground">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between md:items-start">
@@ -185,7 +185,7 @@ export default async function Page(props: {
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-(--sidebar-width) flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="h-(--top-spacing) shrink-0"></div>
{doc.toc?.length ? (
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
<div className="flex scroll-fade scrollbar-none flex-col gap-8 overflow-y-auto px-8">
<DocsTableOfContents toc={doc.toc} />
</div>
) : null}

View File

@@ -46,7 +46,7 @@ export default function ChangelogPage() {
>
<div className="flex min-w-0 flex-1 flex-col">
<div className="h-(--top-spacing) shrink-0" />
<div className="mx-auto flex w-full max-w-160 min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
<div className="mx-auto flex w-full max-w-160 min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-foreground md:px-0 lg:py-8 dark:text-foreground">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h1 className="scroll-m-24 text-4xl font-semibold tracking-tight sm:text-3xl">

View File

@@ -15,6 +15,9 @@ import { PreviewStyle } from "@/app/(app)/create/components/preview-style"
import { RandomizeScript } from "@/app/(app)/create/components/random-button"
import { getBaseComponent, getBaseItem } from "@/app/(app)/create/lib/api"
import "@/app/style-registry.css"
import "streamdown/styles.css"
export const revalidate = false
export const dynamic = "force-static"
export const dynamicParams = true

View File

@@ -0,0 +1,21 @@
"use client"
import * as React from "react"
export function PreviewFontVariables({ className }: { className: string }) {
React.useLayoutEffect(() => {
const classNames = className.split(/\s+/).filter(Boolean)
if (!classNames.length) {
return
}
document.documentElement.classList.add(...classNames)
return () => {
document.documentElement.classList.remove(...classNames)
}
}, [className])
return null
}

View File

@@ -0,0 +1,190 @@
import {
DM_Sans,
EB_Garamond,
Figtree,
Geist,
Geist_Mono,
IBM_Plex_Sans,
Instrument_Sans,
Instrument_Serif,
Inter,
JetBrains_Mono,
Lora,
Manrope,
Merriweather,
Montserrat,
Noto_Sans,
Noto_Serif,
Nunito_Sans,
Outfit,
Oxanium,
Playfair_Display,
Public_Sans,
Raleway,
Roboto,
Roboto_Slab,
Source_Sans_3,
Space_Grotesk,
} from "next/font/google"
import { cn } from "@/lib/utils"
const geistSans = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
})
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
})
const notoSans = Noto_Sans({
subsets: ["latin"],
variable: "--font-noto-sans",
})
const nunitoSans = Nunito_Sans({
subsets: ["latin"],
variable: "--font-nunito-sans",
})
const figtree = Figtree({
subsets: ["latin"],
variable: "--font-figtree",
})
const roboto = Roboto({
subsets: ["latin"],
variable: "--font-roboto",
})
const raleway = Raleway({
subsets: ["latin"],
variable: "--font-raleway",
})
const dmSans = DM_Sans({
subsets: ["latin"],
variable: "--font-dm-sans",
})
const publicSans = Public_Sans({
subsets: ["latin"],
variable: "--font-public-sans",
})
const outfit = Outfit({
subsets: ["latin"],
variable: "--font-outfit",
})
const oxanium = Oxanium({
subsets: ["latin"],
variable: "--font-oxanium",
})
const manrope = Manrope({
subsets: ["latin"],
variable: "--font-manrope",
})
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
variable: "--font-space-grotesk",
})
const montserrat = Montserrat({
subsets: ["latin"],
variable: "--font-montserrat",
})
const ibmPlexSans = IBM_Plex_Sans({
subsets: ["latin"],
variable: "--font-ibm-plex-sans",
})
const sourceSans3 = Source_Sans_3({
subsets: ["latin"],
variable: "--font-source-sans-3",
})
const instrumentSans = Instrument_Sans({
subsets: ["latin"],
variable: "--font-instrument-sans",
})
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains-mono",
})
const geistMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-geist-mono",
})
const notoSerif = Noto_Serif({
subsets: ["latin"],
variable: "--font-noto-serif",
})
const robotoSlab = Roboto_Slab({
subsets: ["latin"],
variable: "--font-roboto-slab",
})
const merriweather = Merriweather({
subsets: ["latin"],
variable: "--font-merriweather",
})
const lora = Lora({
subsets: ["latin"],
variable: "--font-lora",
})
const playfairDisplay = Playfair_Display({
subsets: ["latin"],
variable: "--font-playfair-display",
})
const ebGaramond = EB_Garamond({
subsets: ["latin"],
variable: "--font-eb-garamond",
})
const instrumentSerif = Instrument_Serif({
subsets: ["latin"],
weight: "400",
variable: "--font-instrument-serif",
})
export const previewFontVariables = cn(
geistSans.variable,
inter.variable,
notoSans.variable,
nunitoSans.variable,
figtree.variable,
roboto.variable,
raleway.variable,
dmSans.variable,
publicSans.variable,
outfit.variable,
oxanium.variable,
manrope.variable,
spaceGrotesk.variable,
montserrat.variable,
ibmPlexSans.variable,
sourceSans3.variable,
instrumentSans.variable,
geistMono.variable,
jetbrainsMono.variable,
notoSerif.variable,
robotoSlab.variable,
merriweather.variable,
lora.variable,
playfairDisplay.variable,
ebGaramond.variable,
instrumentSerif.variable
)

View File

@@ -0,0 +1,15 @@
import { PreviewFontVariables } from "@/app/(create)/preview/font-variables"
import { previewFontVariables } from "@/app/(create)/preview/fonts"
export default function PreviewLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className={previewFontVariables}>
<PreviewFontVariables className={previewFontVariables} />
{children}
</div>
)
}

View File

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

View File

@@ -3,13 +3,7 @@
@import "shadcn/tailwind.css";
@import "./legacy-themes.css";
@import "../registry/styles/style-vega.css" layer(base);
@import "../registry/styles/style-nova.css" layer(base);
@import "../registry/styles/style-lyra.css" layer(base);
@import "../registry/styles/style-maia.css" layer(base);
@import "../registry/styles/style-mira.css" layer(base);
@import "../registry/styles/style-luma.css" layer(base);
@import "../registry/styles/style-sera.css" layer(base);
@source "../node_modules/streamdown/dist/*.js";
@custom-variant style-vega (&:where(.style-vega *));
@custom-variant style-nova (&:where(.style-nova *));
@@ -18,6 +12,7 @@
@custom-variant style-mira (&:where(.style-mira *));
@custom-variant style-luma (&:where(.style-luma *));
@custom-variant style-sera (&:where(.style-sera *));
@custom-variant style-rhea (&:where(.style-rhea *));
@custom-variant dark (&:is(.dark *));
@custom-variant fixed (&:is(.layout-fixed *));
@@ -80,12 +75,12 @@
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--foreground: oklch(0% 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--card-foreground: oklch(0% 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--popover-foreground: oklch(0% 0 0);
--primary: oklch(0% 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
@@ -104,7 +99,7 @@
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-foreground: oklch(0% 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
@@ -117,7 +112,7 @@
--code-foreground: var(--surface-foreground);
--code-highlight: oklch(0.96 0 0);
--code-number: oklch(0.56 0 0);
--selection: oklch(0.145 0 0);
--selection: oklch(0% 0 0);
--selection-foreground: oklch(1 0 0);
}
@@ -211,7 +206,7 @@
}
@utility section-soft {
@apply from-background to-surface/40 dark:bg-background 3xl:fixed:bg-none bg-linear-to-b;
@apply bg-linear-to-b from-background to-surface/40 dark:bg-background 3xl:fixed:bg-none;
}
@utility theme-container {
@@ -219,11 +214,11 @@
}
@utility container-wrapper {
@apply 3xl:fixed:max-w-[calc(var(--breakpoint-2xl)+2rem)] mx-auto w-full px-2;
@apply mx-auto w-full px-2 3xl:fixed:max-w-[calc(var(--breakpoint-2xl)+2rem)];
}
@utility container {
@apply 3xl:max-w-screen-2xl mx-auto max-w-[1400px] px-4 lg:px-8;
@apply mx-auto max-w-[1400px] px-4 3xl:max-w-screen-2xl lg:px-8;
}
@utility no-scrollbar {
@@ -236,14 +231,14 @@
}
@utility border-ghost {
@apply after:border-border relative after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten;
@apply relative after:absolute after:inset-0 after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten;
}
@utility step {
counter-increment: step;
&:before {
@apply border-background bg-muted mr-2 inline-flex size-6 items-center justify-center rounded-full text-center -indent-px font-mono text-sm font-medium md:absolute md:mt-[-4px] md:ml-[-50px] md:size-9 md:border-4;
@apply mr-2 inline-flex size-6 items-center justify-center rounded-full border-background bg-muted text-center -indent-px font-mono text-sm font-medium md:absolute md:mt-[-4px] md:ml-[-50px] md:size-9 md:border-4;
content: counter(step);
}
}
@@ -291,6 +286,14 @@
}
}
[data-rehype-pretty-code-figure] code,
[data-rehype-pretty-code-figure] code span {
font-variant-ligatures: none;
font-feature-settings:
"liga" 0,
"calt" 0;
}
[data-rehype-pretty-code-title] {
border-bottom: color-mix(in oklab, var(--border) 30%, transparent);
border-bottom-width: 1px;
@@ -353,14 +356,8 @@
white-space: pre;
line-height: 0.95;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
"Liberation Mono",
"Courier New",
monospace;
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-variant-ligatures: none;
}

View File

@@ -4,7 +4,6 @@ import { NuqsAdapter } from "nuqs/adapters/next/app"
import { META_THEME_COLORS, siteConfig } from "@/lib/config"
import { fontVariables } from "@/lib/fonts"
import { cn } from "@/lib/utils"
import { LayoutProvider } from "@/hooks/use-layout"
import { ActiveThemeProvider } from "@/components/active-theme"
import { Analytics } from "@/components/analytics"
import { TailwindIndicator } from "@/components/tailwind-indicator"
@@ -81,9 +80,6 @@ export default function RootLayout({
if (localStorage.theme === 'dark' || ((!('theme' in localStorage) || localStorage.theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${META_THEME_COLORS.dark}')
}
if (localStorage.layout) {
document.documentElement.classList.add('layout-' + localStorage.layout)
}
} catch (_) {}
`,
}}
@@ -92,24 +88,22 @@ export default function RootLayout({
</head>
<body
className={cn(
"group/body overscroll-none antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]"
"group/body overscroll-none antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] lg:[--header-height:calc(var(--spacing)*16)] xl:[--footer-height:calc(var(--spacing)*24)]"
)}
>
<ThemeProvider>
<LayoutProvider>
<ActiveThemeProvider>
<NuqsAdapter>
<BaseTooltipProvider delay={0}>
<RadixTooltipProvider delayDuration={0}>
{children}
<Toaster position="top-center" />
</RadixTooltipProvider>
</BaseTooltipProvider>
</NuqsAdapter>
<TailwindIndicator />
<Analytics />
</ActiveThemeProvider>
</LayoutProvider>
<ActiveThemeProvider>
<NuqsAdapter>
<BaseTooltipProvider delay={0}>
<RadixTooltipProvider delayDuration={0}>
{children}
<Toaster position="top-center" />
</RadixTooltipProvider>
</BaseTooltipProvider>
</NuqsAdapter>
<TailwindIndicator />
<Analytics />
</ActiveThemeProvider>
</ThemeProvider>
</body>
</html>

View File

@@ -394,3 +394,243 @@
--sidebar-ring: var(--color-violet-900);
}
}
.theme-sketch {
--background: oklch(0.9721 0.0158 110.5501);
--foreground: oklch(0.5066 0.2501 271.8903);
--card: oklch(0.9721 0.0158 110.5501);
--card-foreground: oklch(0.5066 0.2501 271.8903);
--popover: oklch(0.9721 0.0158 110.5501);
--popover-foreground: oklch(0.5066 0.2501 271.8903);
--primary: oklch(0.5066 0.2501 271.8903);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(1 0 0);
--secondary-foreground: oklch(0.5066 0.2501 271.8903);
--muted: oklch(0.9189 0.0147 106.6853);
--muted-foreground: oklch(0.5066 0.2501 271.8903);
--accent: oklch(0.9168 0.0214 109.7161);
--accent-foreground: oklch(0.4486 0.2266 271.5512);
--destructive: oklch(0.63 0.19 23.03);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.5066 0.2501 271.8903);
--input: oklch(0.5066 0.2501 271.8903);
--ring: oklch(0.468 0.2721 279.6007);
--chart-1: oklch(0.5066 0.2501 271.8903);
--chart-2: oklch(0.7 0.19 48);
--chart-3: oklch(0.77 0.2 131);
--chart-4: oklch(0.68 0.15 237);
--chart-5: oklch(0.66 0.21 354);
--sidebar: oklch(0.9721 0.0158 110.5501);
--sidebar-foreground: oklch(0.5066 0.2501 271.8903);
--sidebar-primary: oklch(0.5066 0.2501 271.8903);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.9168 0.0214 109.7161);
--sidebar-accent-foreground: oklch(0.4486 0.2266 271.5512);
--sidebar-border: oklch(0.4486 0.2266 271.5512);
--sidebar-ring: oklch(0.4486 0.2266 271.5512);
--font-sans: Geist Mono;
--shadow-2xs: 4px 4px 0px 0px hsl(238.3146 85.5769% 59.2157% / 0.07);
--shadow-xs: 4px 4px 0px 0px hsl(238.3146 85.5769% 59.2157% / 0.07);
--shadow-sm:
4px 4px 0px 0px hsl(238.3146 85.5769% 59.2157% / 0.15),
4px 1px 2px -1px hsl(238.3146 85.5769% 59.2157% / 0.15);
--shadow:
4px 4px 0px 0px hsl(238.3146 85.5769% 59.2157% / 0.15),
4px 1px 2px -1px hsl(238.3146 85.5769% 59.2157% / 0.15);
--shadow-md:
4px 4px 0px 0px hsl(238.3146 85.5769% 59.2157% / 0.15),
4px 2px 4px -1px hsl(238.3146 85.5769% 59.2157% / 0.15);
--shadow-lg:
4px 4px 0px 0px hsl(238.3146 85.5769% 59.2157% / 0.15),
4px 4px 6px -1px hsl(238.3146 85.5769% 59.2157% / 0.15);
--shadow-xl:
4px 4px 0px 0px hsl(238.3146 85.5769% 59.2157% / 0.15),
4px 8px 10px -1px hsl(238.3146 85.5769% 59.2157% / 0.15);
--shadow-2xl: 4px 4px 0px 0px hsl(238.3146 85.5769% 59.2157% / 0.38);
--radius: 0rem;
@variant dark {
--background: oklch(0.256 0.151 268.343);
--foreground: oklch(0.972 0.016 110.55);
--card: oklch(0.256 0.151 268.343);
--card-foreground: oklch(0.972 0.016 110.55);
--popover: oklch(0.507 0.25 271.89);
--popover-foreground: oklch(0.972 0.016 110.55);
--primary: oklch(0.972 0.016 110.55);
--primary-foreground: oklch(0.253 0.094 275.725);
--secondary: oklch(1 0 0 / 0.2);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(0.228 0.127 269.556);
--muted-foreground: oklch(0.972 0.016 110.55);
--accent: oklch(0.228 0.127 269.556);
--accent-foreground: oklch(0.972 0.016 110.55);
--destructive: oklch(0.711 0.166 22.216);
--destructive-foreground: oklch(0 0 0);
--border: oklch(0.427 0.149 277.089);
--input: oklch(0.427 0.149 277.089);
--ring: oklch(1 0 0);
--chart-1: oklch(0.972 0.016 110.55);
--chart-2: oklch(0.7 0.19 48);
--chart-3: oklch(0.77 0.2 131);
--chart-4: oklch(0.68 0.15 237);
--chart-5: oklch(0.66 0.21 354);
--sidebar: oklch(0.256 0.151 268.343);
--sidebar-foreground: oklch(0.972 0.016 110.55);
--sidebar-primary: oklch(0.507 0.25 271.89);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.416 0.203 272.082);
--sidebar-accent-foreground: oklch(0.972 0.016 110.55);
--sidebar-border: oklch(0.972 0.016 110.55);
--sidebar-ring: oklch(0.972 0.016 110.55);
--sidebar-background: oklch(0.253 0.094 275.725);
--font-sans: Geist Mono;
--shadow-2xs: 4px 4px 0px 0px oklch(0.427 0.149 277.089 / 0.5);
--shadow-xs: 4px 4px 0px 0px oklch(0.427 0.149 277.089 / 0.5);
--shadow-sm: 4px 4px 0px 0px oklch(0.427 0.149 277.089 / 0.5);
--shadow: 4px 4px 0px 0px oklch(0.427 0.149 277.089 / 0.5);
--shadow-md: 4px 4px 0px 0px oklch(0.427 0.149 277.089 / 0.5);
--shadow-lg: 4px 4px 0px 0px oklch(0.427 0.149 277.089 / 0.5);
--shadow-xl: 4px 4px 0px 0px oklch(0.427 0.149 277.089 / 0.5);
--shadow-2xl: 4px 4px 0px 0px oklch(0.427 0.149 277.089 / 0.5);
}
}
.theme-neutral {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
@variant dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
}
.theme-blue {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.488 0.243 264.376);
--primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.546 0.245 262.881);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--selection: oklch(0.93 0.03 256);
--selection-foreground: oklch(0.145 0 0);
@variant dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.424 0.199 265.638);
--primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.623 0.214 259.815);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
}

View File

@@ -0,0 +1,16 @@
import { NextResponse } from "next/server"
import directory from "@/registry/directory.json"
export const dynamic = "force-static"
export async function GET() {
const registries = directory.map(({ name, homepage, url, description }) => ({
name,
homepage,
url,
description,
}))
return NextResponse.json(registries)
}

View File

@@ -0,0 +1,10 @@
@reference "./globals.css";
@import "../registry/styles/style-vega.css" layer(base);
@import "../registry/styles/style-nova.css" layer(base);
@import "../registry/styles/style-lyra.css" layer(base);
@import "../registry/styles/style-maia.css" layer(base);
@import "../registry/styles/style-mira.css" layer(base);
@import "../registry/styles/style-luma.css" layer(base);
@import "../registry/styles/style-sera.css" layer(base);
@import "../registry/styles/style-rhea.css" layer(base);

View File

@@ -7,7 +7,7 @@ export function Announcement() {
return (
<Badge asChild variant="secondary" className="bg-muted">
<Link href="/docs/changelog">
New preset commands <ArrowRightIcon />
Components for Chat Interfaces <ArrowRightIcon />
</Link>
</Badge>
)

View File

@@ -391,7 +391,7 @@ export function CommandMenu({
<Button
variant="outline"
className={cn(
"relative h-8 w-full justify-start rounded-lg pl-3 font-normal text-foreground shadow-none hover:bg-muted/50 sm:pr-12 md:w-48 lg:w-40 xl:w-64 dark:bg-card"
"relative h-8 w-full justify-start rounded-lg border-none bg-muted pl-3 text-foreground shadow-none transition-colors hover:bg-muted/50 md:w-48 lg:w-40 xl:w-64 dark:bg-card"
)}
onClick={() => setOpen(true)}
{...props}

View File

@@ -1,33 +1,65 @@
import Link from "next/link"
import { PAGES_NEW } from "@/lib/docs"
import { getPagesFromFolder, type PageTreeFolder } from "@/lib/page-tree"
import {
getPagesFromFolder,
type PageTreeFolder,
type PageTreePage,
} from "@/lib/page-tree"
function ComponentLink({
component,
showNewIndicator,
}: {
component: PageTreePage
showNewIndicator: boolean
}) {
const isNew = showNewIndicator && PAGES_NEW.includes(component.url)
return (
<Link
href={component.url}
className="inline-flex items-center gap-2 text-lg font-medium underline-offset-4 hover:underline md:text-base"
>
{component.name}
{isNew && (
<>
<span className="sr-only">New</span>
<span
aria-hidden="true"
className="flex size-2 rounded-full bg-blue-500"
/>
</>
)}
</Link>
)
}
export function ComponentsList({
componentsFolder,
currentBase,
variant = "all",
}: {
componentsFolder: PageTreeFolder
currentBase: string
variant?: "all" | "new"
}) {
const list = getPagesFromFolder(componentsFolder, currentBase)
const list = getPagesFromFolder(componentsFolder, currentBase).filter(
(component) => variant === "all" || PAGES_NEW.includes(component.url)
)
if (!list.length) {
return null
}
return (
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 md:gap-x-8 lg:gap-x-16 lg:gap-y-6 xl:gap-x-20">
<div className="mt-8 grid grid-cols-2 gap-4 md:grid-cols-3 md:gap-x-8 lg:gap-x-16 lg:gap-y-6 xl:gap-x-20">
{list.map((component) => (
<Link
<ComponentLink
key={component.$id}
href={component.url}
className="inline-flex items-center gap-2 text-lg font-medium underline-offset-4 hover:underline md:text-base"
>
{component.name}
{PAGES_NEW.includes(component.url) && (
<span
className="flex size-2 rounded-full bg-blue-500"
title="New"
/>
)}
</Link>
component={component}
showNewIndicator={variant === "all"}
/>
))}
</div>
)

View File

@@ -160,6 +160,18 @@ function DirectoryPaginationNext({
}
export function DirectoryList() {
return (
<DirectoryAddProvider>
<div className="mt-6">
<React.Suspense fallback={<DirectoryListSkeleton />}>
<DirectoryListContent />
</React.Suspense>
</div>
</DirectoryAddProvider>
)
}
function DirectoryListContent() {
const pathname = usePathname()
const {
isLoading,
@@ -204,119 +216,115 @@ export function DirectoryList() {
[page, setPage]
)
if (isLoading) {
return <DirectoryListSkeleton />
}
return (
<DirectoryAddProvider>
<div className="mt-6">
{isLoading ? (
<DirectoryListSkeleton />
) : (
<>
<SearchDirectory
query={query}
registriesCount={registries.length}
setQuery={setQuery}
/>
<ItemGroup className="my-8">
{paginatedRegistries.map((registry, index) => (
<React.Fragment key={registry.name}>
<Item className="group/item relative gap-6 px-0">
<ItemMedia
variant="image"
dangerouslySetInnerHTML={{ __html: registry.logo }}
className="grayscale *:[svg]:size-8 *:[svg]:fill-foreground"
/>
<ItemContent>
<ItemTitle>
<a
href={getHomepageUrl(registry.homepage)}
target="_blank"
rel="noopener noreferrer external"
className="group flex items-center gap-1"
>
{registry.name}{" "}
<IconArrowUpRight className="size-4 opacity-0 group-hover:opacity-100" />
</a>
</ItemTitle>
{registry.description && (
<ItemDescription className="text-pretty">
{registry.description}
</ItemDescription>
)}
</ItemContent>
<ItemActions className="relative z-10 hidden self-start sm:flex">
<DirectoryAddButton registry={registry} />
</ItemActions>
<ItemFooter className="justify-start pl-16 sm:hidden">
<Button size="sm" variant="outline">
View <IconArrowUpRight />
</Button>
<DirectoryAddButton registry={registry} />
</ItemFooter>
</Item>
{index < paginatedRegistries.length - 1 && (
<ItemSeparator className="my-1" />
)}
</React.Fragment>
))}
</ItemGroup>
{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<DirectoryPaginationPrevious
href={previousHref}
aria-disabled={page <= 1 || undefined}
tabIndex={page <= 1 ? -1 : undefined}
onClick={(event) =>
handlePageChange(event, page - 1, page <= 1)
}
className={cn(
page <= 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
)}
/>
</PaginationItem>
{getPageNumbers(page, totalPages).map((p, i) =>
p === "ellipsis" ? (
<PaginationItem key={`ellipsis-${i}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={p}>
<DirectoryPaginationLink
href={getPageHref(pathname, query, p)}
isActive={p === page}
onClick={(event) => handlePageChange(event, p)}
className="cursor-pointer"
>
{p}
</DirectoryPaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<DirectoryPaginationNext
href={nextHref}
aria-disabled={page >= totalPages || undefined}
tabIndex={page >= totalPages ? -1 : undefined}
onClick={(event) =>
handlePageChange(event, page + 1, page >= totalPages)
}
className={cn(
page >= totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
)}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<>
<SearchDirectory
query={query}
registriesCount={registries.length}
setQuery={setQuery}
/>
<ItemGroup className="my-8">
{paginatedRegistries.map((registry, index) => (
<React.Fragment key={registry.name}>
<Item className="group/item relative gap-6 px-0">
<ItemMedia
variant="image"
dangerouslySetInnerHTML={{ __html: registry.logo }}
className="grayscale *:[svg]:size-8 *:[svg]:fill-foreground"
/>
<ItemContent>
<ItemTitle>
<a
href={getHomepageUrl(registry.homepage)}
target="_blank"
rel="noopener noreferrer external"
className="group flex items-center gap-1"
>
{registry.name}{" "}
<IconArrowUpRight className="size-4 opacity-0 group-hover:opacity-100" />
</a>
</ItemTitle>
{registry.description && (
<ItemDescription className="text-pretty">
{registry.description}
</ItemDescription>
)}
</ItemContent>
<ItemActions className="relative z-10 hidden self-start sm:flex">
<DirectoryAddButton registry={registry} />
</ItemActions>
<ItemFooter className="justify-start pl-16 sm:hidden">
<Button size="sm" variant="outline">
View <IconArrowUpRight />
</Button>
<DirectoryAddButton registry={registry} />
</ItemFooter>
</Item>
{index < paginatedRegistries.length - 1 && (
<ItemSeparator className="my-1" />
)}
</>
)}
</div>
</DirectoryAddProvider>
</React.Fragment>
))}
</ItemGroup>
{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<DirectoryPaginationPrevious
href={previousHref}
aria-disabled={page <= 1 || undefined}
tabIndex={page <= 1 ? -1 : undefined}
onClick={(event) =>
handlePageChange(event, page - 1, page <= 1)
}
className={cn(
page <= 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
)}
/>
</PaginationItem>
{getPageNumbers(page, totalPages).map((p, i) =>
p === "ellipsis" ? (
<PaginationItem key={`ellipsis-${i}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={p}>
<DirectoryPaginationLink
href={getPageHref(pathname, query, p)}
isActive={p === page}
onClick={(event) => handlePageChange(event, p)}
className="cursor-pointer"
>
{p}
</DirectoryPaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<DirectoryPaginationNext
href={nextHref}
aria-disabled={page >= totalPages || undefined}
tabIndex={page >= totalPages ? -1 : undefined}
onClick={(event) =>
handlePageChange(event, page + 1, page >= totalPages)
}
className={cn(
page >= totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
)}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</>
)
}

View File

@@ -6,10 +6,12 @@ import { BASES } from "@/registry/bases"
export function DocsBaseSwitcher({
base,
component,
hrefPrefix = "/docs/components",
className,
}: {
base: string
component: string
hrefPrefix?: string
className?: string
}) {
const activeBase = BASES.find((baseItem) => base === baseItem.name)
@@ -19,7 +21,7 @@ export function DocsBaseSwitcher({
{BASES.map((baseItem) => (
<Link
key={baseItem.name}
href={`/docs/components/${baseItem.name}/${component}`}
href={`${hrefPrefix}/${baseItem.name}/${component}`}
data-active={base === baseItem.name}
className="relative inline-flex items-center justify-center gap-1 pt-1 pb-0.5 text-base font-medium text-muted-foreground transition-colors after:absolute after:inset-x-0 after:bottom-[-4px] after:h-0.5 after:bg-foreground after:opacity-0 after:transition-opacity hover:text-foreground data-[active=true]:text-foreground data-[active=true]:after:opacity-100"
>

View File

@@ -52,6 +52,10 @@ const TOP_LEVEL_SECTIONS = [
name: "Registry",
href: "/docs/registry",
},
{
name: "@shadcn/react",
href: "/docs/react",
},
{
name: "Forms",
href: "/docs/forms",
@@ -73,15 +77,12 @@ export function DocsSidebar({
return (
<Sidebar
className="sticky top-[calc(var(--header-height)+0.6rem)] z-30 hidden h-[calc(100svh-10rem)] overscroll-none bg-transparent [--sidebar-menu-width:--spacing(56)] lg:flex"
className="sticky top-[calc(var(--header-height)+0.6rem)] z-30 hidden h-[calc(100svh-10rem)] overflow-hidden overscroll-none bg-transparent [--sidebar-menu-width:--spacing(56)] lg:flex"
collapsible="none"
{...props}
>
<div className="h-9" />
<div className="absolute top-8 z-10 h-8 w-(--sidebar-menu-width) shrink-0 bg-linear-to-b from-background via-background/80 to-background/50 blur-xs" />
<div className="absolute top-12 right-2 bottom-0 hidden h-full w-px bg-linear-to-b from-transparent via-border to-transparent lg:flex" />
<SidebarContent className="mx-auto no-scrollbar w-(--sidebar-menu-width) overflow-x-hidden px-2">
<SidebarGroup className="pt-6">
<SidebarContent className="w-(--sidebar-menu-width) scroll-fade scrollbar-none overflow-x-hidden pl-2.5">
<SidebarGroup className="pt-12">
<SidebarGroupLabel className="font-medium text-muted-foreground">
Sections
</SidebarGroupLabel>
@@ -168,7 +169,6 @@ export function DocsSidebar({
</SidebarGroup>
)
})}
<div className="sticky -bottom-1 z-10 h-16 shrink-0 bg-linear-to-t from-background via-background/80 to-background/50 blur-xs" />
</SidebarContent>
</Sidebar>
)

View File

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

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