mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-12 10:21:39 +00:00
Compare commits
34 Commits
fix/cn-cla
...
shadcn@4.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
fix failing version derivation test
|
||||
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
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.0.6
|
||||
version: 10.33.4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
275
.github/workflows/templates.yml
vendored
Normal file
275
.github/workflows/templates.yml
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
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 }} ${{ 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]
|
||||
env:
|
||||
NEXT_PUBLIC_APP_URL: http://localhost:4000
|
||||
NEXT_PUBLIC_V0_URL: https://v0.dev
|
||||
REGISTRY_URL: http://localhost:4000/r
|
||||
TEMPLATE_PNPM_VERSION: 10.33.4
|
||||
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: 10.33.4
|
||||
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"
|
||||
|
||||
cd "$project_path"
|
||||
test ! -f pnpm-workspace.yaml
|
||||
test ! -f pnpm-lock.yaml
|
||||
|
||||
EXPECTED_PACKAGE_MANAGER="$package_manager" \
|
||||
CHECK_WORKSPACE_PROTOCOL="$check_workspace_protocol" \
|
||||
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 pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
|
||||
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}`
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
else
|
||||
args+=(--no-monorepo)
|
||||
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"
|
||||
;;
|
||||
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"
|
||||
;;
|
||||
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"
|
||||
;;
|
||||
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
|
||||
|
||||
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,21 +48,20 @@ 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">
|
||||
<div className="container-wrapper flex-1 pb-6 md:px-0">
|
||||
<div className="container overflow-hidden md:px-0 lg:max-w-none">
|
||||
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
|
||||
<Image
|
||||
src="/r/styles/new-york-v4/dashboard-01-light.png"
|
||||
@@ -80,8 +80,8 @@ export default function IndexPage() {
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -143,6 +143,11 @@ export function DesignSystemProvider({
|
||||
React.useEffect(() => {
|
||||
if (style === "lyra" || (style === "sera" && radius !== "none")) {
|
||||
setSearchParams({ radius: "none" })
|
||||
return
|
||||
}
|
||||
|
||||
if (style === "rhea" && radius === "large") {
|
||||
setSearchParams({ radius: "default" })
|
||||
}
|
||||
}, [style, radius, setSearchParams])
|
||||
|
||||
|
||||
@@ -12,20 +12,25 @@ import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { BASES, type BaseName } from "@/registry/config"
|
||||
import {
|
||||
BASES,
|
||||
buildThemeForPreset,
|
||||
DEFAULT_CONFIG,
|
||||
type BaseName,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/styles/base-nova/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
@@ -41,6 +46,10 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/styles/base-nova/ui/tabs"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/styles/base-nova/ui/toggle-group"
|
||||
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
@@ -61,6 +70,54 @@ const SHADCN_VERSION = process.env.NEXT_PUBLIC_RC ? "@rc" : "@latest"
|
||||
const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"] as const
|
||||
type PackageManager = (typeof PACKAGE_MANAGERS)[number]
|
||||
|
||||
const APPLY_MODES = [
|
||||
{
|
||||
value: "full",
|
||||
title: "Full preset",
|
||||
description:
|
||||
"Everything from the preset, including components, theme, and fonts.",
|
||||
flag: null,
|
||||
label: "full preset",
|
||||
},
|
||||
{
|
||||
value: "theme",
|
||||
title: "Theme only",
|
||||
description:
|
||||
"Theme tokens only, like colors, radii, and shadows. Components stay as they are.",
|
||||
flag: "--only theme",
|
||||
label: "--only theme",
|
||||
},
|
||||
{
|
||||
value: "font",
|
||||
title: "Fonts only",
|
||||
description:
|
||||
"Only preset fonts for body and headings. Components stay as they are.",
|
||||
flag: "--only font",
|
||||
label: "--only font",
|
||||
},
|
||||
] as const
|
||||
type ApplyMode = (typeof APPLY_MODES)[number]["value"]
|
||||
type ProjectFormTab = "new-project" | "existing-project" | "theme"
|
||||
type CopyTarget = "command" | "apply" | "theme"
|
||||
type ThemeCssVars = NonNullable<
|
||||
ReturnType<typeof buildThemeForPreset>["cssVars"]
|
||||
>
|
||||
|
||||
function formatCssVarsRule(selector: string, cssVars?: Record<string, string>) {
|
||||
const declarations = Object.entries(cssVars ?? {})
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
return `${selector} {\n${declarations}\n}`
|
||||
}
|
||||
|
||||
function formatThemeCss(cssVars: ThemeCssVars) {
|
||||
return [
|
||||
formatCssVarsRule(":root", cssVars.light),
|
||||
formatCssVarsRule(".dark", cssVars.dark),
|
||||
].join("\n\n")
|
||||
}
|
||||
|
||||
export function ProjectForm({
|
||||
className,
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
@@ -68,7 +125,12 @@ export function ProjectForm({
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const presetCode = usePresetCode()
|
||||
const [config, setConfig] = useConfig()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
const [copiedTarget, setCopiedTarget] = React.useState<CopyTarget | null>(
|
||||
null
|
||||
)
|
||||
const [applyMode, setApplyMode] = React.useState<ApplyMode>("full")
|
||||
const [activeTab, setActiveTab] =
|
||||
React.useState<ProjectFormTab>("new-project")
|
||||
|
||||
const packageManager = (config.packageManager || "pnpm") as PackageManager
|
||||
const framework = React.useMemo(
|
||||
@@ -117,12 +179,85 @@ export function ProjectForm({
|
||||
|
||||
const command = commands[packageManager]
|
||||
|
||||
const applyCommands = React.useMemo(() => {
|
||||
const presetFlag = ` --preset ${presetCode}`
|
||||
const onlyFlag =
|
||||
applyMode === "theme"
|
||||
? " --only theme"
|
||||
: applyMode === "font"
|
||||
? " --only font"
|
||||
: ""
|
||||
const flags = `${presetFlag}${onlyFlag}`
|
||||
|
||||
return IS_LOCAL_DEV
|
||||
? {
|
||||
pnpm: `shadcn apply${flags}`,
|
||||
npm: `shadcn apply${flags}`,
|
||||
yarn: `shadcn apply${flags}`,
|
||||
bun: `shadcn apply${flags}`,
|
||||
}
|
||||
: {
|
||||
pnpm: `pnpm dlx shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
npm: `npx shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
yarn: `yarn dlx shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
bun: `bunx --bun shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
}
|
||||
}, [applyMode, presetCode])
|
||||
|
||||
const applyCommand = applyCommands[packageManager]
|
||||
const themeConfig = React.useMemo<DesignSystemConfig>(() => {
|
||||
const isRadiusLocked = params.style === "lyra" || params.style === "sera"
|
||||
|
||||
return {
|
||||
...DEFAULT_CONFIG,
|
||||
base: params.base,
|
||||
style: params.style,
|
||||
baseColor: params.baseColor,
|
||||
theme: params.theme,
|
||||
chartColor: params.chartColor,
|
||||
iconLibrary: params.iconLibrary,
|
||||
font: params.font,
|
||||
fontHeading: params.fontHeading,
|
||||
menuAccent: params.menuAccent,
|
||||
menuColor: params.menuColor,
|
||||
radius: isRadiusLocked ? "none" : params.radius,
|
||||
template: params.template,
|
||||
rtl: params.rtl,
|
||||
pointer: params.pointer,
|
||||
}
|
||||
}, [
|
||||
params.base,
|
||||
params.baseColor,
|
||||
params.chartColor,
|
||||
params.font,
|
||||
params.fontHeading,
|
||||
params.iconLibrary,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
params.pointer,
|
||||
params.radius,
|
||||
params.rtl,
|
||||
params.style,
|
||||
params.template,
|
||||
params.theme,
|
||||
])
|
||||
|
||||
const themeCss = React.useMemo(() => {
|
||||
const theme = buildThemeForPreset(themeConfig)
|
||||
|
||||
if (!theme.cssVars) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatThemeCss(theme.cssVars)
|
||||
}, [themeConfig])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
const timer = setTimeout(() => setHasCopied(false), 2000)
|
||||
if (copiedTarget) {
|
||||
const timer = setTimeout(() => setCopiedTarget(null), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [hasCopied])
|
||||
}, [copiedTarget])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
const properties: Record<string, string> = {
|
||||
@@ -135,159 +270,316 @@ export function ProjectForm({
|
||||
name: "copy_npm_command",
|
||||
properties,
|
||||
})
|
||||
setHasCopied(true)
|
||||
setCopiedTarget("command")
|
||||
}, [command, params.template])
|
||||
|
||||
const handleCopyApply = React.useCallback(() => {
|
||||
copyToClipboardWithMeta(applyCommand, {
|
||||
name: "copy_apply_command",
|
||||
properties: {
|
||||
command: applyCommand,
|
||||
applyMode,
|
||||
},
|
||||
})
|
||||
setCopiedTarget("apply")
|
||||
}, [applyCommand, applyMode])
|
||||
|
||||
const handleCopyTheme = React.useCallback(() => {
|
||||
copyToClipboardWithMeta(themeCss, {
|
||||
name: "copy_theme_code",
|
||||
properties: {
|
||||
preset: presetCode,
|
||||
baseColor: params.baseColor,
|
||||
theme: params.theme,
|
||||
format: "css",
|
||||
},
|
||||
})
|
||||
setCopiedTarget("theme")
|
||||
}, [params.baseColor, params.theme, presetCode, themeCss])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger render={<Button className={cn(className)} />}>
|
||||
Create Project
|
||||
Get Code
|
||||
</DialogTrigger>
|
||||
<DialogContent className="dark no-scrollbar max-h-[calc(100svh-2rem)] overflow-y-auto rounded-2xl p-6 shadow-xl **:data-[slot=field-separator]:h-2 sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pick a template and configure your project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<FieldGroup>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field className="-mt-2 gap-3">
|
||||
<FieldLabel>Template</FieldLabel>
|
||||
<TemplateGrid template={params.template} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field className="-mt-2">
|
||||
<FieldLabel>Base</FieldLabel>
|
||||
<BaseGrid base={params.base} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label" className="sr-only">
|
||||
Options
|
||||
</FieldLegend>
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="pointer">
|
||||
<HugeiconsIcon
|
||||
icon={HandPointingRight04Icon}
|
||||
className="size-4 -rotate-90"
|
||||
/>
|
||||
Use pointer on buttons
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="pointer"
|
||||
checked={params.pointer}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ pointer: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-disabled={hasMonorepo ? undefined : "true"}
|
||||
>
|
||||
<FieldLabel htmlFor="monorepo">
|
||||
<span
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 [&_svg]:fill-current"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: TURBOREPO_LOGO,
|
||||
}}
|
||||
/>
|
||||
Create a monorepo
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="monorepo"
|
||||
checked={params.template?.endsWith("-monorepo") ?? false}
|
||||
disabled={!hasMonorepo}
|
||||
onCheckedChange={(checked) => {
|
||||
const framework = getFramework(params.template ?? "next")
|
||||
setParams({
|
||||
template: getTemplateValue(
|
||||
framework,
|
||||
checked === true
|
||||
) as typeof params.template,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="rtl">
|
||||
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
|
||||
Enable RTL support
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="rtl"
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ rtl: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
<DialogFooter className="-mx-6 -mb-6 min-w-0">
|
||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
packageManager: value as PackageManager,
|
||||
}))
|
||||
}}
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
|
||||
<DialogContent className="dark top-[64px] no-scrollbar flex max-h-[calc(100svh-2rem)] translate-y-0 flex-col rounded-2xl p-0 shadow-xl **:data-[slot=dialog-close]:top-4.5 **:data-[slot=dialog-close]:right-4 **:data-[slot=field-separator]:h-2 sm:max-w-md">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0 overflow-hidden rounded-2xl">
|
||||
<DialogHeader className="border-b px-6 py-5">
|
||||
<ToggleGroup
|
||||
value={[activeTab]}
|
||||
onValueChange={(values) =>
|
||||
setActiveTab((values[0] as typeof activeTab) ?? "new-project")
|
||||
}
|
||||
aria-label="Project type"
|
||||
spacing={2}
|
||||
className="**:data-[slot=toggle-group-item]:data-pressed:bg-neutral-700/70"
|
||||
>
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
|
||||
<TabsList className="bg-transparent font-mono">
|
||||
{PACKAGE_MANAGERS.map((manager) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={manager}
|
||||
value={manager}
|
||||
className="py-0 leading-none data-[state=active]:shadow-none"
|
||||
<ToggleGroupItem value="new-project">New Project</ToggleGroupItem>
|
||||
<ToggleGroupItem value="existing-project">
|
||||
Existing Project
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="theme">Theme</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</DialogHeader>
|
||||
{activeTab === "new-project" && (
|
||||
<div className="no-scrollbar overflow-y-auto">
|
||||
<FieldGroup className="px-6 py-4">
|
||||
<Field className="gap-3">
|
||||
<FieldLabel>Template</FieldLabel>
|
||||
<TemplateGrid
|
||||
template={params.template}
|
||||
setParams={setParams}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field>
|
||||
<FieldLabel>Base</FieldLabel>
|
||||
<BaseGrid base={params.base} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label" className="sr-only">
|
||||
Options
|
||||
</FieldLegend>
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="pointer">
|
||||
<HugeiconsIcon
|
||||
icon={HandPointingRight04Icon}
|
||||
className="size-4 -rotate-90"
|
||||
/>
|
||||
Use pointer on buttons
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="pointer"
|
||||
checked={params.pointer}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ pointer: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-disabled={hasMonorepo ? undefined : "true"}
|
||||
>
|
||||
<FieldLabel htmlFor="monorepo">
|
||||
<span
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 [&_svg]:fill-current"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: TURBOREPO_LOGO,
|
||||
}}
|
||||
/>
|
||||
Create a monorepo
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="monorepo"
|
||||
checked={params.template?.endsWith("-monorepo") ?? false}
|
||||
disabled={!hasMonorepo}
|
||||
onCheckedChange={(checked) => {
|
||||
const framework = getFramework(
|
||||
params.template ?? "next"
|
||||
)
|
||||
setParams({
|
||||
template: getTemplateValue(
|
||||
framework,
|
||||
checked === true
|
||||
) as typeof params.template,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="rtl">
|
||||
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
|
||||
Enable RTL support
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="rtl"
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ rtl: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
<DialogFooter className="m-0 min-w-0 p-6">
|
||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
packageManager: value as PackageManager,
|
||||
}))
|
||||
}}
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
|
||||
>
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
|
||||
<TabsList className="bg-transparent font-mono">
|
||||
{PACKAGE_MANAGERS.map((manager) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={manager}
|
||||
value={manager}
|
||||
className="py-0 leading-none data-[state=active]:shadow-none"
|
||||
>
|
||||
{manager}
|
||||
</TabsTrigger>
|
||||
)
|
||||
})}
|
||||
</TabsList>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{manager}
|
||||
</TabsTrigger>
|
||||
)
|
||||
})}
|
||||
</TabsList>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="relative overflow-hidden border-t bg-popover p-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
{copiedTarget === "command" ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<Button onClick={handleCopy} className="h-9 w-full">
|
||||
{hasCopied ? "Copied" : "Copy Command"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="relative overflow-hidden border-t bg-popover p-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<Button onClick={handleCopy} className="h-9 w-full">
|
||||
{copiedTarget === "command" ? "Copied" : "Copy Command"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "existing-project" && (
|
||||
<div className="no-scrollbar overflow-y-auto">
|
||||
<FieldGroup className="px-6 py-4">
|
||||
<FieldSet className="gap-3">
|
||||
<FieldLegend variant="label">Apply Preset</FieldLegend>
|
||||
<FieldDescription>
|
||||
Pick which parts of the preset to apply.
|
||||
</FieldDescription>
|
||||
<ApplyModeGrid mode={applyMode} setMode={setApplyMode} />
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
<DialogFooter className="m-0 min-w-0 p-6">
|
||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
packageManager: value as PackageManager,
|
||||
}))
|
||||
}}
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
|
||||
>
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
|
||||
<TabsList className="bg-transparent font-mono">
|
||||
{PACKAGE_MANAGERS.map((manager) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={manager}
|
||||
value={manager}
|
||||
className="py-0 leading-none data-[state=active]:shadow-none"
|
||||
>
|
||||
{manager}
|
||||
</TabsTrigger>
|
||||
)
|
||||
})}
|
||||
</TabsList>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopyApply}
|
||||
>
|
||||
{copiedTarget === "apply" ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(applyCommands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="relative overflow-hidden border-t bg-popover p-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<Button onClick={handleCopyApply} className="h-9 w-full">
|
||||
{copiedTarget === "apply" ? "Copied" : "Copy Command"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "theme" && (
|
||||
<div className="no-scrollbar overflow-y-auto">
|
||||
<FieldGroup className="min-w-0 px-6 py-4">
|
||||
<FieldSet className="min-w-0 gap-3">
|
||||
<FieldLegend variant="label">Theme Tokens</FieldLegend>
|
||||
<FieldDescription>
|
||||
Copy the CSS variables for this preset.
|
||||
</FieldDescription>
|
||||
<div className="w-full min-w-0 overflow-hidden rounded-xl border-0 ring-1 ring-border">
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-3">
|
||||
<div className="min-w-0 truncate font-mono text-sm text-muted-foreground">
|
||||
globals.css
|
||||
</div>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopyTheme}
|
||||
>
|
||||
{copiedTarget === "theme" ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy theme</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative no-scrollbar max-h-[45svh] overflow-auto border-t bg-popover p-3">
|
||||
<pre className="min-w-max font-mono leading-normal whitespace-pre">
|
||||
<code>{themeCss}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
<DialogFooter className="m-0 min-w-0 p-6">
|
||||
<Button onClick={handleCopyTheme} className="h-9 w-full">
|
||||
{copiedTarget === "theme" ? "Copied" : "Copy Theme"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
@@ -352,6 +644,34 @@ const TemplateGrid = React.memo(function TemplateGrid({
|
||||
)
|
||||
})
|
||||
|
||||
const ApplyModeGrid = React.memo(function ApplyModeGrid({
|
||||
mode,
|
||||
setMode,
|
||||
}: {
|
||||
mode: ApplyMode
|
||||
setMode: (mode: ApplyMode) => void
|
||||
}) {
|
||||
return (
|
||||
<RadioGroup
|
||||
value={mode}
|
||||
onValueChange={(value) => setMode(value as ApplyMode)}
|
||||
aria-label="Apply"
|
||||
>
|
||||
{APPLY_MODES.map((option) => (
|
||||
<FieldLabel key={option.value} htmlFor={`apply-${option.value}`}>
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value={option.value} id={`apply-${option.value}`} />
|
||||
<FieldContent>
|
||||
<FieldTitle>{option.title}</FieldTitle>
|
||||
<FieldDescription>{option.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
})
|
||||
|
||||
const BaseGrid = React.memo(function BaseGrid({
|
||||
base,
|
||||
setParams,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -15,6 +15,8 @@ import { PreviewStyle } from "@/app/(app)/create/components/preview-style"
|
||||
import { RandomizeScript } from "@/app/(app)/create/components/random-button"
|
||||
import { getBaseComponent, getBaseItem } from "@/app/(app)/create/lib/api"
|
||||
|
||||
import "@/app/style-registry.css"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
export const dynamicParams = true
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -353,14 +346,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);
|
||||
@@ -7,7 +7,7 @@ export function Announcement() {
|
||||
return (
|
||||
<Badge asChild variant="secondary" className="bg-muted">
|
||||
<Link href="/docs/changelog">
|
||||
New preset commands <ArrowRightIcon />
|
||||
Introducing Rhea <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}
|
||||
|
||||
@@ -8,11 +8,9 @@ import { siteConfig } from "@/lib/config"
|
||||
import { source } from "@/lib/source"
|
||||
import { CommandMenu } from "@/components/command-menu"
|
||||
import { GitHubLink } from "@/components/github-link"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { MainNav } from "@/components/main-nav"
|
||||
import { MobileNav } from "@/components/mobile-nav"
|
||||
import { ModeSwitcher } from "@/components/mode-switcher"
|
||||
import { SiteConfig } from "@/components/site-config"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ProjectForm } from "@/app/(app)/create/components/project-form"
|
||||
@@ -31,17 +29,6 @@ export function SiteHeader() {
|
||||
items={siteConfig.navItems}
|
||||
className="flex lg:hidden"
|
||||
/>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hidden size-8 lg:flex"
|
||||
>
|
||||
<Link href="/">
|
||||
<Icons.logo className="size-5" />
|
||||
<span className="sr-only">{siteConfig.name}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
|
||||
<div className="ml-auto flex items-center gap-2 md:flex-1 md:justify-end">
|
||||
<div className="hidden w-full flex-1 md:flex md:w-auto md:flex-none">
|
||||
@@ -56,11 +43,6 @@ export function SiteHeader() {
|
||||
className="ml-2 hidden lg:block"
|
||||
/>
|
||||
<GitHubLink />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="hidden group-has-data-[slot=designer]/layout:hidden 3xl:flex"
|
||||
/>
|
||||
<SiteConfig className="hidden 3xl:flex 3xl:group-has-data-[slot=designer]/layout:hidden" />
|
||||
<Separator orientation="vertical" />
|
||||
<ModeSwitcher />
|
||||
<div className="hidden items-center gap-2 group-has-data-[slot=designer]/layout:md:flex">
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"[CLI](/docs/cli)",
|
||||
"monorepo",
|
||||
"skills",
|
||||
"v0",
|
||||
"javascript",
|
||||
"blocks",
|
||||
"figma",
|
||||
|
||||
120
apps/v4/content/docs/changelog/2026-05-registry-include.mdx
Normal file
120
apps/v4/content/docs/changelog/2026-05-registry-include.mdx
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: May 2026 - Registry Include and Validate
|
||||
description: Organize and validate source registries.
|
||||
date: 2026-05-20
|
||||
---
|
||||
|
||||
This release adds two updates for registry authors:
|
||||
|
||||
- `include` for composing large source registries from multiple `registry.json`
|
||||
files.
|
||||
- `shadcn registry validate` for checking source registries before publishing.
|
||||
|
||||
This makes it easier to maintain source and dynamic registries without keeping
|
||||
one large `registry.json` file by hand.
|
||||
|
||||
Registry authors can now organize a large source registry across multiple
|
||||
`registry.json` files and compose them with `shadcn build`.
|
||||
|
||||
```txt /registry.json/
|
||||
registry.json
|
||||
components
|
||||
└── ui
|
||||
├── button.tsx
|
||||
├── input.tsx
|
||||
└── registry.json
|
||||
hooks
|
||||
├── registry.json
|
||||
├── use-media-query.ts
|
||||
└── use-toggle.ts
|
||||
```
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```json title="registry.json" showLineNumbers {6-7}
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"name": "acme",
|
||||
"homepage": "https://acme.com",
|
||||
"include": [
|
||||
"components/ui/registry.json",
|
||||
"hooks/registry.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Included `registry.json` files are valid registry files for composition and may
|
||||
omit `name` and `homepage`. Only the root `registry.json` must define the
|
||||
registry metadata.
|
||||
|
||||
```json title="components/ui/registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"items": [
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"files": [
|
||||
{
|
||||
"path": "button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Build output
|
||||
|
||||
`shadcn build` resolves included registries and writes a flattened
|
||||
`registry.json` without `include`. Item file paths are preserved from the root
|
||||
registry, so a file declared in `components/ui/registry.json` is written as
|
||||
`components/ui/button.tsx` in the built registry item.
|
||||
|
||||
## Validate your registry
|
||||
|
||||
You can now validate a source registry before publishing or serving it.
|
||||
|
||||
```bash
|
||||
npx shadcn registry validate
|
||||
```
|
||||
|
||||
Validation runs against the source registry files directly. You do not need to
|
||||
run `shadcn build` first.
|
||||
|
||||
The command checks the root `registry.json`, included registry files, item
|
||||
schema errors, duplicate item names, include rules, and local item file paths.
|
||||
Validation reports all actionable errors it can find in one run.
|
||||
|
||||
## Registry loaders
|
||||
|
||||
The `shadcn/registry` package also exports `loadRegistry` and
|
||||
`loadRegistryItem` for dynamic registry routes.
|
||||
|
||||
```ts title="app/r/registry.json/route.ts" showLineNumbers
|
||||
import { loadRegistry } from "shadcn/registry"
|
||||
|
||||
export async function GET() {
|
||||
const registry = await loadRegistry()
|
||||
|
||||
return Response.json(registry)
|
||||
}
|
||||
```
|
||||
|
||||
```ts title="app/r/[name].json/route.ts" showLineNumbers
|
||||
import { loadRegistryItem } from "shadcn/registry"
|
||||
|
||||
export async function GET(
|
||||
_: Request,
|
||||
{ params }: { params: Promise<{ name: string }> }
|
||||
) {
|
||||
const { name } = await params
|
||||
const item = await loadRegistryItem(name)
|
||||
|
||||
return Response.json(item)
|
||||
}
|
||||
```
|
||||
|
||||
See the [registry.json documentation](/docs/registry/registry-json#include) and
|
||||
[getting started guide](/docs/registry/getting-started#structure-your-registry)
|
||||
for more details.
|
||||
45
apps/v4/content/docs/changelog/2026-05-rhea.mdx
Normal file
45
apps/v4/content/docs/changelog/2026-05-rhea.mdx
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: May 2026 - Introducing Rhea
|
||||
description: A more compact Luma. Smaller spacing. Denser surfaces. Built for focused product interfaces.
|
||||
date: 2026-05-26
|
||||
---
|
||||
|
||||
Introducing Rhea, a new shadcn/ui style. A more compact Luma. Smaller spacing. Denser surfaces. Built for focused product interfaces.
|
||||
|
||||
<a href="/create?preset=b27GcrRo">
|
||||
<Image
|
||||
src="/images/rhea-light.png"
|
||||
width="3840"
|
||||
height="2160"
|
||||
alt="Rhea style preview"
|
||||
className="mt-6 w-full overflow-hidden rounded-lg border dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/images/rhea-dark.png"
|
||||
width="3840"
|
||||
height="2160"
|
||||
alt="Rhea style preview"
|
||||
className="mt-6 hidden w-full overflow-hidden rounded-lg border shadow-sm dark:block"
|
||||
/>
|
||||
<span className="sr-only">Try Rhea in shadcn/create</span>
|
||||
</a>
|
||||
|
||||
Rhea started from a simple request we've heard a lot: Luma, but more compact. We looked at how people were using the new styles and what they were asking for, and the pattern was clear. A lot of teams wanted the softness and shape of Luma with tighter spacing, smaller controls, and more information density.
|
||||
|
||||
Rhea keeps the same rounded foundation, but makes it more compact for product interfaces where space matters. Buttons, inputs, menus, cards, and lists all sit a little tighter so the UI can carry more without feeling crowded.
|
||||
|
||||
## Why a new style?
|
||||
|
||||
We considered making this a spacing tweak for Luma, but `--spacing` is a multiplier. Changing it would change what familiar utilities mean across your app. `p-2`, `w-4`, and `m-16` would no longer mean the same size.
|
||||
|
||||
That tradeoff felt wrong. Compactness should not force you to relearn Tailwind's spacing scale or wonder whether a utility means something different in one style than another.
|
||||
|
||||
So Rhea is a new style instead. It lets us adjust component sizes, gaps, and density directly while keeping the underlying utility scale predictable.
|
||||
|
||||
Available now in [shadcn/create](/create) for both Radix and Base UI.
|
||||
|
||||
<Button asChild size="sm">
|
||||
<Link href="/create?preset=b27GcrRo" className="mt-6 no-underline!">
|
||||
Try Rhea
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -156,7 +156,7 @@ The `Field` family is designed for composing accessible forms. A typical field i
|
||||
|
||||
## Form
|
||||
|
||||
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form) or [Tanstack Form](/docs/forms/tanstack-form).
|
||||
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form), [Tanstack Form](/docs/forms/tanstack-form), or [Formisch](/docs/forms/formisch).
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -129,3 +129,9 @@ To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl)
|
||||
## API Reference
|
||||
|
||||
See the [Base UI Toggle Group](https://base-ui.com/react/components/toggle-group#api-reference) documentation.
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-05-17 Default Spacing
|
||||
|
||||
Changed the default `spacing` from `0` to `2` so toggle groups render with space between items by default. Use `spacing={0}` for connected items.
|
||||
|
||||
@@ -156,7 +156,7 @@ The `Field` family is designed for composing accessible forms. A typical field i
|
||||
|
||||
## Form
|
||||
|
||||
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form) or [Tanstack Form](/docs/forms/tanstack-form).
|
||||
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form), [Tanstack Form](/docs/forms/tanstack-form), or [Formisch](/docs/forms/formisch).
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -129,3 +129,9 @@ To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl)
|
||||
## API Reference
|
||||
|
||||
See the [Radix Toggle Group](https://www.radix-ui.com/docs/primitives/components/toggle-group#api-reference) documentation.
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-05-17 Default Spacing
|
||||
|
||||
Changed the default `spacing` from `0` to `2` so toggle groups render with space between items by default. Use `spacing={0}` for connected items.
|
||||
|
||||
669
apps/v4/content/docs/forms/formisch.mdx
Normal file
669
apps/v4/content/docs/forms/formisch.mdx
Normal file
@@ -0,0 +1,669 @@
|
||||
---
|
||||
title: Formisch
|
||||
description: Build forms in React using Formisch and Valibot.
|
||||
links:
|
||||
doc: https://formisch.dev
|
||||
---
|
||||
|
||||
import { InfoIcon } from "lucide-react"
|
||||
|
||||
This guide covers building forms with [Formisch](https://formisch.dev), the lightweight, schema-first, and fully type-safe form library for React. We'll create forms with the `<Field />` component, validate them with Valibot schemas, handle errors, and ensure accessibility.
|
||||
|
||||
## Demo
|
||||
|
||||
We'll build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** For the purpose of this demo, we have intentionally disabled browser
|
||||
validation to show how schema validation and form errors work in Formisch. It
|
||||
is recommended to add basic browser validation in your production code.
|
||||
</Callout>
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-demo"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Approach
|
||||
|
||||
This form leverages Formisch for headless, schema-first form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
|
||||
|
||||
- Uses Formisch's `useForm` hook for form state management.
|
||||
- `<Form />` component to wrap the native `<form>` element with submit handling.
|
||||
- `<Field />` render-prop component for controlled inputs.
|
||||
- Schema validation using [Valibot](https://valibot.dev).
|
||||
- Type-safe field paths inferred from the schema.
|
||||
|
||||
## Form Methods
|
||||
|
||||
Formisch exposes form operations as **top-level functions** rather than methods on a form object. Import only what you need:
|
||||
|
||||
```ts
|
||||
import { getInput, insert, reset, submit } from "@formisch/react"
|
||||
```
|
||||
|
||||
Every method follows the same signature: the **first parameter is always the form store**, and the **second parameter (if necessary) is always a config object**.
|
||||
|
||||
```ts
|
||||
// Read a field value
|
||||
const email = getInput(form, { path: ["email"] })
|
||||
|
||||
// Reset the form with new initial values
|
||||
reset(form, { initialInput: { email: "", password: "" } })
|
||||
|
||||
// Move an item in a field array
|
||||
move(form, { path: ["items"], from: 0, to: 3 })
|
||||
```
|
||||
|
||||
This design keeps the API flexible and consistent across all methods. You'll see the same `(form, config)` shape used throughout this guide for reading state (`getInput`, `getErrors`), writing state (`setInput`, `setErrors`), form control (`submit`, `validate`, `focus`), and array operations (`insert`, `remove`, `move`, `swap`, `replace`). See the [full methods reference](https://formisch.dev/react/guides/form-methods) for details.
|
||||
|
||||
## Anatomy
|
||||
|
||||
Here's a basic example of a form using the `<Field />` component from Formisch and the shadcn `<Field />` component.
|
||||
|
||||
```tsx showLineNumbers {3-21}
|
||||
<Form of={form} onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["title"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-title">Bug Title</FieldLabel>
|
||||
<Input
|
||||
{...field.props}
|
||||
id="form-title"
|
||||
value={field.input}
|
||||
aria-invalid={field.errors !== null}
|
||||
placeholder="Login button not working on mobile"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Provide a concise title for your bug report.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
```
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** Formisch ships its own `Field` component. To avoid a name clash with
|
||||
the shadcn `Field`, the examples below import the Formisch one as
|
||||
`FormischField` and keep the shadcn `Field` under its original name. In your
|
||||
own code you can alias either side — just be consistent.
|
||||
</Callout>
|
||||
|
||||
## Form
|
||||
|
||||
### Create a form schema
|
||||
|
||||
We'll start by defining the shape of our form using a Valibot schema. Formisch infers all input and output types directly from this schema.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
import * as v from "valibot"
|
||||
|
||||
const FormSchema = v.object({
|
||||
title: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(5, "Bug title must be at least 5 characters."),
|
||||
v.maxLength(32, "Bug title must be at most 32 characters.")
|
||||
),
|
||||
description: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(20, "Description must be at least 20 characters."),
|
||||
v.maxLength(100, "Description must be at most 100 characters.")
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
### Set up the form
|
||||
|
||||
Next, we'll use the `useForm` hook from Formisch to create our form instance. The schema is passed directly to `useForm` — there is no resolver step.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {1-2,21-25}
|
||||
import { Form, Field as FormischField, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import * as v from "valibot"
|
||||
|
||||
const FormSchema = v.object({
|
||||
title: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(5, "Bug title must be at least 5 characters."),
|
||||
v.maxLength(32, "Bug title must be at most 32 characters.")
|
||||
),
|
||||
description: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(20, "Description must be at least 20 characters."),
|
||||
v.maxLength(100, "Description must be at most 100 characters.")
|
||||
),
|
||||
})
|
||||
|
||||
export function BugReportForm() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
// Do something with the validated form values.
|
||||
console.log(output)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form of={form} onSubmit={handleSubmit}>
|
||||
{/* ... */}
|
||||
{/* Build the form here */}
|
||||
{/* ... */}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `<Form />` component wraps a native `<form>` element. It calls `event.preventDefault()`, runs validation, and only invokes `onSubmit` when the data is valid. The `output` you receive is fully typed from the schema.
|
||||
|
||||
### Build the form
|
||||
|
||||
We can now build the form using the `<Field />` component from Formisch and the shadcn `<Field />` component.
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-formisch-demo.tsx"
|
||||
title="form.tsx"
|
||||
/>
|
||||
|
||||
### Done
|
||||
|
||||
That's it. You now have a fully accessible form with client-side validation.
|
||||
|
||||
When you submit the form, the `handleSubmit` function will be called with the validated form data. If the form data is invalid, Formisch will populate `field.errors` for each invalid field and the UI will display them.
|
||||
|
||||
## Validation
|
||||
|
||||
### Client-side Validation
|
||||
|
||||
Formisch validates your form data using the Valibot schema you pass to `useForm`. There is no resolver — the schema is the single source of truth for both runtime validation and static types.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {1,3-6,11}
|
||||
import { useForm } from "@formisch/react"
|
||||
|
||||
const FormSchema = v.object({
|
||||
title: v.string(),
|
||||
description: v.optional(v.string()),
|
||||
})
|
||||
|
||||
export function ExampleForm() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Modes
|
||||
|
||||
Formisch separates the **first** validation from **subsequent** validations. You configure them with the `validate` and `revalidate` options on `useForm`.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {3-4}
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
validate: "blur",
|
||||
revalidate: "input",
|
||||
})
|
||||
```
|
||||
|
||||
| Option | Value | Description |
|
||||
| ------------ | ----------- | --------------------------------------------------------------- |
|
||||
| `validate` | `"submit"` | Validate on form submission (default). |
|
||||
| `validate` | `"blur"` | Validate when a field loses focus. |
|
||||
| `validate` | `"input"` | Validate on every input change. |
|
||||
| `validate` | `"initial"` | Validate immediately on form creation. |
|
||||
| `revalidate` | `"input"` | Revalidate on every input change after the first run (default). |
|
||||
| `revalidate` | `"blur"` | Revalidate on blur after the first run. |
|
||||
| `revalidate` | `"submit"` | Revalidate only on form submission. |
|
||||
|
||||
## Displaying Errors
|
||||
|
||||
Display errors next to the field using `<FieldError />`. Formisch returns errors as an array of strings, so map them to the shape `<FieldError />` expects. For styling and accessibility:
|
||||
|
||||
- Add the `data-invalid` prop to the `<Field />` component.
|
||||
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {3,10,12-14}
|
||||
<FormischField of={form} path={["email"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-email">Email</FieldLabel>
|
||||
<Input
|
||||
{...field.props}
|
||||
id="form-email"
|
||||
value={field.input}
|
||||
type="email"
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
## Working with Different Field Types
|
||||
|
||||
Formisch exposes two ways to bind a field to an element:
|
||||
|
||||
- **Native HTML elements** (like `<Input />` and `<Textarea />`) — spread `field.props` and provide `value={field.input}`. Formisch wires up `name`, `ref`, `onChange`, `onBlur`, and `onFocus` for you.
|
||||
- **Component-library inputs** (like Radix-based `<Select />`, `<Checkbox />`, `<RadioGroup />`, `<Switch />`) — read the value from `field.input` and call `field.onChange(value)` to update it.
|
||||
|
||||
### Input
|
||||
|
||||
- For input fields, spread `field.props` and provide `value={field.input}`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-input"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5-8}
|
||||
<FormischField of={form} path={["username"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-username">Username</FieldLabel>
|
||||
<Input
|
||||
{...field.props}
|
||||
id="form-username"
|
||||
value={field.input}
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Textarea
|
||||
|
||||
- For textarea fields, spread `field.props` and provide `value={field.input}`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-textarea"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {7-10}
|
||||
<FormischField of={form} path={["about"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-about">More about you</FieldLabel>
|
||||
<Textarea
|
||||
{...field.props}
|
||||
id="form-about"
|
||||
value={field.input}
|
||||
aria-invalid={field.errors !== null}
|
||||
placeholder="I'm a software engineer..."
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Tell us more about yourself. This will be used to help us personalize
|
||||
your experience.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Select
|
||||
|
||||
- For select components, read `field.input` and call `field.onChange` from `<Select />`'s `onValueChange`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-select"
|
||||
className="sm:[&_.preview]:h-[500px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {15-19}
|
||||
<FormischField of={form} path={["language"]}>
|
||||
{(field) => (
|
||||
<Field orientation="responsive" data-invalid={field.errors !== null}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-language">Spoken Language</FieldLabel>
|
||||
<FieldDescription>
|
||||
For best results, select the language you speak.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Select value={field.input} onValueChange={field.onChange}>
|
||||
<SelectTrigger
|
||||
id="form-language"
|
||||
aria-invalid={field.errors !== null}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Checkbox
|
||||
|
||||
- For checkbox arrays, read `field.input` and update it from `onCheckedChange` using `field.onChange`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-checkbox"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {16,19-25}
|
||||
<FormischField of={form} path={["tasks"]}>
|
||||
{(field) => (
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Tasks</FieldLegend>
|
||||
<FieldDescription>
|
||||
Get notified when tasks you've created have updates.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{tasks.map((task) => (
|
||||
<Field
|
||||
key={task.id}
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<Checkbox
|
||||
id={`form-checkbox-${task.id}`}
|
||||
aria-invalid={field.errors !== null}
|
||||
checked={field.input?.includes(task.id) ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
const current = field.input ?? []
|
||||
field.onChange(
|
||||
checked === true
|
||||
? [...current, task.id]
|
||||
: current.filter((value) => value !== task.id)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor={`form-checkbox-${task.id}`}
|
||||
className="font-normal"
|
||||
>
|
||||
{task.label}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Radio Group
|
||||
|
||||
- For radio groups, read `field.input` and call `field.onChange` from `onValueChange`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-radiogroup"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {9-13,21}
|
||||
<FormischField of={form} path={["plan"]}>
|
||||
{(field) => (
|
||||
<FieldSet>
|
||||
<FieldLegend>Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
You can upgrade or downgrade your plan at any time.
|
||||
</FieldDescription>
|
||||
<RadioGroup value={field.input} onValueChange={field.onChange}>
|
||||
{plans.map((plan) => (
|
||||
<FieldLabel key={plan.id} htmlFor={`form-radiogroup-${plan.id}`}>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<FieldContent>
|
||||
<FieldTitle>{plan.title}</FieldTitle>
|
||||
<FieldDescription>{plan.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={plan.id}
|
||||
id={`form-radiogroup-${plan.id}`}
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Switch
|
||||
|
||||
- For switches, read `field.input` and call `field.onChange` from `onCheckedChange`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-switch"
|
||||
className="sm:[&_.preview]:h-[500px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {15-19}
|
||||
<FormischField of={form} path={["twoFactor"]}>
|
||||
{(field) => (
|
||||
<Field orientation="horizontal" data-invalid={field.errors !== null}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-twoFactor">
|
||||
Multi-factor authentication
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable multi-factor authentication to secure your account.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="form-twoFactor"
|
||||
checked={field.input ?? false}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Complex Forms
|
||||
|
||||
Here is an example of a more complex form with multiple fields and validation.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-complex"
|
||||
className="sm:[&_.preview]:h-[1300px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Resetting the Form
|
||||
|
||||
Formisch exposes a top-level `reset` function. Pass the form store to reset it to its initial input.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
```
|
||||
|
||||
You can also reset to new initial values, or reset while keeping the user's current input:
|
||||
|
||||
```tsx showLineNumbers
|
||||
// Reset to a fresh set of initial values
|
||||
reset(form, { initialInput: { title: "", description: "" } })
|
||||
|
||||
// Sync the baseline to new server data, but keep the user's edits
|
||||
reset(form, { initialInput: serverData, keepInput: true })
|
||||
```
|
||||
|
||||
## Array Fields
|
||||
|
||||
Formisch provides a `<FieldArray />` component and a set of helper functions for managing dynamic array fields. Use it whenever you need to add, remove, or reorder items.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-array"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
### Using FieldArray
|
||||
|
||||
`<FieldArray />` follows the same render-prop pattern as `<Field />`. Its `items` array contains a stable key per item that you should use as the React `key`.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {1,7-22}
|
||||
import {
|
||||
Field as FormischField,
|
||||
FieldArray,
|
||||
insert,
|
||||
remove,
|
||||
} from "@formisch/react"
|
||||
|
||||
export function ExampleForm() {
|
||||
// ... form config
|
||||
|
||||
return (
|
||||
<FieldArray of={form} path={["emails"]}>
|
||||
{(fieldArray) => (
|
||||
<FieldGroup className="gap-4">
|
||||
{fieldArray.items.map((item, index) => (
|
||||
<FormischField
|
||||
key={item}
|
||||
of={form}
|
||||
path={["emails", index, "address"]}
|
||||
>
|
||||
{(field) => /* ... */}
|
||||
</FormischField>
|
||||
))}
|
||||
</FieldGroup>
|
||||
)}
|
||||
</FieldArray>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Array Field Structure
|
||||
|
||||
Wrap your array fields in a `<FieldSet />` with a `<FieldLegend />` and `<FieldDescription />`.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<FieldSet className="gap-4">
|
||||
<FieldLegend variant="label">Email Addresses</FieldLegend>
|
||||
<FieldDescription>
|
||||
Add up to 5 email addresses where we can contact you.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
|
||||
</FieldSet>
|
||||
```
|
||||
|
||||
### Adding Items
|
||||
|
||||
Use the `insert` function to add new items to the array. By default new items are appended to the end. You can also pass an `at` index to insert at a specific position.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
insert(form, { path: ["emails"], initialInput: { address: "" } })
|
||||
}
|
||||
disabled={fieldArray.items.length >= 5}
|
||||
>
|
||||
Add Email Address
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Removing Items
|
||||
|
||||
Use the `remove` function with an `at` index to remove items from the array.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
import { remove } from "@formisch/react"
|
||||
|
||||
{
|
||||
fieldArray.items.length > 1 && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => remove(form, { path: ["emails"], at: index })}
|
||||
aria-label={`Remove email ${index + 1}`}
|
||||
>
|
||||
<XIcon />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Formisch also exposes `move`, `swap`, and `replace` for reordering and replacing items. They follow the same `(form, config)` signature.
|
||||
|
||||
### Array Validation
|
||||
|
||||
Use Valibot's `array` and pipeline validators to constrain array fields.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
const FormSchema = v.object({
|
||||
emails: v.pipe(
|
||||
v.array(
|
||||
v.object({
|
||||
address: v.pipe(
|
||||
v.string(),
|
||||
v.nonEmpty("Enter an email address."),
|
||||
v.email("Enter a valid email address.")
|
||||
),
|
||||
})
|
||||
),
|
||||
v.minLength(1, "Add at least one email address."),
|
||||
v.maxLength(5, "You can add up to 5 email addresses.")
|
||||
),
|
||||
})
|
||||
```
|
||||
@@ -25,6 +25,24 @@ Start by selecting your framework. Then follow the instructions to learn how to
|
||||
</svg>
|
||||
<p className="mt-2 font-medium">TanStack Form</p>
|
||||
</LinkedCard>
|
||||
<LinkedCard href="/docs/forms/formisch">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
className="size-10"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="20"
|
||||
d="M90.9 291.7 49 299.4v125.5l193.3 63.8 221-63.8V304.7l-62-13.7m-352.2 8.6 193.2 53.6V488m221-183.2-221.1 48.4m-171-67.7 147.5 47 212.7-48.3L437 76 271.9 25 72.1 66.3Zm1-219.3L220.5 118l-1.8 214m1.8-214.3 216.2-41.6m-84.4 159.6-.9 1.9c-5.2 11.3-16.2 34.8-33.5 35.5h-1a22 22 0 0 1-9.7-2.2 26 26 0 0 1-7.4-5.5c-4.3-4.4-7.4-10-10-14.5q-1.2-2.4-2.7-4.7a54 54 0 0 0 21.7 4.1 74 74 0 0 0 23.6-4 70 70 0 0 0 11.4-5 64 64 0 0 0 8.5-5.6M247.6 168c19.5-20.8 34.8-12.3 36-18.4 1.5-7-12.2-7.7-22.2-2.6s-18 17.7-13.8 21m101.2-33.2c1.6 2.6 8.7-1.6 19.5-1.2s15.8 5.7 18 4.1-4.6-14.7-17.1-15.2-22 9.7-20.4 12.3m21.2 16.3c9.4.6 17.1 12.8 16.1 27.6s-10.4 25.9-19.8 25.2-17.1-12.9-16.1-27.7 10.4-25.8 19.8-25.1m-97.4 19c11 1.2 19 15.1 17.4 30.5s-12.3 27.4-23.4 26.2-19-15-17.4-30.5 12.3-27.3 23.4-26.2"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mt-2 font-medium">Formisch</p>
|
||||
</LinkedCard>
|
||||
<LinkedCard href="#" className="border border-dashed bg-transparent">
|
||||
<svg
|
||||
role="img"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"pages": ["react-hook-form", "tanstack-form"]
|
||||
"pages": ["react-hook-form", "tanstack-form", "formisch"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ If you're starting a new registry project, you can use the [registry template](h
|
||||
|
||||
## Requirements
|
||||
|
||||
You are free to design and host your custom registry as you see fit. The only requirement is that your registry items must be valid JSON files that conform to the [registry-item schema specification](/docs/registry/registry-item-json).
|
||||
You are free to design and host your custom registry as you see fit. The only requirement is that your registry catalog and registry items must be valid JSON files that conform to the [registry schema specification](/docs/registry/registry-json) and [registry-item schema specification](/docs/registry/registry-item-json).
|
||||
|
||||
Your registry can be a Next.js, Vite, Vue, Svelte, PHP or any other framework as long as it supports serving JSON over HTTP.
|
||||
|
||||
If you'd like to see an example of a registry, we have a [template project](https://github.com/shadcn-ui/registry-template) for you to use as a starting point.
|
||||
|
||||
@@ -19,11 +21,7 @@ The `registry.json` is the entry point for the registry. It contains the registr
|
||||
|
||||
Your registry must have this file (or JSON payload) present at the root of the registry endpoint. The registry endpoint is the URL where your registry is hosted.
|
||||
|
||||
The `shadcn` CLI will automatically generate this file for you when you run the `build` command.
|
||||
|
||||
## Add a registry.json file
|
||||
|
||||
Create a `registry.json` file in the root of your project. Your project can be a Next.js, Vite, Vue, Svelte, PHP or any other framework as long as it supports serving JSON over HTTP.
|
||||
Here's an example `registry.json` file:
|
||||
|
||||
```json title="registry.json" showLineNumbers
|
||||
{
|
||||
@@ -31,44 +29,204 @@ Create a `registry.json` file in the root of your project. Your project can be a
|
||||
"name": "acme",
|
||||
"homepage": "https://acme.com",
|
||||
"items": [
|
||||
// ...
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"title": "Button",
|
||||
"description": "A simple button component.",
|
||||
"files": [
|
||||
{
|
||||
"path": "components/ui/button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Structure your registry
|
||||
|
||||
You can structure your source registry in one of two ways:
|
||||
|
||||
- Define all items in a single root `registry.json`.
|
||||
- Use a root `registry.json` with `include` to compose multiple `registry.json` files.
|
||||
|
||||
### Option A: Single registry.json
|
||||
|
||||
Create a `registry.json` file in the root of your project. Add all your registry items to the `items` array. This is the simplest way to define a registry.
|
||||
|
||||
```json title="registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"name": "acme",
|
||||
"homepage": "https://acme.com",
|
||||
"items": [
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"title": "Button",
|
||||
"description": "A simple button component.",
|
||||
"files": [
|
||||
{
|
||||
"path": "components/ui/button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hello-world",
|
||||
"type": "registry:block",
|
||||
"title": "Hello World",
|
||||
"description": "A simple hello world component.",
|
||||
"registryDependencies": ["button"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/hello-world/hello-world.tsx",
|
||||
"type": "registry:component"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This `registry.json` file must conform to the [registry schema specification](/docs/registry/registry-json).
|
||||
|
||||
## Add a registry item
|
||||
### Option B: Using include
|
||||
|
||||
### Create your component
|
||||
For larger registries, you can use `include` to compose your source registry
|
||||
from multiple `registry.json` files.
|
||||
|
||||
Add your first component. Here's an example of a simple `<HelloWorld />` component:
|
||||
```txt
|
||||
registry.json
|
||||
components
|
||||
└── ui
|
||||
├── button.tsx
|
||||
├── input.tsx
|
||||
└── registry.json
|
||||
hooks
|
||||
├── registry.json
|
||||
├── use-media-query.ts
|
||||
└── use-toggle.ts
|
||||
```
|
||||
|
||||
```tsx title="registry/new-york/hello-world/hello-world.tsx" showLineNumbers
|
||||
import { Button } from "@/components/ui/button"
|
||||
The root `registry.json` defines the registry metadata and includes the nested
|
||||
registry files.
|
||||
|
||||
export function HelloWorld() {
|
||||
return <Button>Hello World</Button>
|
||||
{/* prettier-ignore */}
|
||||
```json title="registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"name": "acme",
|
||||
"homepage": "https://acme.com",
|
||||
"include": [
|
||||
"components/ui/registry.json",
|
||||
"hooks/registry.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Included `registry.json` files are valid registry files for composition and may
|
||||
omit `name` and `homepage`. Only the root `registry.json` must define the
|
||||
registry metadata.
|
||||
|
||||
```json title="components/ui/registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"items": [
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"files": [
|
||||
{
|
||||
"path": "button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "input",
|
||||
"type": "registry:ui",
|
||||
"files": [
|
||||
{
|
||||
"path": "input.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json title="hooks/registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"items": [
|
||||
{
|
||||
"name": "use-toggle",
|
||||
"type": "registry:hook",
|
||||
"files": [
|
||||
{
|
||||
"path": "use-toggle.ts",
|
||||
"type": "registry:hook"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "use-media-query",
|
||||
"type": "registry:hook",
|
||||
"files": [
|
||||
{
|
||||
"path": "use-media-query.ts",
|
||||
"type": "registry:hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When using `include`, file paths are relative to the `registry.json` file that
|
||||
declares the item.
|
||||
|
||||
## Add an item
|
||||
|
||||
### Create a UI component
|
||||
|
||||
Add your first item. Here's an example of a simple `<Button />` component:
|
||||
|
||||
```tsx title="components/ui/button.tsx" showLineNumbers
|
||||
import * as React from "react"
|
||||
|
||||
export function Button(props: React.ComponentProps<"button">) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className="rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white"
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Callout className="mt-6">
|
||||
**Note:** This example places the component in the `registry/new-york`
|
||||
directory. You can place it anywhere in your project as long as you set the
|
||||
correct path in the `registry.json` file and you follow the `registry/[NAME]`
|
||||
directory structure.
|
||||
**Note:** This example places the component in the `components/ui` directory.
|
||||
You can place it anywhere in your project as long as you set the correct path
|
||||
in the `registry.json` file.
|
||||
</Callout>
|
||||
|
||||
```txt
|
||||
registry
|
||||
└── new-york
|
||||
└── hello-world
|
||||
└── hello-world.tsx
|
||||
components
|
||||
└── ui
|
||||
└── button.tsx
|
||||
```
|
||||
|
||||
### Add your component to the registry
|
||||
### Add the item to the registry
|
||||
|
||||
To add your component to the registry, you need to add your component definition to `registry.json`.
|
||||
To add your component to the registry, add an item definition to `registry.json`.
|
||||
If you are using `include`, add the item to the included `registry.json` file
|
||||
that owns the component. For example, add a UI component to
|
||||
`components/ui/registry.json`.
|
||||
|
||||
```json title="registry.json" showLineNumbers {6-17}
|
||||
{
|
||||
@@ -77,14 +235,14 @@ To add your component to the registry, you need to add your component definition
|
||||
"homepage": "https://acme.com",
|
||||
"items": [
|
||||
{
|
||||
"name": "hello-world",
|
||||
"type": "registry:block",
|
||||
"title": "Hello World",
|
||||
"description": "A simple hello world component.",
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"title": "Button",
|
||||
"description": "A simple button component.",
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york/hello-world/hello-world.tsx",
|
||||
"type": "registry:component"
|
||||
"path": "components/ui/button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -94,76 +252,144 @@ To add your component to the registry, you need to add your component definition
|
||||
|
||||
You define your registry item by adding a `name`, `type`, `title`, `description` and `files`.
|
||||
|
||||
For every file you add, you must specify the `path` and `type` of the file. The `path` is the relative path to the file from the root of your project. The `type` is the type of the file.
|
||||
For every file you add, you must specify the `path` and `type` of the file. In a single-file registry, the `path` is relative to the root of your project. When using `include`, the `path` is relative to the `registry.json` file that declares the item. The `type` is the type of the file.
|
||||
|
||||
You can read more about the registry item schema and file types in the [registry item schema docs](/docs/registry/registry-item-json).
|
||||
|
||||
## Build your registry
|
||||
## Serve your registry
|
||||
|
||||
### Install the shadcn CLI
|
||||
You can serve your registry as static JSON files or from dynamic route handlers.
|
||||
|
||||
### Option A: Static JSON files
|
||||
|
||||
Run the build command to generate static registry JSON files.
|
||||
|
||||
```bash
|
||||
npm install shadcn@latest
|
||||
npx shadcn@latest build
|
||||
```
|
||||
|
||||
### Add a build script
|
||||
|
||||
Add a `registry:build` script to your `package.json` file.
|
||||
|
||||
```json title="package.json" showLineNumbers
|
||||
{
|
||||
"scripts": {
|
||||
"registry:build": "shadcn build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Run the build script
|
||||
|
||||
Run the build script to generate the registry JSON files.
|
||||
|
||||
```bash
|
||||
npm run registry:build
|
||||
```
|
||||
If your source registry uses `include`, `shadcn build` resolves the included
|
||||
registries and writes a flattened registry to your output directory. The
|
||||
generated `registry.json` does not contain `include`.
|
||||
|
||||
<Callout className="mt-6">
|
||||
**Note:** By default, the build script will generate the registry JSON files
|
||||
in `public/r` e.g `public/r/hello-world.json`.
|
||||
|
||||
You can change the output directory by passing the `--output` option. See the [shadcn build command](/docs/cli#build) for more information.
|
||||
**Note:** By default, the build command will generate the registry JSON files
|
||||
in `public/r` e.g `public/r/button.json`. You can change the output directory by passing the `--output` option. See the [shadcn build command](/docs/cli#build) for more information.
|
||||
|
||||
</Callout>
|
||||
|
||||
## Serve your registry
|
||||
|
||||
If you're running your registry on Next.js, you can now serve your registry by running the `next` server. The command might differ for other frameworks.
|
||||
If you're running your registry on Next.js, you can serve these files by running
|
||||
the `next` server. The command might differ for other frameworks.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your files will now be served at `http://localhost:3000/r/[NAME].json` eg. `http://localhost:3000/r/hello-world.json`.
|
||||
Your files will now be served at `http://localhost:3000/r/[NAME].json` eg. `http://localhost:3000/r/button.json`.
|
||||
|
||||
## Content negotiation
|
||||
### Option B: Dynamic route handlers
|
||||
|
||||
If you want to serve registry JSON from your source `registry.json` at request
|
||||
time, use the producer-side loader APIs from `shadcn/registry`.
|
||||
|
||||
Install `shadcn` as a runtime dependency:
|
||||
|
||||
```bash
|
||||
npm install shadcn
|
||||
```
|
||||
|
||||
Use `loadRegistry` to serve the registry catalog.
|
||||
|
||||
```ts title="app/r/registry.json/route.ts" showLineNumbers
|
||||
import { loadRegistry } from "shadcn/registry"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const registry = await loadRegistry()
|
||||
|
||||
return Response.json(registry)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
return Response.json({ error: "Failed to load registry." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `loadRegistryItem` to serve individual registry items.
|
||||
|
||||
```ts title="app/r/[name].json/route.ts" showLineNumbers
|
||||
import { loadRegistryItem, RegistryItemNotFoundError } from "shadcn/registry"
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
context: {
|
||||
params: Promise<{
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
) {
|
||||
const { name } = await context.params
|
||||
|
||||
try {
|
||||
const item = await loadRegistryItem(name)
|
||||
|
||||
return Response.json(item)
|
||||
} catch (error) {
|
||||
if (error instanceof RegistryItemNotFoundError) {
|
||||
return Response.json(
|
||||
{ error: `Registry item "${name}" was not found.` },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
console.error(error)
|
||||
|
||||
return Response.json(
|
||||
{ error: "Failed to load registry item." },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both loaders resolve `include` before returning JSON, so route handlers can use
|
||||
the same source `registry.json` structure without running `shadcn build`.
|
||||
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="content-negotiation">
|
||||
<AccordionTrigger>Content negotiation</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
||||
The `shadcn` CLI supports **HTTP Content Negotiation**. This allows you to host your registry at any endpoint — including the root of your domain — and serve different content depending on who is asking.
|
||||
|
||||
From a single URL, you can serve:
|
||||
|
||||
- **HTML** to browsers — a landing page, documentation, or marketing site.
|
||||
- **JSON** to the `shadcn` CLI — an installable registry item.
|
||||
- **Markdown** to AI agents and LLMs — a machine-readable version of your content.
|
||||
<ul>
|
||||
<li>
|
||||
<strong>HTML</strong> to browsers — a landing page, documentation, or
|
||||
marketing site.
|
||||
</li>
|
||||
<li>
|
||||
<strong>JSON</strong> to the <code>shadcn</code> CLI — an installable
|
||||
registry item.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Markdown</strong> to AI agents and LLMs — a machine-readable version
|
||||
of your content.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
The client signals its preference using the `Accept` request header, and your server decides what to return.
|
||||
|
||||
### Request headers
|
||||
#### Request headers
|
||||
|
||||
When the CLI makes a request to a registry, it sends the following headers:
|
||||
|
||||
- **User-Agent**: `shadcn`
|
||||
- **Accept**: `application/vnd.shadcn.v1+json, application/json;q=0.9`
|
||||
|
||||
### Root hosting
|
||||
#### Root hosting
|
||||
|
||||
By checking these headers on your server, you can route CLI traffic to an installable registry item while keeping browser traffic flowing to your documentation or homepage.
|
||||
|
||||
@@ -240,34 +466,179 @@ app.get("/", (req, res) => {
|
||||
|
||||
This enables:
|
||||
|
||||
- **Branded Registry URLs**: `shadcn add https://ui.example.com`
|
||||
- **Shorter URLs**: Users type your domain root, not `/r/` or `/registry/` sub-paths.
|
||||
- **Easy Mnemonics**: Easier for users to remember and share your registry.
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Branded Registry URLs</strong>:{" "}
|
||||
<code>shadcn add https://ui.example.com</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Shorter URLs</strong>: Users type your domain root, not{" "}
|
||||
<code>/r/</code> or <code>/registry/</code> sub-paths.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Easy Mnemonics</strong>: Easier for users to remember and share your
|
||||
registry.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</AccordionContent>
|
||||
|
||||
</AccordionItem>
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Test your registry
|
||||
|
||||
After your registry is being served, test it with the same CLI commands that
|
||||
other developers will use.
|
||||
|
||||
### Using URL
|
||||
|
||||
Use the catalog URL for commands that discover items, like `list` and `search`.
|
||||
Use item URLs for commands that read or install a specific item, like `view` and
|
||||
`add`.
|
||||
|
||||
#### List items
|
||||
|
||||
Start by confirming that the registry catalog can be discovered.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest list http://localhost:3000/r/registry.json
|
||||
```
|
||||
|
||||
#### Search items
|
||||
|
||||
Search the registry by query.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest search http://localhost:3000/r/registry.json --query button
|
||||
```
|
||||
|
||||
#### View an item
|
||||
|
||||
Then view one registry item by name.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest view http://localhost:3000/r/button.json
|
||||
```
|
||||
|
||||
#### Add an item
|
||||
|
||||
To test the install flow, run `add` from a project where you want to install the
|
||||
item.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add http://localhost:3000/r/button.json
|
||||
```
|
||||
|
||||
### Using namespace
|
||||
|
||||
#### Add the registry
|
||||
|
||||
You can also test your registry with a namespace. From a project with a
|
||||
`components.json` file, add your registry URL template to the project.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest registry add @acme=http://localhost:3000/r/{name}.json
|
||||
```
|
||||
|
||||
The `{name}` placeholder must resolve to an item JSON file. For example,
|
||||
`@acme/button` resolves to `http://localhost:3000/r/button.json`. The catalog is
|
||||
still served separately at `http://localhost:3000/r/registry.json`.
|
||||
|
||||
#### List items
|
||||
|
||||
Then list the items in your registry.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest list @acme
|
||||
```
|
||||
|
||||
#### Search items
|
||||
|
||||
Search the registry by query.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest search @acme --query button
|
||||
```
|
||||
|
||||
#### View an item
|
||||
|
||||
View one registry item by name.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest view @acme/button
|
||||
```
|
||||
|
||||
#### Add an item
|
||||
|
||||
To test the install flow, run `add` from a project where you want to install the
|
||||
item.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @acme/button
|
||||
```
|
||||
|
||||
See the [Namespaced Registries](/docs/registry/namespace) docs for more
|
||||
information.
|
||||
|
||||
## Publish your registry
|
||||
|
||||
To make your registry available to other developers, you can publish it by deploying your project to a public URL.
|
||||
To make your registry available to other developers, publish your project to a
|
||||
public URL. Once deployed, users can install items directly from item URLs, or
|
||||
they can add your registry as a namespace in their project.
|
||||
|
||||
### Share namespace setup instructions
|
||||
|
||||
If you want users to install items with a namespace like `@acme/button`, tell
|
||||
them to add your registry URL template to their project. The `{name}`
|
||||
placeholder is replaced by the item name when the CLI resolves the registry
|
||||
item.
|
||||
|
||||
The template must resolve to item JSON files. For example, `@acme/button`
|
||||
resolves to `https://acme.com/r/button.json`. Your registry catalog should still
|
||||
be served separately at `https://acme.com/r/registry.json`.
|
||||
|
||||
They can add the namespace with the CLI.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest registry add @acme=https://acme.com/r/{name}.json
|
||||
```
|
||||
|
||||
Or they can add it manually under the `registries` field in their
|
||||
`components.json` file.
|
||||
|
||||
```json title="components.json" showLineNumbers
|
||||
{
|
||||
"registries": {
|
||||
"@acme": "https://acme.com/r/{name}.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Users can then consume items from your registry by namespace.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @acme/button
|
||||
```
|
||||
|
||||
### Add your namespace to the registry index
|
||||
|
||||
If your registry is open source and publicly available, you can submit your
|
||||
namespace to the official registry index. This lets users add your namespace by
|
||||
name instead of pasting the full URL template.
|
||||
|
||||
See the [Registry Index](/docs/registry/registry-index) docs for the submission
|
||||
requirements.
|
||||
|
||||
## Guidelines
|
||||
|
||||
Here are some guidelines to follow when building components for a registry.
|
||||
|
||||
- Place your registry item in the `registry/[STYLE]/[NAME]` directory. I'm using `new-york` as an example. It can be anything you want as long as it's nested under the `registry` directory.
|
||||
- The following properties are required for the block definition: `name`, `description`, `type` and `files`.
|
||||
- Place your registry item in the `registry/[STYLE]/[NAME]` directory. I'm using `default` as an example. It can be anything you want as long as it's nested under the `registry` directory.
|
||||
- For blocks, the following properties are required: `name`, `description`, `type` and `files`.
|
||||
- It is recommended to add a proper name and description to your registry item. This helps LLMs understand the component and its purpose.
|
||||
- Make sure to list all registry dependencies in `registryDependencies`. A registry dependency is the name of the component in the registry eg. `input`, `button`, `card`, etc or a URL to a registry item eg. `http://localhost:3000/r/editor.json`.
|
||||
- Make sure to list all dependencies in `dependencies`. A dependency is the name of the package in the registry eg. `zod`, `sonner`, etc. To set a version, you can use the `name@version` format eg. `zod@^3.20.0`.
|
||||
- **Imports should always use the `@/registry` path.** eg. `import { HelloWorld } from "@/registry/new-york/hello-world/hello-world"`
|
||||
- **Imports should always use the `@/registry` path.** eg. `import { HelloWorld } from "@/registry/default/hello-world/hello-world"`
|
||||
- Ideally, place your files within a registry item in `components`, `hooks`, `lib` directories.
|
||||
|
||||
## Install using the CLI
|
||||
|
||||
To install a registry item using the `shadcn` CLI, use the `add` command followed by the URL of the registry item.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add http://localhost:3000/r/hello-world.json
|
||||
```
|
||||
|
||||
See the [Namespaced
|
||||
Registries](/docs/registry/namespace) docs for more information on
|
||||
how to install registry items from a namespaced registry.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user