Compare commits

..

68 Commits

Author SHA1 Message Date
shadcn
1994caba0b perf: dev server (#10904)
* perf: dev server

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

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

* Update Saaskit entry in directory.json

* Add new registry entry for @saaskit

---------

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

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

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

---------

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

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

* chore: update lockfile after moving msw to devDependencies

* chore: add changeset for msw devDependency fix

---------

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

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

* fix

* fix: validate and search

* docs: update docs for GitHub registries

* docs: add changelog

* fix

* chore: update announcement

* docs(skills): update GitHub registry guidance

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

* fix(registry): limit search registry discovery

* fix(registry): bound GitHub validation concurrency

* fix(registry): reject whitespace in GitHub refs

* fix(registry): track URL dependency sources

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

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

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

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

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

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

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

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

---------

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

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

Fixes #10798

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 16:13:58 +04:00
shadcn
5c849297d0 feat(release): add beta and rc prerelease labels (#10806) 2026-05-29 15:13:21 +04:00
github-actions[bot]
2baa86081d chore(release): version packages (#10791)
Co-authored-by: shadcn <m@shadcn.com>
2026-05-29 12:10:45 +04:00
shadcn
980f288149 ci(templates): test pnpm 11 (#10790) 2026-05-29 11:15:40 +04:00
Raashish Aggarwal
07900769d9 fix(cli): update template handling for pnpm 11 (#10659)
* fix(cli): allow esbuild builds in Vite templates

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

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

* chore: changeset

* fix(templates): allow monorepo pnpm builds

* ci(templates): validate app workspace conversion

---------

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

* chore(changeset): add rtl quote preservation note

---------

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

* fix(cli): parse pnpm workspace packages

* chore(changeset): add shadcn patch

* chore(changeset): update description

* ci(templates): validate bun and npx init

* ci(templates): expand package manager validation

* ci(templates): parallelize validation

* ci(templates): allow yarn template lockfiles

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

* ci(templates): validate generated starters

* fix

* fix(templates): support pnpm 9 workspace config

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

* fix(v4): statically cache registries route

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

* fix: blocks

* feat: build chat example

* fix

* fix: sidebar

* fix

* feat: update home

* fix

* fix

* fix

* feat: optimizine fonts

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix: font in preview

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

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

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

* fix: declare pnpm 10 tool dependencies

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

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

* feat: updates

* fix

* refactor: implementation

* fix(registry): correct directory registry json

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

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

* feat: updates

* fix

* refactor: implementation

* fix(registry): correct directory registry json

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

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

## New Registry: @next-ui

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

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

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

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

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

* chore: update valibot dependency to version 1.4.0

* style(docs): format formisch imports

---------

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

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

* feat: switch to toggle

* fix

* fix: color
2026-05-12 15:15:07 +04:00
Sergey Kuznetsov
8ca30ed32c feat(registry): add @turbopills-ui registry (#10579) 2026-05-10 20:34:04 +04:00
Alex Kostyniuk
e2605bc7c2 Update glasscn registry URL (#10597) 2026-05-10 20:33:38 +04:00
Chánh Đại
b8608d0976 Update description for @ncdai (#10591) 2026-05-09 16:50:22 +04:00
shadcn
fc1ca40af4 fix(v4): revert next dependency bump (#10584) 2026-05-08 10:28:15 +04:00
1071 changed files with 76918 additions and 15463 deletions

View File

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

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

37
.github/version-script-prerelease.js vendored Normal file
View File

@@ -0,0 +1,37 @@
import fs from "fs"
const pkgJsonPath = "packages/shadcn/package.json"
const channel = process.argv[2]
const headSha = process.argv[3]
if (!["beta", "rc"].includes(channel)) {
console.error(
`Expected prerelease channel to be "beta" or "rc", got "${channel}".`
)
process.exit(1)
}
if (!headSha) {
console.error("Expected pull request head SHA.")
process.exit(1)
}
try {
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"))
const shortSha = headSha.trim().slice(0, 7)
const baseVersion = channel === "beta" ? "0.0.0" : pkg.version
if (channel === "rc" && baseVersion.includes("-")) {
console.error(
`Expected a stable planned version for rc, got "${baseVersion}".`
)
process.exit(1)
}
pkg.version = `${baseVersion}-${channel}.${shortSha}`
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, "\t") + "\n")
console.log(`Prepared shadcn@${pkg.version}`)
} catch (error) {
console.error(error)
process.exit(1)
}

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

View File

@@ -1,5 +1,5 @@
# Adapted from create-t3-app.
name: Write Beta Release comment
name: Write Prerelease comment
on:
workflow_run:
@@ -32,9 +32,13 @@ jobs:
const match = /^npm-package-shadcn@(.*?)-pr-(\d+)/.exec(artifact.name);
if (match) {
const version = match[1];
const channel = version.includes("-rc.") ? "rc" : "beta";
require("fs").appendFileSync(
process.env.GITHUB_ENV,
`\nBETA_PACKAGE_VERSION=${match[1]}` +
`\nPRERELEASE_PACKAGE_VERSION=${version}` +
`\nPRERELEASE_CHANNEL=${channel}` +
`\nPRERELEASE_LABEL=release: ${channel}` +
`\nWORKFLOW_RUN_PR=${match[2]}` +
`\nWORKFLOW_RUN_ID=${context.payload.workflow_run.id}`
);
@@ -47,20 +51,30 @@ jobs:
with:
number: ${{ env.WORKFLOW_RUN_PR }}
message: |
A new prerelease is available for testing:
A new ${{ env.PRERELEASE_CHANNEL }} prerelease is available for testing:
```sh
pnpm dlx shadcn@${{ env.BETA_PACKAGE_VERSION }}
pnpm dlx shadcn@${{ env.PRERELEASE_PACKAGE_VERSION }}
```
- name: "Remove the autorelease label once published"
View on npm: https://www.npmjs.com/package/shadcn/v/${{ env.PRERELEASE_PACKAGE_VERSION }}
- name: "Remove the prerelease label once published"
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: '${{ env.WORKFLOW_RUN_PR }}',
name: '${{ env.PRERELEASE_LABEL }}',
});
} 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,54 @@ 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"
}.`
);
}
const selected = selectedLabels[0];
const pullRequest = context.payload.pull_request;
if (
selected.channel === "rc" &&
(pullRequest.head.ref !== "changeset-release/main" ||
pullRequest.title !== "chore(release): version packages")
) {
throw new Error(
"The release: rc label can only be used on the Changesets version PR from changeset-release/main."
);
}
core.setOutput("channel", selected.channel);
core.setOutput("label", selected.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
@@ -48,10 +87,7 @@ jobs:
run: pnpm install
- name: Modify package.json version
run: node .github/version-script-beta.js
- name: Publish Beta to NPM
run: pnpm pub:beta
run: node .github/version-script-prerelease.js ${{ steps.prerelease.outputs.channel }} ${{ github.event.pull_request.head.sha }}
- name: get-npm-version
id: package-version
@@ -59,6 +95,23 @@ jobs:
with:
path: packages/shadcn
- name: Check package version on NPM
id: package-exists
run: |
if npm view "shadcn@${{ steps.package-version.outputs.current-version }}" version >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish Prerelease to NPM
if: ${{ steps.package-exists.outputs.exists == 'false' }}
run: pnpm pub:${{ steps.prerelease.outputs.channel }}
- name: Build packaged artifact
if: ${{ steps.package-exists.outputs.exists == 'true' }}
run: pnpm shadcn:build
- name: Upload packaged artifact
uses: actions/upload-artifact@v4
with:
@@ -83,7 +136,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

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

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

1
.gitignore vendored
View File

@@ -43,5 +43,6 @@ tsconfig.tsbuildinfo
.notes
.playwright-mcp
.playwright-cli
shadcn-workspace
.codex-artifacts

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

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

@@ -55,7 +55,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

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

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

@@ -2,7 +2,7 @@
import * as React from "react"
import { PRESETS, type Style, type StyleName } from "@/registry/config"
import { type Style, type StyleName } from "@/registry/config"
import { LockButton } from "@/app/(app)/create/components/lock-button"
import {
Picker,
@@ -53,24 +53,7 @@ export function StylePicker({
<PickerRadioGroup
value={currentStyle?.name}
onValueChange={(value) => {
const styleName = value as StyleName
const preset = PRESETS.find(
(p) => p.base === params.base && p.style === styleName
)
setParams({
style: styleName,
...(preset && {
baseColor: preset.baseColor,
theme: preset.theme,
chartColor: preset.chartColor,
iconLibrary: preset.iconLibrary,
font: preset.font,
fontHeading: preset.fontHeading,
menuAccent: preset.menuAccent,
menuColor: preset.menuColor,
radius: preset.radius,
}),
})
setParams({ style: value as StyleName })
}}
>
<PickerGroup>

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

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

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,8 @@ 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"
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

@@ -3,14 +3,6 @@
@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);
@custom-variant style-vega (&:where(.style-vega *));
@custom-variant style-nova (&:where(.style-nova *));
@custom-variant style-lyra (&:where(.style-lyra *));
@@ -18,6 +10,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 +73,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 +97,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 +110,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 +204,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 +212,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 +229,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 +284,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 +354,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,172 @@
--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);
}
}

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

@@ -6,8 +6,8 @@ import { Badge } from "@/registry/new-york-v4/ui/badge"
export function Announcement() {
return (
<Badge asChild variant="secondary" className="bg-muted">
<Link href="/docs/changelog">
New preset commands <ArrowRightIcon />
<Link href="/docs/registry/github">
Introducing GitHub Registries <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

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

@@ -79,8 +79,7 @@ export function DocsSidebar({
>
<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">
<SidebarContent className="no-scrollbar w-(--sidebar-menu-width) overflow-x-hidden px-2.5">
<SidebarGroup className="pt-6">
<SidebarGroupLabel className="font-medium text-muted-foreground">
Sections

View File

@@ -118,9 +118,6 @@ export function MobileNav({
Menu
</div>
<div className="flex flex-col gap-3">
<MobileLink href="/" onOpenChange={setOpen}>
Home
</MobileLink>
{items.map((item, index) => (
<MobileLink key={index} href={item.href} onOpenChange={setOpen}>
{item.label}

View File

@@ -8,11 +8,9 @@ import { siteConfig } from "@/lib/config"
import { source } from "@/lib/source"
import { CommandMenu } from "@/components/command-menu"
import { GitHubLink } from "@/components/github-link"
import { Icons } from "@/components/icons"
import { MainNav } from "@/components/main-nav"
import { MobileNav } from "@/components/mobile-nav"
import { ModeSwitcher } from "@/components/mode-switcher"
import { SiteConfig } from "@/components/site-config"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Button } from "@/styles/radix-nova/ui/button"
import { ProjectForm } from "@/app/(app)/create/components/project-form"
@@ -31,17 +29,6 @@ export function SiteHeader() {
items={siteConfig.navItems}
className="flex lg:hidden"
/>
<Button
asChild
variant="ghost"
size="icon"
className="hidden size-8 lg:flex"
>
<Link href="/">
<Icons.logo className="size-5" />
<span className="sr-only">{siteConfig.name}</span>
</Link>
</Button>
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
<div className="ml-auto flex items-center gap-2 md:flex-1 md:justify-end">
<div className="hidden w-full flex-1 md:flex md:w-auto md:flex-none">
@@ -56,11 +43,6 @@ export function SiteHeader() {
className="ml-2 hidden lg:block"
/>
<GitHubLink />
<Separator
orientation="vertical"
className="hidden group-has-data-[slot=designer]/layout:hidden 3xl:flex"
/>
<SiteConfig className="hidden 3xl:flex 3xl:group-has-data-[slot=designer]/layout:hidden" />
<Separator orientation="vertical" />
<ModeSwitcher />
<div className="hidden items-center gap-2 group-has-data-[slot=designer]/layout:md:flex">

View File

@@ -3,6 +3,8 @@ title: shadcn
description: Use the shadcn CLI to add components to your project.
---
import { TriangleAlertIcon } from "lucide-react"
## init
Use the `init` command to initialize configuration and dependencies for an existing project, or create a new project with `--name`.
@@ -503,3 +505,87 @@ npx shadcn@latest migrate radix "src/components/ui/**"
If no path is provided, the migration will transform all files in your `ui` directory (from `components.json`).
Once complete, you can remove any unused `@radix-ui/react-*` packages from your `package.json`.
---
## eject
When you run `init`, shadcn adds `@import "shadcn/tailwind.css"` to your global CSS file. This import provides shared Tailwind v4 utilities such as custom variants (`data-open:`, `data-closed:`, etc.) and accordion animations.
Use the `eject` command to inline `shadcn/tailwind.css` into your global CSS file and remove the `shadcn` dependency from your project.
<Callout icon={<TriangleAlertIcon />}>
**Note: This action is irreversible.** After ejecting, future shadcn CLI
updates to `shadcn/tailwind.css` will not apply automatically.
</Callout>
```bash
npx shadcn@latest eject
```
**Before**
```css
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
```
**After**
```css
@import "tailwindcss";
@import "tw-animate-css";
/* ejected from shadcn@4.8.3 */
@theme inline {
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(
--radix-accordion-content-height,
var(--accordion-panel-height, auto)
);
}
}
}
@custom-variant data-open {
&:where([data-state="open"]),
&:where([data-open]:not([data-open="false"])) {
@slot;
}
}
@utility no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
```
**Monorepo**
In a monorepo, run the command from the workspace that contains your `components.json` and global CSS file:
```bash
npx shadcn@latest eject -c packages/ui
```
**Options**
```bash
Usage: shadcn eject [options]
inline shadcn/tailwind.css and remove the shadcn dependency
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
-y, --yes skip confirmation prompt. (default: false)
-s, --silent mute output. (default: false)
-h, --help display help for command
```

View File

@@ -11,7 +11,6 @@
"[CLI](/docs/cli)",
"monorepo",
"skills",
"v0",
"javascript",
"blocks",
"figma",

View File

@@ -0,0 +1,120 @@
---
title: May 2026 - Registry Include and Validate
description: Organize and validate source registries.
date: 2026-05-20
---
This release adds two updates for registry authors:
- `include` for composing large source registries from multiple `registry.json`
files.
- `shadcn registry validate` for checking source registries before publishing.
This makes it easier to maintain source and dynamic registries without keeping
one large `registry.json` file by hand.
Registry authors can now organize a large source registry across multiple
`registry.json` files and compose them with `shadcn build`.
```txt /registry.json/
registry.json
components
└── ui
├── button.tsx
├── input.tsx
└── registry.json
hooks
├── registry.json
├── use-media-query.ts
└── use-toggle.ts
```
{/* prettier-ignore */}
```json title="registry.json" showLineNumbers {6-7}
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "https://acme.com",
"include": [
"components/ui/registry.json",
"hooks/registry.json"
]
}
```
Included `registry.json` files are valid registry files for composition and may
omit `name` and `homepage`. Only the root `registry.json` must define the
registry metadata.
```json title="components/ui/registry.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"items": [
{
"name": "button",
"type": "registry:ui",
"files": [
{
"path": "button.tsx",
"type": "registry:ui"
}
]
}
]
}
```
## Build output
`shadcn build` resolves included registries and writes a flattened
`registry.json` without `include`. Item file paths are preserved from the root
registry, so a file declared in `components/ui/registry.json` is written as
`components/ui/button.tsx` in the built registry item.
## Validate your registry
You can now validate a source registry before publishing or serving it.
```bash
npx shadcn registry validate
```
Validation runs against the source registry files directly. You do not need to
run `shadcn build` first.
The command checks the root `registry.json`, included registry files, item
schema errors, duplicate item names, include rules, and local item file paths.
Validation reports all actionable errors it can find in one run.
## Registry loaders
The `shadcn/registry` package also exports `loadRegistry` and
`loadRegistryItem` for dynamic registry routes.
```ts title="app/r/registry.json/route.ts" showLineNumbers
import { loadRegistry } from "shadcn/registry"
export async function GET() {
const registry = await loadRegistry()
return Response.json(registry)
}
```
```ts title="app/r/[name].json/route.ts" showLineNumbers
import { loadRegistryItem } from "shadcn/registry"
export async function GET(
_: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params
const item = await loadRegistryItem(name)
return Response.json(item)
}
```
See the [registry.json documentation](/docs/registry/registry-json#include) and
[getting started guide](/docs/registry/getting-started#structure-your-registry)
for more details.

View File

@@ -0,0 +1,45 @@
---
title: May 2026 - Introducing Rhea
description: A more compact Luma. Smaller spacing. Denser surfaces. Built for focused product interfaces.
date: 2026-05-26
---
Introducing Rhea, a new shadcn/ui style. A more compact Luma. Smaller spacing. Denser surfaces. Built for focused product interfaces.
<a href="/create?preset=b27GcrRo">
<Image
src="/images/rhea-light.png"
width="3840"
height="2160"
alt="Rhea style preview"
className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
/>
<Image
src="/images/rhea-dark.png"
width="3840"
height="2160"
alt="Rhea style preview"
className="mt-6 hidden w-full overflow-hidden rounded-lg border shadow-sm dark:block"
/>
<span className="sr-only">Try Rhea in shadcn/create</span>
</a>
Rhea started from a simple request we've heard a lot: Luma, but more compact. We looked at how people were using the new styles and what they were asking for, and the pattern was clear. A lot of teams wanted the softness and shape of Luma with tighter spacing, smaller controls, and more information density.
Rhea keeps the same rounded foundation, but makes it more compact for product interfaces where space matters. Buttons, inputs, menus, cards, and lists all sit a little tighter so the UI can carry more without feeling crowded.
## Why a new style?
We considered making this a spacing tweak for Luma, but `--spacing` is a multiplier. Changing it would change what familiar utilities mean across your app. `p-2`, `w-4`, and `m-16` would no longer mean the same size.
That tradeoff felt wrong. Compactness should not force you to relearn Tailwind's spacing scale or wonder whether a utility means something different in one style than another.
So Rhea is a new style instead. It lets us adjust component sizes, gaps, and density directly while keeping the underlying utility scale predictable.
Available now in [shadcn/create](/create) for both Radix and Base UI.
<Button asChild size="sm">
<Link href="/create?preset=b27GcrRo" className="mt-6 no-underline!">
Try Rhea
</Link>
</Button>

View File

@@ -0,0 +1,61 @@
---
title: May 2026 - shadcn eject
description: Inline shadcn/tailwind.css and remove the shadcn dependency.
date: 2026-05-31
---
When we added support for both Radix and Base UI, we needed a place for shared Tailwind utilities that both libraries depend on, e.g. custom variants like `data-open:` and `data-closed:` and utilities like `no-scrollbar`.
We also ran into a few bugs while working on RTL support that were easier to fix in one shared place rather than duplicating across every component.
So we created `shadcn/tailwind.css`. When you run `init`, it adds `@import "shadcn/tailwind.css"` to your global CSS file. It works just like other CSS imports such as `tw-animate-css`: a small dependency that is tree-shaken in production and resolved at build time.
If you prefer not to depend on the `shadcn` package for that CSS, we've added the `shadcn eject` command. It inlines `shadcn/tailwind.css` into your global CSS file and removes the `shadcn` dependency from your project.
```bash
npx shadcn@latest eject
```
**Before**
```css
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
```
**After**
```css
@import "tailwindcss";
@import "tw-animate-css";
/* ejected from shadcn@4.8.3 */
@theme inline {
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(
--radix-accordion-content-height,
var(--accordion-panel-height, auto)
);
}
}
}
@custom-variant data-open {
&:where([data-state="open"]),
&:where([data-open]:not([data-open="false"])) {
@slot;
}
}
```
In a monorepo, run the command from the workspace that contains your `components.json` and global CSS file:
```bash
npx shadcn@latest eject -c packages/ui
```
See the [CLI documentation](/docs/cli#eject) for more details.

View File

@@ -0,0 +1,98 @@
---
title: June 2026 - GitHub Registries
description: Turn any public GitHub repository into a shadcn registry.
date: 2026-06-01
---
**You can now turn any public GitHub repository into a registry.**
Add a `registry.json` file at the root of the repository, define the items you
want to distribute, and users can install them directly from GitHub with the
`shadcn` CLI.
```bash
npx shadcn@latest add <username>/<repo>/<item>
```
For example, to install the `project-conventions` item from the `acme/toolkit` repository:
```bash
npx shadcn@latest add acme/toolkit/project-conventions
```
GitHub registries are source registries. You do not need to run `shadcn build`,
publish generated item JSON files or set up a registry server. The CLI reads the
root `registry.json`, resolves `include` entries, finds the requested item and
installs the files declared by that item.
## Distribute anything
Registry items are not limited to components. A GitHub registry can distribute
components, hooks, utilities, design tokens, feature kits, project conventions,
agent instructions, testing setup, CI workflows, release workflows, templates,
codemods, migration kits and other project files.
For example, a repository can expose a `project-conventions` item that installs
shared docs, editor settings and agent instructions:
```json title="registry.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme-toolkit",
"homepage": "https://github.com/acme/toolkit",
"items": [
{
"name": "project-conventions",
"type": "registry:item",
"files": [
{
"path": "AGENTS.md",
"type": "registry:file",
"target": "~/AGENTS.md"
},
{
"path": ".editorconfig",
"type": "registry:file",
"target": "~/.editorconfig"
},
{
"path": "docs/conventions.md",
"type": "registry:file",
"target": "~/docs/conventions.md"
}
]
}
]
}
```
## Commands
GitHub registry addresses work with the same commands as other registry
addresses.
List items from a GitHub registry:
```bash
npx shadcn@latest list acme/toolkit
```
Search items:
```bash
npx shadcn@latest search acme/toolkit --query conventions
```
View an item:
```bash
npx shadcn@latest view acme/toolkit/project-conventions
```
Install an item:
```bash
npx shadcn@latest add acme/toolkit/project-conventions
```
See the [GitHub Registries](/docs/registry/github) docs for the full guide.

View File

@@ -103,6 +103,24 @@ Use the `size="sm"` prop to set the size of the card to small. The small size va
previewClassName="h-96"
/>
### Spacing
In addition to the `size` prop, you can use the `--card-spacing` CSS variable to control the spacing between sections and the inset of card parts.
<ComponentPreview
styleName="base-nova"
name="card-spacing"
previewClassName="h-[34rem]"
/>
Use negative margins with `-mx-(--card-spacing)` to make content go edge to edge while keeping it aligned with the card inset. When the edge-to-edge content sits above a footer, use `-mb-(--card-spacing)` on `CardContent` to remove the section gap.
<ComponentPreview
styleName="base-nova"
name="card-edge-to-edge"
previewClassName="h-[24rem]"
/>
### Image
Add an image before the card header to create a card with an image.
@@ -182,3 +200,70 @@ The `CardFooter` component is used for actions and secondary content at the bott
| Prop | Type | Default |
| ----------- | -------- | ------- |
| `className` | `string` | - |
## Changelog
### Spacing Variable
If you're upgrading from a previous version of the `Card` component, you'll need to apply the following updates to use the `--card-spacing` variable:
<Steps>
<Step>Update the Card root spacing classes.</Step>
Replace the hard-coded gap and vertical padding with `--card-spacing`, and set the default and small size values on the root:
```diff
className={cn(
- "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
+ "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
```
<Step>Update CardHeader spacing classes.</Step>
Replace the horizontal padding and border spacing with the shared variable:
```diff
className={cn(
- "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
className
)}
```
<Step>Update CardContent and CardFooter spacing classes.</Step>
Use `--card-spacing` for the content inset and footer padding:
```diff
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
- className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
+ className={cn("px-(--card-spacing)", className)}
{...props}
/>
)
}
```
```diff
className={cn(
- "flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
+ "flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
className
)}
```
</Steps>
After applying these changes, you can customize card spacing by setting `--card-spacing` on the `Card` with an arbitrary property class:
```tsx
function Example() {
return <Card className="[--card-spacing:--spacing(6)]">...</Card>
}
```

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