mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 18:01:34 +00:00
Compare commits
156 Commits
shadcn@4.3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea9d371a2d | ||
|
|
d15f17d717 | ||
|
|
46ca8c5d4b | ||
|
|
a5eb279650 | ||
|
|
1994caba0b | ||
|
|
1450bea8d6 | ||
|
|
ced2a5beb5 | ||
|
|
10f1717a3e | ||
|
|
deba036d50 | ||
|
|
5bd81beebf | ||
|
|
3f2ff18157 | ||
|
|
05eb2b968b | ||
|
|
a721cc08e5 | ||
|
|
8da4592308 | ||
|
|
f47d48f316 | ||
|
|
e6d9d6023b | ||
|
|
7dfd933102 | ||
|
|
9c6a5ee1b1 | ||
|
|
c87897b2a5 | ||
|
|
c61197f627 | ||
|
|
a1fb619cef | ||
|
|
d84c4a8ca5 | ||
|
|
cd54e0927f | ||
|
|
adac7cae1f | ||
|
|
7c63c46736 | ||
|
|
916c012132 | ||
|
|
460ad60d84 | ||
|
|
8e2d2d1439 | ||
|
|
67cef8fcb9 | ||
|
|
4ff43ba694 | ||
|
|
efdec3ca45 | ||
|
|
5c849297d0 | ||
|
|
2baa86081d | ||
|
|
980f288149 | ||
|
|
07900769d9 | ||
|
|
360e8a19c3 | ||
|
|
e2fa0101e3 | ||
|
|
55ea86f252 | ||
|
|
f584f05489 | ||
|
|
a06ba18dcc | ||
|
|
f3e16e7db7 | ||
|
|
64afddefd9 | ||
|
|
c873713992 | ||
|
|
3751fdfa4c | ||
|
|
c824d6b78d | ||
|
|
df1752dfe0 | ||
|
|
e826e543f2 | ||
|
|
f7eecafb45 | ||
|
|
6e6cf9ee96 | ||
|
|
5b628e23e3 | ||
|
|
4a4dc8eb0f | ||
|
|
a33becad35 | ||
|
|
d60e8b6ce3 | ||
|
|
072c27fcd5 | ||
|
|
194dcc4571 | ||
|
|
51e3cfaf32 | ||
|
|
c8ab3801ec | ||
|
|
731e6dd8a2 | ||
|
|
d7066f4a2d | ||
|
|
5274de83d6 | ||
|
|
7e4dac7f31 | ||
|
|
28122dba18 | ||
|
|
93cde61946 | ||
|
|
c2dc06a99c | ||
|
|
c9930b7fda | ||
|
|
d1149454a8 | ||
|
|
36139f6200 | ||
|
|
15ac1be92b | ||
|
|
8ca30ed32c | ||
|
|
e2605bc7c2 | ||
|
|
b8608d0976 | ||
|
|
fc1ca40af4 | ||
|
|
f454f6e4d1 | ||
|
|
8cc7073aec | ||
|
|
031387a471 | ||
|
|
dd3567c39d | ||
|
|
ad2b8891a5 | ||
|
|
f6e18c65cf | ||
|
|
1c4a53a37a | ||
|
|
bc2db187aa | ||
|
|
92b4927a80 | ||
|
|
3cbabe012e | ||
|
|
1137b24a97 | ||
|
|
bb251e2ab6 | ||
|
|
28b3e5f360 | ||
|
|
309d95017f | ||
|
|
eb42ae25fd | ||
|
|
3977fb9ace | ||
|
|
7865621397 | ||
|
|
b07070cd07 | ||
|
|
ad68a44717 | ||
|
|
56161142f1 | ||
|
|
c2e1a5793f | ||
|
|
ea6086cbcc | ||
|
|
68a69d81f7 | ||
|
|
55fd4dc71b | ||
|
|
6dea65ebcb | ||
|
|
ba10089b8d | ||
|
|
8a814f926b | ||
|
|
c236d0c009 | ||
|
|
fd0e0c369b | ||
|
|
07d14abde1 | ||
|
|
8dd51c49f8 | ||
|
|
c20e0cc596 | ||
|
|
0126502236 | ||
|
|
94074e4bb2 | ||
|
|
eb6e783fb3 | ||
|
|
f785bfab44 | ||
|
|
cc20c8a794 | ||
|
|
05948dce8e | ||
|
|
5d23df4e35 | ||
|
|
abbdd32953 | ||
|
|
3f14ffa632 | ||
|
|
5927f6de80 | ||
|
|
39eb34104b | ||
|
|
7cbc7e8d53 | ||
|
|
d0ac558ce2 | ||
|
|
bc0c46a93c | ||
|
|
a64575d8a4 | ||
|
|
5d0cd7819b | ||
|
|
13478b26b6 | ||
|
|
aee8a71679 | ||
|
|
4507f1c794 | ||
|
|
81cd2266aa | ||
|
|
cf756b1b55 | ||
|
|
5e61f9c4a4 | ||
|
|
c4def9305f | ||
|
|
e456fed9d3 | ||
|
|
b95cd29508 | ||
|
|
11cbc32840 | ||
|
|
01539fb4d7 | ||
|
|
e47ee89dcf | ||
|
|
2f5c32c0b1 | ||
|
|
fbfe9f34bb | ||
|
|
d55e059fda | ||
|
|
9c572ab778 | ||
|
|
91403eeb63 | ||
|
|
3411d53856 | ||
|
|
efa2b38d07 | ||
|
|
d00605c5fb | ||
|
|
4bdeea4c63 | ||
|
|
f632f5d798 | ||
|
|
7d6d489f83 | ||
|
|
e8b1be1f22 | ||
|
|
d987955893 | ||
|
|
7b5435ac0b | ||
|
|
f289497e35 | ||
|
|
0d266984e6 | ||
|
|
cf92d4f8f2 | ||
|
|
b7cfc364ac | ||
|
|
de385d04fc | ||
|
|
eeb5d22fe5 | ||
|
|
a757e80242 | ||
|
|
945298ed2d | ||
|
|
f9b216af77 | ||
|
|
6525227036 |
41
.cursor-plugin/plugin.json
Normal file
41
.cursor-plugin/plugin.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"displayName": "shadcn/ui",
|
||||
"version": "1.0.0",
|
||||
"description": "UI component and design system framework. Search registries, install components as source code, and audit your project.",
|
||||
"author": {
|
||||
"name": "shadcn"
|
||||
},
|
||||
"homepage": "https://ui.shadcn.com",
|
||||
"repository": "https://github.com/shadcn-ui/ui",
|
||||
"license": "MIT",
|
||||
"logo": "skills/shadcn/assets/shadcn.png",
|
||||
"keywords": [
|
||||
"shadcn",
|
||||
"shadcn-ui",
|
||||
"ui",
|
||||
"components",
|
||||
"tailwind",
|
||||
"tailwindcss",
|
||||
"radix",
|
||||
"react",
|
||||
"design-system",
|
||||
"registry",
|
||||
"mcp"
|
||||
],
|
||||
"category": "developer-tools",
|
||||
"tags": [
|
||||
"ui",
|
||||
"components",
|
||||
"design-system",
|
||||
"react",
|
||||
"tailwind"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": ["shadcn@latest", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
21
.github/version-script-beta.js
vendored
21
.github/version-script-beta.js
vendored
@@ -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
37
.github/version-script-prerelease.js
vendored
Normal 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)
|
||||
}
|
||||
6
.github/workflows/code-check.yml
vendored
6
.github/workflows/code-check.yml
vendored
@@ -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
|
||||
|
||||
45
.github/workflows/prerelease-comment.yml
vendored
45
.github/workflows/prerelease-comment.yml
vendored
@@ -1,9 +1,9 @@
|
||||
# Adapted from create-t3-app.
|
||||
name: Write Beta Release comment
|
||||
name: Write Prerelease comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Release - Beta"]
|
||||
workflows: ["Release"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
@@ -11,12 +11,13 @@ jobs:
|
||||
comment:
|
||||
if: |
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
name: Write comment to the PR
|
||||
steps:
|
||||
- name: "Comment on PR"
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -31,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}`
|
||||
);
|
||||
@@ -46,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"
|
||||
uses: actions/github-script@v6
|
||||
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.");
|
||||
}
|
||||
|
||||
64
.github/workflows/prerelease.yml
vendored
64
.github/workflows/prerelease.yml
vendored
@@ -1,64 +0,0 @@
|
||||
# Adapted from create-t3-app.
|
||||
|
||||
name: Release - Beta
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prerelease:
|
||||
if: |
|
||||
github.repository_owner == 'shadcn-ui' &&
|
||||
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
|
||||
name: Build & Publish a beta release to NPM
|
||||
runs-on: ubuntu-latest
|
||||
environment: Preview
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-beta.js
|
||||
|
||||
- name: Publish Beta to NPM
|
||||
run: pnpm pub:beta
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
path: packages/shadcn
|
||||
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/shadcn/dist/index.js
|
||||
139
.github/workflows/release.yml
vendored
139
.github/workflows/release.yml
vendored
@@ -2,31 +2,141 @@
|
||||
|
||||
name: Release
|
||||
|
||||
run-name: ${{ github.event_name == 'pull_request' && format('Release Prerelease - PR {0}', github.event.number) || 'Release Stable' }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'shadcn-ui' }}
|
||||
name: Create a PR for release workflow
|
||||
prerelease:
|
||||
if: "${{ github.event_name == 'pull_request' && github.repository_owner == 'shadcn-ui' && (contains(github.event.pull_request.labels.*.name, 'release: beta') || contains(github.event.pull_request.labels.*.name, 'release: rc')) }}"
|
||||
name: Publish Prerelease to NPM
|
||||
runs-on: ubuntu-latest
|
||||
environment: Preview
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Select prerelease channel
|
||||
id: prerelease
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prereleaseLabels = [
|
||||
{ name: "release: beta", channel: "beta" },
|
||||
{ name: "release: rc", channel: "rc" },
|
||||
];
|
||||
const labels = context.payload.pull_request.labels.map((label) => label.name);
|
||||
const selectedLabels = prereleaseLabels.filter((label) =>
|
||||
labels.includes(label.name)
|
||||
);
|
||||
|
||||
if (selectedLabels.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected exactly one prerelease label, found: ${
|
||||
selectedLabels.map((label) => label.name).join(", ") || "none"
|
||||
}.`
|
||||
);
|
||||
}
|
||||
|
||||
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: 10.33.4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-prerelease.js ${{ steps.prerelease.outputs.channel }} ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
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:
|
||||
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/shadcn/dist/index.js
|
||||
|
||||
release:
|
||||
if: ${{ github.event_name == 'push' && github.repository_owner == 'shadcn-ui' }}
|
||||
name: Create Version PR or Publish Stable Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
@@ -47,10 +157,19 @@ jobs:
|
||||
- name: Build the package
|
||||
run: pnpm shadcn:build
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
git_tag_gpgsign: true
|
||||
|
||||
- name: Create Version PR or Publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
setupGitUser: false
|
||||
commit: "chore(release): version packages"
|
||||
title: "chore(release): version packages"
|
||||
version: node .github/changeset-version.js
|
||||
|
||||
75
.github/workflows/signed-commits.yml
vendored
Normal file
75
.github/workflows/signed-commits.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Signed commits
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
signed-commits:
|
||||
if: github.repository_owner == 'shadcn-ui'
|
||||
runs-on: ubuntu-latest
|
||||
name: Signed commits
|
||||
steps:
|
||||
- name: Check PR commits
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const body = "Can you sign the commits please? See https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits. Thank you."
|
||||
|
||||
const { owner, repo } = context.repo
|
||||
const pullNumber = context.payload.pull_request.number
|
||||
|
||||
const commits = await github.paginate(github.rest.pulls.listCommits, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullNumber,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
const unsignedCommits = commits.filter((commit) => {
|
||||
return commit.commit.verification?.reason === "unsigned"
|
||||
})
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
const existingComments = comments.filter((comment) => {
|
||||
return comment.user.type === "Bot" && comment.body.trim() === body
|
||||
})
|
||||
|
||||
if (unsignedCommits.length > 0) {
|
||||
core.info(`Found ${unsignedCommits.length} unsigned commits.`)
|
||||
|
||||
if (existingComments.length === 0) {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pullNumber,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
core.info("All commits are signed.")
|
||||
|
||||
for (const comment of existingComments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id,
|
||||
})
|
||||
}
|
||||
314
.github/workflows/templates.yml
vendored
Normal file
314
.github/workflows/templates.yml
vendored
Normal 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"
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
59
.github/workflows/validate-registries.yml
vendored
59
.github/workflows/validate-registries.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -43,5 +43,6 @@ tsconfig.tsbuildinfo
|
||||
|
||||
.notes
|
||||
.playwright-mcp
|
||||
.playwright-cli
|
||||
shadcn-workspace
|
||||
.codex-artifacts
|
||||
|
||||
@@ -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
|
||||
|
||||
94
apps/v4/app/(app)/(root)/cards/account-access.tsx
Normal file
94
apps/v4/app/(app)/(root)/cards/account-access.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
46
apps/v4/app/(app)/(root)/cards/analytics-card.tsx
Normal file
46
apps/v4/app/(app)/(root)/cards/analytics-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
75
apps/v4/app/(app)/(root)/cards/claimable-balance.tsx
Normal file
75
apps/v4/app/(app)/(root)/cards/claimable-balance.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
88
apps/v4/app/(app)/(root)/cards/contribution-history.tsx
Normal file
88
apps/v4/app/(app)/(root)/cards/contribution-history.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
116
apps/v4/app/(app)/(root)/cards/dividend-income.tsx
Normal file
116
apps/v4/app/(app)/(root)/cards/dividend-income.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
37
apps/v4/app/(app)/(root)/cards/empty-distribute-track.tsx
Normal file
37
apps/v4/app/(app)/(root)/cards/empty-distribute-track.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
117
apps/v4/app/(app)/(root)/cards/index.tsx
Normal file
117
apps/v4/app/(app)/(root)/cards/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
52
apps/v4/app/(app)/(root)/cards/new-milestone.tsx
Normal file
52
apps/v4/app/(app)/(root)/cards/new-milestone.tsx
Normal 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'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>
|
||||
)
|
||||
}
|
||||
76
apps/v4/app/(app)/(root)/cards/notification-settings.tsx
Normal file
76
apps/v4/app/(app)/(root)/cards/notification-settings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
139
apps/v4/app/(app)/(root)/cards/payments.tsx
Normal file
139
apps/v4/app/(app)/(root)/cards/payments.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
112
apps/v4/app/(app)/(root)/cards/payout-threshold.tsx
Normal file
112
apps/v4/app/(app)/(root)/cards/payout-threshold.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
67
apps/v4/app/(app)/(root)/cards/power-usage.tsx
Normal file
67
apps/v4/app/(app)/(root)/cards/power-usage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
64
apps/v4/app/(app)/(root)/cards/qr-connect.tsx
Normal file
64
apps/v4/app/(app)/(root)/cards/qr-connect.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
81
apps/v4/app/(app)/(root)/cards/savings-targets.tsx
Normal file
81
apps/v4/app/(app)/(root)/cards/savings-targets.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
218
apps/v4/app/(app)/(root)/cards/sidebar-nav.tsx
Normal file
218
apps/v4/app/(app)/(root)/cards/sidebar-nav.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
35
apps/v4/app/(app)/(root)/cards/skeleton/account-access.tsx
Normal file
35
apps/v4/app/(app)/(root)/cards/skeleton/account-access.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
17
apps/v4/app/(app)/(root)/cards/skeleton/analytics-card.tsx
Normal file
17
apps/v4/app/(app)/(root)/cards/skeleton/analytics-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
49
apps/v4/app/(app)/(root)/cards/skeleton/dividend-income.tsx
Normal file
49
apps/v4/app/(app)/(root)/cards/skeleton/dividend-income.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
56
apps/v4/app/(app)/(root)/cards/skeleton/index.tsx
Normal file
56
apps/v4/app/(app)/(root)/cards/skeleton/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
38
apps/v4/app/(app)/(root)/cards/skeleton/new-milestone.tsx
Normal file
38
apps/v4/app/(app)/(root)/cards/skeleton/new-milestone.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
37
apps/v4/app/(app)/(root)/cards/skeleton/payments.tsx
Normal file
37
apps/v4/app/(app)/(root)/cards/skeleton/payments.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
43
apps/v4/app/(app)/(root)/cards/skeleton/payout-threshold.tsx
Normal file
43
apps/v4/app/(app)/(root)/cards/skeleton/payout-threshold.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
54
apps/v4/app/(app)/(root)/cards/skeleton/power-usage.tsx
Normal file
54
apps/v4/app/(app)/(root)/cards/skeleton/power-usage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
17
apps/v4/app/(app)/(root)/cards/skeleton/qr-connect.tsx
Normal file
17
apps/v4/app/(app)/(root)/cards/skeleton/qr-connect.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
44
apps/v4/app/(app)/(root)/cards/skeleton/savings-targets.tsx
Normal file
44
apps/v4/app/(app)/(root)/cards/skeleton/savings-targets.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
39
apps/v4/app/(app)/(root)/cards/skeleton/sidebar-nav.tsx
Normal file
39
apps/v4/app/(app)/(root)/cards/skeleton/sidebar-nav.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
55
apps/v4/app/(app)/(root)/cards/skeleton/transfer-funds.tsx
Normal file
55
apps/v4/app/(app)/(root)/cards/skeleton/transfer-funds.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
45
apps/v4/app/(app)/(root)/cards/skeleton/ui-elements.tsx
Normal file
45
apps/v4/app/(app)/(root)/cards/skeleton/ui-elements.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
139
apps/v4/app/(app)/(root)/cards/transfer-funds.tsx
Normal file
139
apps/v4/app/(app)/(root)/cards/transfer-funds.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
176
apps/v4/app/(app)/(root)/cards/ui-elements.tsx
Normal file
176
apps/v4/app/(app)/(root)/cards/ui-elements.tsx
Normal 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'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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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're looking for doesn'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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as React from "react"
|
||||
import {
|
||||
buildRegistryTheme,
|
||||
DEFAULT_CONFIG,
|
||||
POINTER_CURSOR_SELECTOR,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
import { useIframeMessageListener } from "@/app/(app)/create/hooks/use-iframe-sync"
|
||||
@@ -16,6 +17,12 @@ import {
|
||||
|
||||
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
|
||||
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
|
||||
const POINTER_CURSOR_CSS = `@layer base {
|
||||
${POINTER_CURSOR_SELECTOR} {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type RegistryThemeCssVars = NonNullable<
|
||||
ReturnType<typeof buildRegistryTheme>["cssVars"]
|
||||
@@ -44,14 +51,17 @@ function buildCssRule(selector: string, cssVars?: Record<string, string>) {
|
||||
return `${selector} {\n${declarations}\n}\n`
|
||||
}
|
||||
|
||||
function buildThemeCssText(cssVars: RegistryThemeCssVars) {
|
||||
function buildThemeCssText(cssVars: RegistryThemeCssVars, pointer: boolean) {
|
||||
return [
|
||||
buildCssRule(":root", {
|
||||
...(cssVars.theme ?? {}),
|
||||
...(cssVars.light ?? {}),
|
||||
}),
|
||||
buildCssRule(".dark", cssVars.dark),
|
||||
].join("\n")
|
||||
pointer ? POINTER_CURSOR_CSS : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
export function DesignSystemProvider({
|
||||
@@ -73,6 +83,7 @@ export function DesignSystemProvider({
|
||||
chartColor,
|
||||
menuAccent,
|
||||
menuColor,
|
||||
pointer,
|
||||
radius,
|
||||
} = searchParams
|
||||
const effectiveRadius = style === "lyra" ? "none" : radius
|
||||
@@ -132,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])
|
||||
|
||||
@@ -208,8 +224,8 @@ export function DesignSystemProvider({
|
||||
document.head.appendChild(styleElement)
|
||||
}
|
||||
|
||||
styleElement.textContent = buildThemeCssText(registryTheme.cssVars)
|
||||
}, [registryTheme])
|
||||
styleElement.textContent = buildThemeCssText(registryTheme.cssVars, pointer)
|
||||
}, [registryTheme, pointer])
|
||||
|
||||
// Handle menu color inversion by adding/removing dark class to elements with cn-menu-target.
|
||||
// useLayoutEffect to apply classes synchronously before paint, avoiding flash.
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Copy01Icon, Globe02Icon, Tick02Icon } from "@hugeicons/core-free-icons"
|
||||
import {
|
||||
Copy01Icon,
|
||||
Globe02Icon,
|
||||
HandPointingRight04Icon,
|
||||
Tick02Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { BASES, 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,
|
||||
@@ -36,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,
|
||||
@@ -56,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>) {
|
||||
@@ -63,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(
|
||||
@@ -85,7 +152,8 @@ export function ProjectForm({
|
||||
const templateFlag = ` --template ${framework}`
|
||||
const monorepoFlag = isMonorepo ? " --monorepo" : ""
|
||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
|
||||
const pointerFlag = params.pointer ? " --pointer" : ""
|
||||
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}${pointerFlag}`
|
||||
|
||||
return IS_LOCAL_DEV
|
||||
? {
|
||||
@@ -100,16 +168,96 @@ export function ProjectForm({
|
||||
yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`,
|
||||
bun: `bunx --bun shadcn${SHADCN_VERSION} init${flags}`,
|
||||
}
|
||||
}, [framework, isMonorepo, params.base, params.rtl, presetCode])
|
||||
}, [
|
||||
framework,
|
||||
isMonorepo,
|
||||
params.base,
|
||||
params.pointer,
|
||||
params.rtl,
|
||||
presetCode,
|
||||
])
|
||||
|
||||
const command = commands[packageManager]
|
||||
|
||||
const applyCommands = React.useMemo(() => {
|
||||
const presetFlag = ` --preset ${presetCode}`
|
||||
const onlyFlag =
|
||||
applyMode === "theme"
|
||||
? " --only theme"
|
||||
: applyMode === "font"
|
||||
? " --only font"
|
||||
: ""
|
||||
const flags = `${presetFlag}${onlyFlag}`
|
||||
|
||||
return IS_LOCAL_DEV
|
||||
? {
|
||||
pnpm: `shadcn apply${flags}`,
|
||||
npm: `shadcn apply${flags}`,
|
||||
yarn: `shadcn apply${flags}`,
|
||||
bun: `shadcn apply${flags}`,
|
||||
}
|
||||
: {
|
||||
pnpm: `pnpm dlx shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
npm: `npx shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
yarn: `yarn dlx shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
bun: `bunx --bun shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
}
|
||||
}, [applyMode, presetCode])
|
||||
|
||||
const applyCommand = applyCommands[packageManager]
|
||||
const themeConfig = React.useMemo<DesignSystemConfig>(() => {
|
||||
const isRadiusLocked = params.style === "lyra" || params.style === "sera"
|
||||
|
||||
return {
|
||||
...DEFAULT_CONFIG,
|
||||
base: params.base,
|
||||
style: params.style,
|
||||
baseColor: params.baseColor,
|
||||
theme: params.theme,
|
||||
chartColor: params.chartColor,
|
||||
iconLibrary: params.iconLibrary,
|
||||
font: params.font,
|
||||
fontHeading: params.fontHeading,
|
||||
menuAccent: params.menuAccent,
|
||||
menuColor: params.menuColor,
|
||||
radius: isRadiusLocked ? "none" : params.radius,
|
||||
template: params.template,
|
||||
rtl: params.rtl,
|
||||
pointer: params.pointer,
|
||||
}
|
||||
}, [
|
||||
params.base,
|
||||
params.baseColor,
|
||||
params.chartColor,
|
||||
params.font,
|
||||
params.fontHeading,
|
||||
params.iconLibrary,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
params.pointer,
|
||||
params.radius,
|
||||
params.rtl,
|
||||
params.style,
|
||||
params.template,
|
||||
params.theme,
|
||||
])
|
||||
|
||||
const themeCss = React.useMemo(() => {
|
||||
const theme = buildThemeForPreset(themeConfig)
|
||||
|
||||
if (!theme.cssVars) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatThemeCss(theme.cssVars)
|
||||
}, [themeConfig])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (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> = {
|
||||
@@ -122,142 +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"
|
||||
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>
|
||||
)
|
||||
@@ -322,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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,8 +16,17 @@ export function ShareButton() {
|
||||
|
||||
const shareUrl = React.useMemo(() => {
|
||||
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
|
||||
return `${origin}/create?preset=${presetCode}&item=${params.item}`
|
||||
}, [presetCode, params.item])
|
||||
const searchParams = new URLSearchParams({
|
||||
preset: presetCode,
|
||||
item: params.item,
|
||||
})
|
||||
|
||||
if (params.pointer) {
|
||||
searchParams.set("pointer", "true")
|
||||
}
|
||||
|
||||
return `${origin}/create?${searchParams.toString()}`
|
||||
}, [params.item, params.pointer, presetCode])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -91,6 +91,7 @@ const designSystemSearchParams = {
|
||||
"laravel",
|
||||
] as const).withDefault("next"),
|
||||
rtl: parseAsBoolean.withDefault(false),
|
||||
pointer: parseAsBoolean.withDefault(false),
|
||||
size: parseAsInteger.withDefault(100),
|
||||
custom: parseAsBoolean.withDefault(false),
|
||||
}
|
||||
@@ -126,6 +127,7 @@ const NON_DESIGN_SYSTEM_KEYS = [
|
||||
"preset",
|
||||
"template",
|
||||
"rtl",
|
||||
"pointer",
|
||||
"size",
|
||||
"custom",
|
||||
] as const
|
||||
@@ -224,6 +226,7 @@ function resolvePresetParams(
|
||||
preset: rawParams.preset,
|
||||
template: rawParams.template,
|
||||
rtl: rawParams.rtl,
|
||||
pointer: rawParams.pointer,
|
||||
size: rawParams.size,
|
||||
custom: rawParams.custom,
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button } from "@/styles/radix-nova/ui/button"
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
|
||||
const NUMBER_OF_LATEST_PAGES = 2
|
||||
const NUMBER_OF_LATEST_PAGES = 5
|
||||
|
||||
export function generateMetadata() {
|
||||
return {
|
||||
@@ -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">
|
||||
|
||||
@@ -6,6 +6,8 @@ import { source } from "@/lib/source"
|
||||
import { getActiveStyle, type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
export const dynamicParams = true
|
||||
|
||||
function getStyleFromSlug(slug: string[] | undefined, fallbackStyle: string) {
|
||||
// Detect base from URL: /docs/components/base/... or /docs/components/radix/...
|
||||
@@ -47,5 +49,5 @@ export async function GET(
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.generateParams()
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -3,6 +3,51 @@ import { describe, expect, it } from "vitest"
|
||||
import { parseDesignSystemConfig } from "./parse-config"
|
||||
|
||||
describe("parseDesignSystemConfig", () => {
|
||||
it("defaults pointer to false when omitted", () => {
|
||||
const result = parseDesignSystemConfig(
|
||||
new URLSearchParams(
|
||||
"base=base&style=sera&baseColor=taupe&theme=taupe&iconLibrary=lucide&font=noto-sans&rtl=false&menuAccent=subtle&menuColor=default&radius=default&fontHeading=playfair-display&template=vite&track=1"
|
||||
)
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
expect(result.data.pointer).toBe(false)
|
||||
})
|
||||
|
||||
it("parses pointer=true", () => {
|
||||
const result = parseDesignSystemConfig(
|
||||
new URLSearchParams(
|
||||
"base=base&style=sera&baseColor=taupe&theme=taupe&iconLibrary=lucide&font=noto-sans&rtl=false&pointer=true&menuAccent=subtle&menuColor=default&radius=default&fontHeading=playfair-display&template=vite&track=1"
|
||||
)
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
expect(result.data.pointer).toBe(true)
|
||||
})
|
||||
|
||||
it("defaults missing chartColor from the selected theme", () => {
|
||||
const result = parseDesignSystemConfig(
|
||||
new URLSearchParams(
|
||||
"base=base&style=sera&baseColor=taupe&theme=taupe&iconLibrary=lucide&font=noto-sans&rtl=false&menuAccent=subtle&menuColor=default&radius=default&fontHeading=playfair-display&template=vite&track=1"
|
||||
)
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
expect(result.data.chartColor).toBe("taupe")
|
||||
})
|
||||
|
||||
it("honors explicit fontHeading and chartColor overrides when a preset is present", () => {
|
||||
const result = parseDesignSystemConfig(
|
||||
new URLSearchParams(
|
||||
@@ -18,4 +63,17 @@ describe("parseDesignSystemConfig", () => {
|
||||
expect(result.data.fontHeading).toBe("playfair-display")
|
||||
expect(result.data.chartColor).toBe("emerald")
|
||||
})
|
||||
|
||||
it("keeps pointer outside preset decoding", () => {
|
||||
const result = parseDesignSystemConfig(
|
||||
new URLSearchParams("preset=a0&pointer=true")
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
expect(result.data.pointer).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||
base: searchParams.get("base") ?? "radix",
|
||||
template: searchParams.get("template") ?? undefined,
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
pointer: searchParams.get("pointer") === "true",
|
||||
}
|
||||
} else {
|
||||
configInput = {
|
||||
@@ -39,6 +40,7 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||
radius: searchParams.get("radius"),
|
||||
template: searchParams.get("template") ?? undefined,
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
pointer: searchParams.get("pointer") === "true",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
74
apps/v4/app/(create)/init/route.test.ts
Normal file
74
apps/v4/app/(create)/init/route.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import {
|
||||
buildRegistryBase,
|
||||
DEFAULT_CONFIG,
|
||||
POINTER_CURSOR_SELECTOR,
|
||||
} from "@/registry/config"
|
||||
|
||||
import { GET } from "./route"
|
||||
|
||||
function createRequest(search = "") {
|
||||
const searchParams = new URLSearchParams(
|
||||
Object.entries(DEFAULT_CONFIG).map(([key, value]) => [key, String(value)])
|
||||
)
|
||||
const url = new URL(`http://localhost:4000/init${search}`)
|
||||
|
||||
for (const [key, value] of url.searchParams) {
|
||||
searchParams.set(key, value)
|
||||
}
|
||||
|
||||
return {
|
||||
nextUrl: new URL(`http://localhost:4000/init?${searchParams}`),
|
||||
} as Parameters<typeof GET>[0]
|
||||
}
|
||||
|
||||
describe("GET /init", () => {
|
||||
it("returns the full registry base when only is omitted", async () => {
|
||||
const response = await GET(createRequest())
|
||||
const json = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(json).toEqual(buildRegistryBase(DEFAULT_CONFIG))
|
||||
expect(json.css["@layer base"][POINTER_CURSOR_SELECTOR]).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns pointer cursor css when pointer is enabled", async () => {
|
||||
const response = await GET(createRequest("?pointer=true"))
|
||||
const json = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(json.css["@layer base"][POINTER_CURSOR_SELECTOR]).toEqual({
|
||||
cursor: "pointer",
|
||||
})
|
||||
})
|
||||
|
||||
it("returns a sparse registry base when only is provided", async () => {
|
||||
const response = await GET(createRequest("?only=theme"))
|
||||
const json = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(json.type).toBe("registry:base")
|
||||
expect(json.config).toEqual({
|
||||
menuColor: "default",
|
||||
menuAccent: "subtle",
|
||||
tailwind: {
|
||||
baseColor: "neutral",
|
||||
},
|
||||
})
|
||||
expect(json.cssVars.light).toBeDefined()
|
||||
expect(json.cssVars.light.radius).toBe("0.625rem")
|
||||
expect(json.dependencies).toBeUndefined()
|
||||
expect(json.registryDependencies).toBeUndefined()
|
||||
})
|
||||
|
||||
it("rejects unsupported only values", async () => {
|
||||
const response = await GET(createRequest("?only=icon"))
|
||||
const json = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(json.error).toBe(
|
||||
"Invalid only value. Use one or more of: theme, font"
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,11 @@ import { track } from "@vercel/analytics/server"
|
||||
import { isPresetCode } from "shadcn/preset"
|
||||
import { registryItemSchema } from "shadcn/schema"
|
||||
|
||||
import { buildRegistryBase } from "@/registry/config"
|
||||
import {
|
||||
buildPartialRegistryBase,
|
||||
buildRegistryBase,
|
||||
parseRegistryBaseParts,
|
||||
} from "@/registry/config"
|
||||
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||
|
||||
@@ -16,13 +20,20 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const onlyResult = parseRegistryBaseParts(searchParams.get("only"))
|
||||
if (!onlyResult.success) {
|
||||
return NextResponse.json({ error: onlyResult.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const rawPreset = searchParams.get("preset")
|
||||
const presetCode =
|
||||
rawPreset && isPresetCode(rawPreset)
|
||||
? rawPreset
|
||||
: getPresetCode(result.data)
|
||||
|
||||
const registryBase = buildRegistryBase(result.data)
|
||||
const registryBase = onlyResult.parts
|
||||
? buildPartialRegistryBase(result.data, onlyResult.parts)
|
||||
: buildRegistryBase(result.data)
|
||||
const parseResult = registryItemSchema.safeParse(registryBase)
|
||||
|
||||
if (!parseResult.success) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { notFound } from "next/navigation"
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||
import { BASES, type Base, type BaseName } from "@/registry/config"
|
||||
import { BASES, type Base } from "@/registry/config"
|
||||
import { ActionMenuScript } from "@/app/(app)/create/components/action-menu"
|
||||
import { DesignSystemProvider } from "@/app/(app)/create/components/design-system-provider"
|
||||
import { HistoryScript } from "@/app/(app)/create/components/history-buttons"
|
||||
@@ -13,15 +13,15 @@ import { DarkModeScript } from "@/app/(app)/create/components/mode-switcher"
|
||||
import { OpenPresetScript } from "@/app/(app)/create/components/open-preset"
|
||||
import { PreviewStyle } from "@/app/(app)/create/components/preview-style"
|
||||
import { RandomizeScript } from "@/app/(app)/create/components/random-button"
|
||||
import {
|
||||
getBaseComponent,
|
||||
getBaseItem,
|
||||
getItemsForBase,
|
||||
} from "@/app/(app)/create/lib/api"
|
||||
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 = false
|
||||
export const dynamicParams = true
|
||||
|
||||
const STATIC_PREVIEW_ITEMS = ["preview", "preview-02"] as const
|
||||
|
||||
function PreventScrollOnFocusScript() {
|
||||
return (
|
||||
@@ -96,19 +96,12 @@ export async function generateMetadata({
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const params: Array<{ base: string; name: string }> = []
|
||||
|
||||
for (const base of BASES) {
|
||||
const items = await getItemsForBase(base.name as BaseName)
|
||||
for (const item of items) {
|
||||
params.push({
|
||||
base: base.name,
|
||||
name: item.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
return BASES.flatMap((base) =>
|
||||
STATIC_PREVIEW_ITEMS.map((name) => ({
|
||||
base: base.name,
|
||||
name,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
export default async function BlockPage({
|
||||
|
||||
21
apps/v4/app/(create)/preview/font-variables.tsx
Normal file
21
apps/v4/app/(create)/preview/font-variables.tsx
Normal 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
|
||||
}
|
||||
190
apps/v4/app/(create)/preview/fonts.ts
Normal file
190
apps/v4/app/(create)/preview/fonts.ts
Normal 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
|
||||
)
|
||||
15
apps/v4/app/(create)/preview/layout.tsx
Normal file
15
apps/v4/app/(create)/preview/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
16
apps/v4/app/r/registries.json/route.ts
Normal file
16
apps/v4/app/r/registries.json/route.ts
Normal 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)
|
||||
}
|
||||
10
apps/v4/app/style-registry.css
Normal file
10
apps/v4/app/style-registry.css
Normal 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);
|
||||
@@ -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="/sera">
|
||||
Introducing Sera <ArrowRightIcon />
|
||||
<Link href="/docs/registry/github">
|
||||
Introducing GitHub Registries <ArrowRightIcon />
|
||||
</Link>
|
||||
</Badge>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user