mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
Compare commits
197 Commits
shadcn@2.6
...
shadcn/tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a5fa409eb | ||
|
|
6567897393 | ||
|
|
2675fa3941 | ||
|
|
fbda67c88c | ||
|
|
e8674ee848 | ||
|
|
adb66f4d43 | ||
|
|
3afb46eaf6 | ||
|
|
7cd019ad36 | ||
|
|
bea7d30536 | ||
|
|
40c3ff513a | ||
|
|
89ebfdce47 | ||
|
|
b83023034a | ||
|
|
6a534d7954 | ||
|
|
ef1987ded9 | ||
|
|
77bf7d28b4 | ||
|
|
41f4f7357d | ||
|
|
bc99818e04 | ||
|
|
162ba7b13c | ||
|
|
f12db1e3a2 | ||
|
|
ce3e2b1df8 | ||
|
|
dcfe911b33 | ||
|
|
7210a4919a | ||
|
|
d198908510 | ||
|
|
b0b1cd1f0d | ||
|
|
f3d70724b6 | ||
|
|
407e9c6802 | ||
|
|
c67e630521 | ||
|
|
f494411953 | ||
|
|
a43c1d1342 | ||
|
|
607a6fd127 | ||
|
|
fbcc665b49 | ||
|
|
7ddcf31e43 | ||
|
|
3e39163b08 | ||
|
|
e311fdae04 | ||
|
|
26640d9d88 | ||
|
|
3e20c228da | ||
|
|
0810c0e1a2 | ||
|
|
1205ea5445 | ||
|
|
4430ab8bab | ||
|
|
d6716db9cc | ||
|
|
da8fa6aacd | ||
|
|
e96f9edf02 | ||
|
|
b19e9cadb2 | ||
|
|
3bb47bf914 | ||
|
|
a72fac6fde | ||
|
|
4b3186c46b | ||
|
|
e67e955f2a | ||
|
|
bf047b9824 | ||
|
|
04432835f9 | ||
|
|
77e6f28e81 | ||
|
|
f1e51ec8a1 | ||
|
|
3c525b8305 | ||
|
|
e7e844ff63 | ||
|
|
e14c55ac65 | ||
|
|
043be944ab | ||
|
|
4eb257bc14 | ||
|
|
1289192d4f | ||
|
|
75dde2e646 | ||
|
|
b9f3ce1988 | ||
|
|
cdf58be7e1 | ||
|
|
fae1a81add | ||
|
|
fc6d909ba2 | ||
|
|
590b9be610 | ||
|
|
41eb9d5c46 | ||
|
|
b7c28199be | ||
|
|
7869defd42 | ||
|
|
6daa5215cc | ||
|
|
722fb81b95 | ||
|
|
543be31722 | ||
|
|
09b90cd5c2 | ||
|
|
c95959a9b3 | ||
|
|
08820ce5ee | ||
|
|
cb96e58992 | ||
|
|
fce5926265 | ||
|
|
f7c0f81258 | ||
|
|
960b22b301 | ||
|
|
6f057c9cc3 | ||
|
|
615a32d97a | ||
|
|
bfe6e1946c | ||
|
|
baaa82e4e7 | ||
|
|
caeed7bd65 | ||
|
|
61254f0c3f | ||
|
|
3dcd797f2c | ||
|
|
b76f5cdbf7 | ||
|
|
fcb1e2ca50 | ||
|
|
df94537e0f | ||
|
|
275e3a2d59 | ||
|
|
e5402f9a20 | ||
|
|
04668da018 | ||
|
|
0805751703 | ||
|
|
9ecb19cf2e | ||
|
|
9c5eb0d20f | ||
|
|
2752ce11d8 | ||
|
|
d972caa853 | ||
|
|
00b2f0796e | ||
|
|
3ed9af5757 | ||
|
|
a4237e38f7 | ||
|
|
1178d40352 | ||
|
|
cc612359ee | ||
|
|
4d0272a659 | ||
|
|
a15534bdb7 | ||
|
|
62c41c3271 | ||
|
|
851c0fa0d1 | ||
|
|
e84c819977 | ||
|
|
64f8baf9aa | ||
|
|
4b44c6489a | ||
|
|
f9021e9388 | ||
|
|
b1e3d4b740 | ||
|
|
084fb927a1 | ||
|
|
7304ef2105 | ||
|
|
b34f3fdc4f | ||
|
|
2ecf876fa1 | ||
|
|
dcd2c3ef14 | ||
|
|
17422714f6 | ||
|
|
fc27ba2692 | ||
|
|
f854190b53 | ||
|
|
396275e46a | ||
|
|
296feb28a2 | ||
|
|
a941287411 | ||
|
|
2e34c95c4e | ||
|
|
fed7e3bfdc | ||
|
|
4f5333ea7a | ||
|
|
b5b8deedde | ||
|
|
7d71b02fb1 | ||
|
|
b3639227d0 | ||
|
|
a4a3600757 | ||
|
|
a426fea941 | ||
|
|
6e870c3993 | ||
|
|
68aa3389de | ||
|
|
2e9ccede8f | ||
|
|
fc8927a1f9 | ||
|
|
ccfd14946b | ||
|
|
01c02b289a | ||
|
|
a80ab37483 | ||
|
|
469250115f | ||
|
|
2c164b0f22 | ||
|
|
578f83cbef | ||
|
|
07eda36b13 | ||
|
|
0eccdc9c5f | ||
|
|
0940c6aec7 | ||
|
|
e244952500 | ||
|
|
0e3d6b24d3 | ||
|
|
cef5af9ed3 | ||
|
|
6deb0fdbb6 | ||
|
|
e9ae79f874 | ||
|
|
d891132f2a | ||
|
|
873f7f2773 | ||
|
|
e6778dee87 | ||
|
|
97a8de1c1b | ||
|
|
19d7fbb731 | ||
|
|
a9ab05ad83 | ||
|
|
6ac114ae68 | ||
|
|
d5770e4350 | ||
|
|
4730276256 | ||
|
|
4e04567b07 | ||
|
|
6f63b04d28 | ||
|
|
e38228b574 | ||
|
|
8807103586 | ||
|
|
3424ab709e | ||
|
|
4a86a55cac | ||
|
|
2926574d0e | ||
|
|
20e913d8e1 | ||
|
|
3433aaffaa | ||
|
|
d9cdc3f7ae | ||
|
|
e75e7b3866 | ||
|
|
ed5237c231 | ||
|
|
f85ca066dc | ||
|
|
54e66d4450 | ||
|
|
6c341c16ae | ||
|
|
06d03d64f4 | ||
|
|
6407a3b330 | ||
|
|
96b15f6090 | ||
|
|
2fe9cf6d26 | ||
|
|
728cb4cfa5 | ||
|
|
db93787712 | ||
|
|
1cdd6c1645 | ||
|
|
4983c6e1f4 | ||
|
|
7443edcfb0 | ||
|
|
9d9a33be52 | ||
|
|
d544a7f7a5 | ||
|
|
48fe0d709f | ||
|
|
ed244ea0b5 | ||
|
|
b8fede1742 | ||
|
|
84d6c83bad | ||
|
|
5b8ee41511 | ||
|
|
7c3d34cdc9 | ||
|
|
56c4c83511 | ||
|
|
2821cb0e39 | ||
|
|
3c87402de2 | ||
|
|
20a88e1f15 | ||
|
|
cb19ab8464 | ||
|
|
cf1851ca09 | ||
|
|
c86c27a2ff | ||
|
|
8847126c65 | ||
|
|
65350857a4 | ||
|
|
40c7473c7e | ||
|
|
4698ee960f |
@@ -7,5 +7,5 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["www", "v4"]
|
||||
"ignore": ["www", "v4", "tests"]
|
||||
}
|
||||
|
||||
5
.changeset/fix-universal-registry-item-empty-files.md
Normal file
5
.changeset/fix-universal-registry-item-empty-files.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
Fix support for universal registry items that only have dependencies without files
|
||||
5
.changeset/hot-feet-beam.md
Normal file
5
.changeset/hot-feet-beam.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
add support for color as var
|
||||
5
.changeset/lazy-mails-wear.md
Normal file
5
.changeset/lazy-mails-wear.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
fix adding registry item with CSS at-property
|
||||
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm test:*)",
|
||||
"Bash(npm run typecheck:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
75
.github/workflows/deprecated.yml
vendored
Normal file
75
.github/workflows/deprecated.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: |
|
||||
apps/www/**
|
||||
files_ignore: |
|
||||
apps/www/public/r/**
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Comment on PR if www files changed
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
|
||||
const wwwFiles = changedFiles.filter(file =>
|
||||
file.startsWith('apps/www/') &&
|
||||
!file.startsWith('apps/www/public/r/')
|
||||
);
|
||||
|
||||
if (wwwFiles.length > 0) {
|
||||
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
|
||||
|
||||
Consider applying the change to \`apps/v4\` if relevant.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
// Add deprecated label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['deprecated']
|
||||
});
|
||||
} else {
|
||||
// Remove deprecated label if no www files are changed
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'deprecated'
|
||||
});
|
||||
} catch (error) {
|
||||
// Label doesn't exist, which is fine
|
||||
console.log('Deprecated label not found, skipping removal');
|
||||
}
|
||||
}
|
||||
20
.github/workflows/issue-stale.yml
vendored
20
.github/workflows/issue-stale.yml
vendored
@@ -18,15 +18,15 @@ jobs:
|
||||
repo-token: ${{ secrets.STALE_TOKEN }}
|
||||
ascending: true
|
||||
days-before-issue-close: 7
|
||||
days-before-issue-stale: 365 # ~2 years
|
||||
days-before-issue-stale: 365
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
remove-issue-stale-when-updated: true
|
||||
stale-issue-label: "stale?"
|
||||
exempt-issue-labels: "roadmap,next,bug"
|
||||
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you."
|
||||
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding!"
|
||||
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
|
||||
exempt-issue-labels: "roadmap,next"
|
||||
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
|
||||
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding! (This is an automated message)"
|
||||
operations-per-run: 300
|
||||
- uses: actions/stale@v9
|
||||
id: pr-state
|
||||
name: "Mark stale PRs, close stale PRs"
|
||||
@@ -36,10 +36,10 @@ jobs:
|
||||
days-before-issue-close: -1
|
||||
days-before-issue-stale: -1
|
||||
days-before-pr-close: 7
|
||||
days-before-pr-stale: 365 # PRs with no activity in over 90 days will be marked as stale
|
||||
days-before-pr-stale: 365
|
||||
remove-pr-stale-when-updated: true
|
||||
exempt-pr-labels: "roadmap,nex,awaiting-approval,work-in-progress"
|
||||
exempt-pr-labels: "roadmap,next,bug"
|
||||
stale-pr-label: "stale?"
|
||||
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you."
|
||||
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding!"
|
||||
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
|
||||
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
|
||||
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding! (This is an automated message)"
|
||||
operations-per-run: 300
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Create Version PR or Publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1.4.1
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
commit: "chore(release): version packages"
|
||||
title: "chore(release): version packages"
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -8,6 +8,9 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm test
|
||||
env:
|
||||
NEXT_PUBLIC_APP_URL: http://localhost:4000
|
||||
NEXT_PUBLIC_V0_URL: https://v0.dev
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -39,4 +42,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build --filter=shadcn
|
||||
|
||||
- run: pnpm test
|
||||
|
||||
54
.github/workflows/validate-registries.yml
vendored
Normal file
54
.github/workflows/validate-registries.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Validate Registries
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm validate:registries
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 9.0.6
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build --filter=shadcn
|
||||
|
||||
- name: Validate registries
|
||||
run: pnpm --filter=v4 validate:registries
|
||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -3,15 +3,13 @@
|
||||
{ "pattern": "apps/*/" },
|
||||
{ "pattern": "packages/*/" }
|
||||
],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]?([^\"'`]+)[\"'`]?"],
|
||||
["cn\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
// "cva\\(([^)]*)\\)",
|
||||
// "[\"'`]([^\"'`]*).*?[\"'`]"
|
||||
],
|
||||
"tailwindCSS.classFunctions": ["cva", "cn"],
|
||||
"vitest.debugExclude": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/fixtures/**"
|
||||
]
|
||||
],
|
||||
"files.exclude": {
|
||||
"apps/www": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ To run the CLI locally, you can follow the workflow:
|
||||
1. Start by running the registry (main site) to make sure the components are up to date:
|
||||
|
||||
```bash
|
||||
pnpm www:dev
|
||||
pnpm v4:dev
|
||||
```
|
||||
|
||||
2. Run the development script for the CLI:
|
||||
|
||||
2
apps/v4/.env.example
Normal file
2
apps/v4/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_V0_URL=https://v0.dev
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:4000
|
||||
1
apps/v4/.gitignore
vendored
1
apps/v4/.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
168
apps/v4/app/(app)/(root)/components/appearance-settings.tsx
Normal file
168
apps/v4/app/(app)/(root)/components/appearance-settings.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
"use client"
|
||||
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { useThemeConfig } from "@/components/active-theme"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
const accents = [
|
||||
{
|
||||
name: "Blue",
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
name: "Amber",
|
||||
value: "amber",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
value: "green",
|
||||
},
|
||||
{
|
||||
name: "Rose",
|
||||
value: "rose",
|
||||
},
|
||||
]
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const { activeTheme, setActiveTheme } = useThemeConfig()
|
||||
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>
|
||||
<FieldTitle>Accent</FieldTitle>
|
||||
<FieldDescription>Select the accent color.</FieldDescription>
|
||||
</FieldContent>
|
||||
<FieldSet aria-label="Accent">
|
||||
<RadioGroup
|
||||
className="flex flex-wrap gap-2"
|
||||
value={activeTheme}
|
||||
onValueChange={setActiveTheme}
|
||||
>
|
||||
{accents.map((accent) => (
|
||||
<Label
|
||||
htmlFor={accent.value}
|
||||
key={accent.value}
|
||||
data-theme={accent.value}
|
||||
className="flex size-6 items-center justify-center rounded-full data-[theme=amber]:bg-amber-600 data-[theme=blue]:bg-blue-700 data-[theme=green]:bg-green-600 data-[theme=rose]:bg-rose-600"
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={accent.value}
|
||||
value={accent.value}
|
||||
aria-label={accent.name}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<CheckIcon className="hidden size-4 stroke-white peer-data-[state=checked]:block" />
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</Field>
|
||||
<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"
|
||||
placeholder="8"
|
||||
size={3}
|
||||
className="h-8 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Decrement"
|
||||
>
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Increment"
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
120
apps/v4/app/(app)/(root)/components/button-group-demo.tsx
Normal file
120
apps/v4/app/(app)/(root)/components/button-group-demo.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterPlusIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/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 [--radius:1rem]">
|
||||
<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>
|
||||
<ListFilterPlusIcon />
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/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>
|
||||
)
|
||||
}
|
||||
32
apps/v4/app/(app)/(root)/components/button-group-nested.tsx
Normal file
32
apps/v4/app/(app)/(root)/components/button-group-nested.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/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>
|
||||
)
|
||||
}
|
||||
45
apps/v4/app/(app)/(root)/components/button-group-popover.tsx
Normal file
45
apps/v4/app/(app)/(root)/components/button-group-popover.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Textarea } from "@/registry/new-york-v4/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="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>
|
||||
)
|
||||
}
|
||||
57
apps/v4/app/(app)/(root)/components/empty-avatar-group.tsx
Normal file
57
apps/v4/app/(app)/(root)/components/empty-avatar-group.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
<Empty className="flex-none border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]: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>
|
||||
</div>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
43
apps/v4/app/(app)/(root)/components/empty-input-group.tsx
Normal file
43
apps/v4/app/(app)/(root)/components/empty-input-group.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
15
apps/v4/app/(app)/(root)/components/field-checkbox.tsx
Normal file
15
apps/v4/app/(app)/(root)/components/field-checkbox.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/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>
|
||||
)
|
||||
}
|
||||
62
apps/v4/app/(app)/(root)/components/field-choice-card.tsx
Normal file
62
apps/v4/app/(app)/(root)/components/field-choice-card.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
153
apps/v4/app/(app)/(root)/components/field-demo.tsx
Normal file
153
apps/v4/app/(app)/(root)/components/field-demo.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-md rounded-lg 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>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
72
apps/v4/app/(app)/(root)/components/field-hear.tsx
Normal file
72
apps/v4/app/(app)/(root)/components/field-hear.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/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>
|
||||
)
|
||||
}
|
||||
35
apps/v4/app/(app)/(root)/components/field-slider.tsx
Normal file
35
apps/v4/app/(app)/(root)/components/field-slider.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Slider } from "@/registry/new-york-v4/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>
|
||||
)
|
||||
}
|
||||
52
apps/v4/app/(app)/(root)/components/index.tsx
Normal file
52
apps/v4/app/(app)/(root)/components/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { FieldSeparator } from "@/registry/new-york-v4/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="theme-container mx-auto grid gap-8 py-1 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>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>
|
||||
)
|
||||
}
|
||||
68
apps/v4/app/(app)/(root)/components/input-group-button.tsx
Normal file
68
apps/v4/app/(app)/(root)/components/input-group-button.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/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="text-muted-foreground !pl-1">
|
||||
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>
|
||||
)
|
||||
}
|
||||
102
apps/v4/app/(app)/(root)/components/input-group-demo.tsx
Normal file
102
apps/v4/app/(app)/(root)/components/input-group-demo.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/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"
|
||||
className="[--radius:0.95rem]"
|
||||
>
|
||||
<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="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
apps/v4/app/(app)/(root)/components/input-group-textarea.tsx
Normal file
46
apps/v4/app/(app)/(root)/components/input-group-textarea.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
78
apps/v4/app/(app)/(root)/components/item-avatar.tsx
Normal file
78
apps/v4/app/(app)/(root)/components/item-avatar.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
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="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *: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>
|
||||
)
|
||||
}
|
||||
42
apps/v4/app/(app)/(root)/components/item-demo.tsx
Normal file
42
apps/v4/app/(app)/(root)/components/item-demo.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
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 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>
|
||||
)
|
||||
}
|
||||
456
apps/v4/app/(app)/(root)/components/notion-prompt-form.tsx
Normal file
456
apps/v4/app/(app)/(root)/components/notion-prompt-form.tsx
Normal file
@@ -0,0 +1,456 @@
|
||||
"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 "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/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 className="[--radius:1.2rem]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onFocusCapture={(e) => e.stopPropagation()}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" 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)
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 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="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-42">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
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="[--radius:1rem]"
|
||||
>
|
||||
<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-muted-foreground text-xs">
|
||||
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>
|
||||
)
|
||||
}
|
||||
21
apps/v4/app/(app)/(root)/components/spinner-badge.tsx
Normal file
21
apps/v4/app/(app)/(root)/components/spinner-badge.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 [--radius:1.2rem]">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Syncing
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Updating
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Loading
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
apps/v4/app/(app)/(root)/components/spinner-empty.tsx
Normal file
31
apps/v4/app/(app)/(root)/components/spinner-empty.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Spinner } from "@/registry/new-york-v4/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>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import { CardsDemo } from "@/components/cards"
|
||||
import { ExamplesNav } from "@/components/examples-nav"
|
||||
import {
|
||||
PageActions,
|
||||
@@ -15,9 +14,11 @@ import { PageNav } from "@/components/page-nav"
|
||||
import { ThemeSelector } from "@/components/theme-selector"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
const title = "Build your Component Library"
|
||||
import { RootComponents } from "./components"
|
||||
|
||||
const title = "The Foundation for your Design System"
|
||||
const description =
|
||||
"A set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks. Open Source. Open Code."
|
||||
"A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code."
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
@@ -51,14 +52,14 @@ export default function IndexPage() {
|
||||
<div className="flex flex-1 flex-col">
|
||||
<PageHeader>
|
||||
<Announcement />
|
||||
<PageHeaderHeading>{title}</PageHeaderHeading>
|
||||
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm">
|
||||
<Link href="/docs/installation">Get Started</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost">
|
||||
<Link href="/blocks">Browse Blocks</Link>
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
@@ -87,7 +88,7 @@ export default function IndexPage() {
|
||||
/>
|
||||
</section>
|
||||
<section className="theme-container hidden md:block">
|
||||
<CardsDemo />
|
||||
<RootComponents />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { findNeighbour } from "fumadocs-core/server"
|
||||
|
||||
import { source } from "@/lib/source"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { DocsCopyPage } from "@/components/docs-copy-page"
|
||||
import { DocsTableOfContents } from "@/components/docs-toc"
|
||||
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
@@ -102,12 +103,17 @@ export default async function Page(props: {
|
||||
<h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
|
||||
{doc.title}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 pt-1.5">
|
||||
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:pt-1.5 sm:backdrop-blur-none">
|
||||
<DocsCopyPage
|
||||
// @ts-expect-error - revisit fumadocs types.
|
||||
page={doc.content}
|
||||
url={absoluteUrl(page.url)}
|
||||
/>
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="extend-touch-target size-8 shadow-none md:size-7"
|
||||
className="extend-touch-target ml-auto size-8 shadow-none md:size-7"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.previous.url}>
|
||||
@@ -138,19 +144,19 @@ export default async function Page(props: {
|
||||
)}
|
||||
</div>
|
||||
{links ? (
|
||||
<div className="flex items-center space-x-2 pt-4">
|
||||
<div className="flex items-center gap-2 pt-4">
|
||||
{links?.doc && (
|
||||
<Badge asChild variant="secondary">
|
||||
<Link href={links.doc} target="_blank" rel="noreferrer">
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<a href={links.doc} target="_blank" rel="noreferrer">
|
||||
Docs <IconArrowUpRight />
|
||||
</Link>
|
||||
</a>
|
||||
</Badge>
|
||||
)}
|
||||
{links?.api && (
|
||||
<Badge asChild variant="secondary">
|
||||
<Link href={links.api} target="_blank" rel="noreferrer">
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<a href={links.api} target="_blank" rel="noreferrer">
|
||||
API Reference <IconArrowUpRight />
|
||||
</Link>
|
||||
</a>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -160,7 +166,7 @@ export default async function Page(props: {
|
||||
<MDX components={mdxComponents} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto flex h-16 w-full max-w-2xl items-center gap-2 px-4 md:px-0">
|
||||
<div className="mx-auto hidden h-16 w-full max-w-2xl items-center gap-2 px-4 sm:flex md:px-0">
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -187,7 +193,7 @@ export default async function Page(props: {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--header-height)-var(--footer-height))] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
|
||||
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
{/* @ts-expect-error - revisit fumadocs types. */}
|
||||
{doc.toc?.length ? (
|
||||
|
||||
@@ -5,8 +5,14 @@ import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function UserAuthForm({
|
||||
className,
|
||||
@@ -26,11 +32,11 @@ export function UserAuthForm({
|
||||
return (
|
||||
<div className={cn("grid gap-6", className)} {...props}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-1">
|
||||
<Label className="sr-only" htmlFor="email">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel className="sr-only" htmlFor="email">
|
||||
Email
|
||||
</Label>
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
@@ -40,31 +46,18 @@ export function UserAuthForm({
|
||||
autoCorrect="off"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && (
|
||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Sign In with Email
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && <Spinner />}
|
||||
Sign In with Email
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background text-muted-foreground px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<FieldSeparator>Or continue with</FieldSeparator>
|
||||
<Button variant="outline" type="button" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Icons.gitHub className="mr-2 h-4 w-4" />
|
||||
)}{" "}
|
||||
{isLoading ? <Spinner /> : <Icons.gitHub className="mr-2 h-4 w-4" />}{" "}
|
||||
GitHub
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from "next/link"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/registry/new-york-v4/ui/button"
|
||||
import { FieldDescription } from "@/registry/new-york-v4/ui/field"
|
||||
import { UserAuthForm } from "@/app/(app)/examples/authentication/components/user-auth-form"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -78,23 +79,11 @@ export default function AuthenticationPage() {
|
||||
</p>
|
||||
</div>
|
||||
<UserAuthForm />
|
||||
<p className="text-muted-foreground px-8 text-center text-sm">
|
||||
<FieldDescription className="px-6 text-center">
|
||||
By clicking continue, you agree to our{" "}
|
||||
<Link
|
||||
href="/terms"
|
||||
className="hover:text-primary underline underline-offset-4"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="hover:text-primary underline underline-offset-4"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<Link href="/terms">Terms of Service</Link> and{" "}
|
||||
<Link href="/privacy">Privacy Policy</Link>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,13 +142,7 @@ const chartConfig = {
|
||||
|
||||
export function ChartAreaInteractive() {
|
||||
const isMobile = useIsMobile()
|
||||
const [timeRange, setTimeRange] = React.useState("90d")
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMobile) {
|
||||
setTimeRange("7d")
|
||||
}
|
||||
}, [isMobile])
|
||||
const [timeRange, setTimeRange] = React.useState("7d")
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
|
||||
@@ -75,7 +75,7 @@ export function DataTable<TData, TValue>({
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<DataTableToolbar table={table} />
|
||||
<div className="rounded-md border">
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
|
||||
32
apps/v4/app/(app)/llm/[[...slug]]/route.ts
Normal file
32
apps/v4/app/(app)/llm/[[...slug]]/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { NextResponse, type NextRequest } from "next/server"
|
||||
|
||||
import { processMdxForLLMs } from "@/lib/llm"
|
||||
import { source } from "@/lib/source"
|
||||
|
||||
export const revalidate = false
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ slug: string[] }> }
|
||||
) {
|
||||
const slug = (await params).slug
|
||||
const page = source.getPage(slug)
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
// @ts-expect-error - revisit fumadocs types.
|
||||
const processedContent = processMdxForLLMs(page.data.content)
|
||||
|
||||
return new NextResponse(processedContent, {
|
||||
headers: {
|
||||
"Content-Type": "text/markdown; charset=utf-8",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.generateParams()
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import Image from "next/image"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
const modes = [
|
||||
{
|
||||
name: "Light",
|
||||
value: "light",
|
||||
image: "/placeholder.svg",
|
||||
},
|
||||
{
|
||||
name: "Dark",
|
||||
value: "dark",
|
||||
image: "/placeholder.svg",
|
||||
},
|
||||
{
|
||||
name: "System",
|
||||
value: "system",
|
||||
image: "/placeholder.svg",
|
||||
},
|
||||
]
|
||||
|
||||
const accents = [
|
||||
{
|
||||
name: "Blue",
|
||||
value: "#007AFF",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
value: "#6A4695",
|
||||
},
|
||||
{
|
||||
name: "Red",
|
||||
value: "#FF3B30",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
value: "#FF9500",
|
||||
},
|
||||
]
|
||||
|
||||
export function AppearanceSettings() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Appearance</FieldLegend>
|
||||
<FieldDescription>
|
||||
Configure appearance. accent, scroll bar, and more.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Mode</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the mode to use for the appearance.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
className="flex flex-col gap-4 @min-[28rem]/field-group:grid @min-[28rem]/field-group:grid-cols-3"
|
||||
defaultValue="light"
|
||||
>
|
||||
{modes.map((mode) => (
|
||||
<FieldLabel
|
||||
htmlFor={mode.value}
|
||||
className="gap-0 overflow-hidden"
|
||||
key={mode.value}
|
||||
>
|
||||
<Image
|
||||
src={mode.image}
|
||||
alt={mode.name}
|
||||
width={160}
|
||||
height={90}
|
||||
className="hidden aspect-video w-full object-cover @min-[28rem]/field-group:block dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="@min-[28rem]/field-group:border-t-input @min-[28rem]/field-group:border-t"
|
||||
>
|
||||
<FieldTitle>{mode.name}</FieldTitle>
|
||||
<RadioGroupItem id={mode.value} value={mode.value} />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Accent</FieldTitle>
|
||||
<FieldDescription>
|
||||
Select the accent color to use for the appearance.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<FieldSet aria-label="Accent">
|
||||
<RadioGroup className="flex flex-wrap gap-2" defaultValue="#007AFF">
|
||||
{accents.map((accent) => (
|
||||
<Label
|
||||
htmlFor={accent.value}
|
||||
key={accent.value}
|
||||
className="flex size-6 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: accent.value }}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={accent.value}
|
||||
value={accent.value}
|
||||
aria-label={accent.name}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<CheckIcon className="hidden size-4 stroke-white peer-data-[state=checked]:block" />
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="icon-size">Sidebar Icon Size</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the size of the sidebar icons.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="icon-size" className="ml-auto">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="small">Small</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="large">Large</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow the wallpaper to be tinted with the accent color.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="tinting" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
463
apps/v4/app/(internal)/sink/(pages)/forms/chat-settings.tsx
Normal file
463
apps/v4/app/(internal)/sink/(pages)/forms/chat-settings.tsx
Normal file
@@ -0,0 +1,463 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { CircleIcon, InfoIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const spokenLanguages = [
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "Spanish", value: "es" },
|
||||
{ label: "French", value: "fr" },
|
||||
{ label: "German", value: "de" },
|
||||
{ label: "Italian", value: "it" },
|
||||
{ label: "Portuguese", value: "pt" },
|
||||
{ label: "Russian", value: "ru" },
|
||||
{ label: "Chinese", value: "zh" },
|
||||
{ label: "Japanese", value: "ja" },
|
||||
{ label: "Korean", value: "ko" },
|
||||
{ label: "Arabic", value: "ar" },
|
||||
{ label: "Hindi", value: "hi" },
|
||||
{ label: "Bengali", value: "bn" },
|
||||
{ label: "Telugu", value: "te" },
|
||||
{ label: "Marathi", value: "mr" },
|
||||
{ label: "Kannada", value: "kn" },
|
||||
{ label: "Malayalam", value: "ml" },
|
||||
]
|
||||
|
||||
const voices = [
|
||||
{ label: "Samantha", value: "samantha" },
|
||||
{ label: "Alex", value: "alex" },
|
||||
{ label: "Fred", value: "fred" },
|
||||
{ label: "Victoria", value: "victoria" },
|
||||
{ label: "Tom", value: "tom" },
|
||||
{ label: "Karen", value: "karen" },
|
||||
{ label: "Sam", value: "sam" },
|
||||
{ label: "Daniel", value: "daniel" },
|
||||
]
|
||||
|
||||
const personalities = [
|
||||
{
|
||||
label: "Friendly",
|
||||
value: "friendly",
|
||||
description: "Friendly and approachable.",
|
||||
},
|
||||
{
|
||||
label: "Professional",
|
||||
value: "professional",
|
||||
description: "Professional and authoritative.",
|
||||
},
|
||||
{ label: "Funny", value: "funny", description: "Funny and light-hearted." },
|
||||
{
|
||||
label: "Sarcastic",
|
||||
value: "sarcastic",
|
||||
description: "Sarcastic and witty.",
|
||||
},
|
||||
{ label: "Cynical", value: "cynical", description: "Cynical and skeptical." },
|
||||
]
|
||||
|
||||
const instructions = [
|
||||
{
|
||||
label: "Witty",
|
||||
value: "witty",
|
||||
description: "Use quick and clever responses when appropriate.",
|
||||
},
|
||||
{
|
||||
label: "Professional",
|
||||
value: "professional",
|
||||
description: "Have a professional and authoritative tone.",
|
||||
},
|
||||
{
|
||||
label: "Funny",
|
||||
value: "funny",
|
||||
description: "Use humor and wit to engage the user.",
|
||||
},
|
||||
{
|
||||
label: "Sarcastic",
|
||||
value: "sarcastic",
|
||||
description: "Use sarcasm and wit to engage the user.",
|
||||
},
|
||||
{
|
||||
label: "Cynical",
|
||||
value: "cynical",
|
||||
description: "Use cynicism and skepticism to engage the user.",
|
||||
},
|
||||
]
|
||||
|
||||
export function ChatSettings() {
|
||||
const [tab, setTab] = useState("general")
|
||||
const [theme, setTheme] = useState("system")
|
||||
const [accentColor, setAccentColor] = useState("default")
|
||||
const [spokenLanguage, setSpokenLanguage] = useState("en")
|
||||
const [voice, setVoice] = useState("samantha")
|
||||
const [personality, setPersonality] = useState("friendly")
|
||||
const [customInstructions, setCustomInstructions] = useState("")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button variant="outline" asChild className="w-full md:hidden">
|
||||
<select
|
||||
value={tab}
|
||||
onChange={(e) => setTab(e.target.value)}
|
||||
className="appearance-none"
|
||||
>
|
||||
<option value="general">General</option>
|
||||
<option value="notifications">Notifications</option>
|
||||
<option value="personalization">Personalization</option>
|
||||
<option value="security">Security</option>
|
||||
</select>
|
||||
</Button>
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList className="hidden md:flex">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
||||
<TabsTrigger value="personalization">Personalization</TabsTrigger>
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="rounded-lg border p-6 [&_[data-slot=select-trigger]]:min-w-[125px]">
|
||||
<TabsContent value="general">
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="theme">Theme</FieldLabel>
|
||||
<Select value={theme} onValueChange={setTheme}>
|
||||
<SelectTrigger id="theme">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="accent-color">Accent Color</FieldLabel>
|
||||
<Select value={accentColor} onValueChange={setAccentColor}>
|
||||
<SelectTrigger id="accent-color">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="default">
|
||||
<CircleIcon className="fill-neutral-500 stroke-neutral-500 dark:fill-neutral-400 dark:stroke-neutral-400" />
|
||||
Default
|
||||
</SelectItem>
|
||||
<SelectItem value="red">
|
||||
<CircleIcon className="fill-red-500 stroke-red-500 dark:fill-red-400 dark:stroke-red-400" />
|
||||
Red
|
||||
</SelectItem>
|
||||
<SelectItem value="blue">
|
||||
<CircleIcon className="fill-blue-500 stroke-blue-500 dark:fill-blue-400 dark:stroke-blue-400" />
|
||||
Blue
|
||||
</SelectItem>
|
||||
<SelectItem value="green">
|
||||
<CircleIcon className="fill-green-500 stroke-green-500 dark:fill-green-400 dark:stroke-green-400" />
|
||||
Green
|
||||
</SelectItem>
|
||||
<SelectItem value="purple">
|
||||
<CircleIcon className="fill-purple-500 stroke-purple-500 dark:fill-purple-400 dark:stroke-purple-400" />
|
||||
Purple
|
||||
</SelectItem>
|
||||
<SelectItem value="pink">
|
||||
<CircleIcon className="fill-pink-500 stroke-pink-500 dark:fill-pink-400 dark:stroke-pink-400" />
|
||||
Pink
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="spoken-language">
|
||||
Spoken Language
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
For best results, select the language you mainly speak. If
|
||||
it's not listed, it may still be supported via
|
||||
auto-detection.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select
|
||||
value={spokenLanguage}
|
||||
onValueChange={setSpokenLanguage}
|
||||
>
|
||||
<SelectTrigger id="spoken-language">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" position="item-aligned">
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectSeparator />
|
||||
{spokenLanguages.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="voice">Voice</FieldLabel>
|
||||
<Select value={voice} onValueChange={setVoice}>
|
||||
<SelectTrigger id="voice">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" position="item-aligned">
|
||||
{voices.map((voice) => (
|
||||
<SelectItem key={voice.value} value={voice.value}>
|
||||
{voice.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</TabsContent>
|
||||
<TabsContent value="notifications">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLabel>Responses</FieldLabel>
|
||||
<FieldDescription>
|
||||
Get notified when ChatGPT responds to requests that take time,
|
||||
like research or image generation.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="push" defaultChecked disabled />
|
||||
<FieldLabel htmlFor="push" className="font-normal">
|
||||
Push notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldLabel>Tasks</FieldLabel>
|
||||
<FieldDescription>
|
||||
Get notified when tasks you've created have updates.{" "}
|
||||
<a href="#">Manage tasks</a>
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="push-tasks" />
|
||||
<FieldLabel htmlFor="push-tasks" className="font-normal">
|
||||
Push notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="email-tasks" />
|
||||
<FieldLabel htmlFor="email-tasks" className="font-normal">
|
||||
Email notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</TabsContent>
|
||||
<TabsContent value="personalization">
|
||||
<FieldGroup>
|
||||
<Field orientation="responsive">
|
||||
<FieldLabel htmlFor="nickname">Nickname</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="nickname"
|
||||
placeholder="Broski"
|
||||
className="@md/field-group:max-w-[200px]"
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton size="icon-xs">
|
||||
<InfoIcon />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-2">
|
||||
Used to identify you in the chat. <Kbd>N</Kbd>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field
|
||||
orientation="responsive"
|
||||
className="@md/field-group:flex-col @2xl/field-group:flex-row"
|
||||
>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="about">More about you</FieldLabel>
|
||||
<FieldDescription>
|
||||
Tell us more about yourself. This will be used to help us
|
||||
personalize your experience.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Textarea
|
||||
id="about"
|
||||
placeholder="I'm a software engineer..."
|
||||
className="min-h-[120px] @md/field-group:min-w-full @2xl/field-group:min-w-[300px]"
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldLabel>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="customization">
|
||||
Enable customizations
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable customizations to make ChatGPT more personalized.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="customization" defaultChecked />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="personality">
|
||||
ChatGPT Personality
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Set the style and tone ChatGPT should use when responding.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select value={personality} onValueChange={setPersonality}>
|
||||
<SelectTrigger id="personality">
|
||||
{personalities.find((p) => p.value === personality)?.label}
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{personalities.map((personality) => (
|
||||
<SelectItem
|
||||
key={personality.value}
|
||||
value={personality.value}
|
||||
>
|
||||
<FieldContent className="gap-0.5">
|
||||
<FieldLabel>{personality.label}</FieldLabel>
|
||||
<FieldDescription className="text-xs">
|
||||
{personality.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field>
|
||||
<FieldLabel htmlFor="instructions">
|
||||
Custom Instructions
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="instructions"
|
||||
value={customInstructions}
|
||||
onChange={(e) => setCustomInstructions(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{instructions.map((instruction) => (
|
||||
<Button
|
||||
variant="outline"
|
||||
key={instruction.value}
|
||||
value={instruction.value}
|
||||
className="rounded-full"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setCustomInstructions(
|
||||
`${customInstructions} ${instruction.description}`
|
||||
)
|
||||
}
|
||||
>
|
||||
{instruction.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</TabsContent>
|
||||
<TabsContent value="security">
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="2fa">
|
||||
Multi-factor authentication
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable multi-factor authentication to secure your account.
|
||||
If you do not have a two-factor authentication device, you
|
||||
can use a one-time code sent to your email.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="2fa" />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Log out</FieldTitle>
|
||||
<FieldDescription>
|
||||
Log out of your account on this device.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Log Out
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Log out of all devices</FieldTitle>
|
||||
<FieldDescription>
|
||||
This will log you out of all devices, including the current
|
||||
session. It may take up to 30 minutes for the changes to
|
||||
take effect.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Button variant="outline" size="sm">
|
||||
Log Out All
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
137
apps/v4/app/(internal)/sink/(pages)/forms/display-settings.tsx
Normal file
137
apps/v4/app/(internal)/sink/(pages)/forms/display-settings.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { SunDimIcon, SunIcon } from "lucide-react"
|
||||
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function DisplaySettings() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Display</FieldLegend>
|
||||
<FieldDescription>
|
||||
Configure display settings, brightness, refresh rate, and more.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="resolution">Resolution</FieldLabel>
|
||||
<FieldDescription>Select the display resolution.</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="resolution" className="ml-auto">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="1920x1080">1920 x 1080</SelectItem>
|
||||
<SelectItem value="2560x1440">2560 x 1440</SelectItem>
|
||||
<SelectItem value="3840x2160">3840 x 2160</SelectItem>
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldTitle>Brightness</FieldTitle>
|
||||
<FieldDescription>
|
||||
Adjust the display brightness level.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<div className="flex min-w-[150px] items-center gap-2">
|
||||
<SunDimIcon className="size-4 shrink-0" />
|
||||
<Slider
|
||||
id="brightness"
|
||||
defaultValue={[75]}
|
||||
max={100}
|
||||
step={1}
|
||||
aria-label="Brightness"
|
||||
/>
|
||||
<SunIcon className="size-4 shrink-0" />
|
||||
</div>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="auto-brightness">
|
||||
Automatically Adjust Brightness
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Automatically adjust brightness based on ambient light.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Checkbox id="auto-brightness" defaultChecked />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="true-tone">True Tone</FieldLabel>
|
||||
<FieldDescription>
|
||||
Automatically adjust colors to match ambient lighting.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="true-tone" />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="refresh-rate">Refresh Rate</FieldLabel>
|
||||
<FieldDescription>
|
||||
Select the display refresh rate.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="refresh-rate" className="ml-auto min-w-[200px]">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="60hz">60 Hz</SelectItem>
|
||||
<SelectItem value="120hz">120 Hz</SelectItem>
|
||||
<SelectItem value="144hz">144 Hz</SelectItem>
|
||||
<SelectItem value="240hz">240 Hz</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="responsive">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tv-connection">
|
||||
When connected to TV
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Choose display behavior when connected to a TV.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Select>
|
||||
<SelectTrigger id="tv-connection" className="ml-auto">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="mirror">Mirror Display</SelectItem>
|
||||
<SelectItem value="extend">Extend Display</SelectItem>
|
||||
<SelectItem value="tv-only">TV Only</SelectItem>
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
464
apps/v4/app/(internal)/sink/(pages)/forms/notion-prompt-form.tsx
Normal file
464
apps/v4/app/(internal)/sink/(pages)/forms/notion-prompt-form.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconBrandAbstract,
|
||||
IconBrandOpenai,
|
||||
IconBrandZeit,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/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: "Cursor",
|
||||
},
|
||||
{
|
||||
type: "user",
|
||||
title: "evilrabbit",
|
||||
image: "https://github.com/evilrabbit.png",
|
||||
workspace: "Vercel",
|
||||
},
|
||||
],
|
||||
models: [
|
||||
{
|
||||
name: "Auto",
|
||||
icon: IconBrandZeit,
|
||||
},
|
||||
{
|
||||
name: "Claude Sonnet 4",
|
||||
icon: IconBrandAbstract,
|
||||
badge: "Beta",
|
||||
},
|
||||
{
|
||||
name: "GPT-5",
|
||||
icon: IconBrandOpenai,
|
||||
badge: "Beta",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
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 className="[--radius:1.2rem]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup className="bg-background dark:bg-background shadow-none">
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" 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)
|
||||
}}
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 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.icon && selectedModel.name !== "Auto" && (
|
||||
<selectedModel.icon />
|
||||
)}
|
||||
{selectedModel.name}
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1.2rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-72">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Get answers about your workspace
|
||||
</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.icon && <model.icon />}
|
||||
{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="start"
|
||||
className="[--radius:1.2rem]"
|
||||
>
|
||||
<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:1.2rem]">
|
||||
<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-muted-foreground text-xs">
|
||||
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>
|
||||
)
|
||||
}
|
||||
31
apps/v4/app/(internal)/sink/(pages)/forms/page.tsx
Normal file
31
apps/v4/app/(internal)/sink/(pages)/forms/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { AppearanceSettings } from "@/app/(internal)/sink/(pages)/forms/appearance-settings"
|
||||
import { ChatSettings } from "@/app/(internal)/sink/(pages)/forms/chat-settings"
|
||||
import { DisplaySettings } from "@/app/(internal)/sink/(pages)/forms/display-settings"
|
||||
import { NotionPromptForm } from "@/app/(internal)/sink/(pages)/forms/notion-prompt-form"
|
||||
import { ShipRegistrationForm } from "@/app/(internal)/sink/(pages)/forms/ship-registration-form"
|
||||
import { ShippingForm } from "@/app/(internal)/sink/(pages)/forms/shipping-form"
|
||||
|
||||
export default function FormsPage() {
|
||||
return (
|
||||
<div className="@container flex flex-1 flex-col gap-12 p-4">
|
||||
<div className="grid flex-1 gap-12 @3xl:grid-cols-2 @5xl:grid-cols-3 @[120rem]:grid-cols-4 [&>div]:max-w-lg">
|
||||
<div className="flex flex-col gap-12">
|
||||
<NotionPromptForm />
|
||||
<ChatSettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<AppearanceSettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<DisplaySettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
<ShippingForm />
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-col gap-12">
|
||||
<ShipRegistrationForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function ShipRegistrationForm() {
|
||||
return (
|
||||
<div className="flex max-w-md flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
Join us in SF or online on October 23
|
||||
</h1>
|
||||
<FieldDescription>
|
||||
Already signed up? <a href="#">Log in</a>
|
||||
</FieldDescription>
|
||||
</div>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>1. Select your ticket type</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select your ticket type to join us in San Francisco or online on
|
||||
October 23.
|
||||
</FieldDescription>
|
||||
<Field>
|
||||
<RadioGroup>
|
||||
<FieldLabel htmlFor="in-person">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>In Person</FieldTitle>
|
||||
<FieldDescription>
|
||||
Join us in San Francisco on October 23.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="in-person" id="in-person" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="online">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Online</FieldTitle>
|
||||
<FieldDescription>
|
||||
Join us online on October 23.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="online" id="online" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="next-conf" />
|
||||
<FieldLabel htmlFor="next-conf">
|
||||
Also sign up for Next.js Conf 2025
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend>2. Complete your attendee information</FieldLegend>
|
||||
<FieldDescription>
|
||||
By entering your information, you acknowledge that you have read
|
||||
and agree to the <a href="#">Terms of Service</a> and{" "}
|
||||
<a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="grid grid-cols-2 gap-x-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="first-name">First Name</FieldLabel>
|
||||
<Input id="first-name" placeholder="Jane" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="last-name">Last Name</FieldLabel>
|
||||
<Input id="last-name" placeholder="Doe" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" placeholder="jane.doe@example.com" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="company">Company</FieldLabel>
|
||||
<Input id="company" placeholder="Example Inc." required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="job-title">Job Title</FieldLabel>
|
||||
<Input
|
||||
id="job-title"
|
||||
placeholder="Software Engineer"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="country">Country</FieldLabel>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a country" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="us">United States</SelectItem>
|
||||
<SelectItem value="uk">United Kingdom</SelectItem>
|
||||
<SelectItem value="ca">Canada</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="topics">
|
||||
What AI-related topics are you most curious about?
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="topics"
|
||||
placeholder="Agents, Security, Improving UX/Personalization, etc."
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</Field>
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="workloads">
|
||||
What types of AI workloads are you tackling right now?
|
||||
</FieldLabel>
|
||||
<Textarea id="workloads" className="min-h-[100px]" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<FieldLegend>3. Buy your ticket</FieldLegend>
|
||||
<FieldDescription>
|
||||
Enter your card details to purchase your ticket.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="grid grid-cols-2 gap-x-4">
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="card-number">Card Number</FieldLabel>
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="expiry-date">Expiry Date</FieldLabel>
|
||||
<Input id="expiry-date" placeholder="MM/YY" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="cvv">CVV</FieldLabel>
|
||||
<Input id="cvv" placeholder="123" required />
|
||||
</Field>
|
||||
<Field className="col-span-2">
|
||||
<FieldLabel htmlFor="promo-code">Promo Code</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="promo-code" placeholder="PROMO10" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton>Apply</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<Button type="submit">Purchase Ticket</Button>
|
||||
<FieldDescription>
|
||||
By clicking Purchase Ticket, you agree to the{" "}
|
||||
<a href="#">Terms of Service</a> and{" "}
|
||||
<a href="#">Privacy Policy</a>.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
121
apps/v4/app/(internal)/sink/(pages)/forms/shipping-form.tsx
Normal file
121
apps/v4/app/(internal)/sink/(pages)/forms/shipping-form.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function ShippingForm() {
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Shipping Details</FieldLegend>
|
||||
<FieldDescription>
|
||||
Please provide your shipping details so we can deliver your order.
|
||||
</FieldDescription>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="street-address">Street Address</FieldLabel>
|
||||
<Input id="street-address" autoComplete="off" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="city">City</FieldLabel>
|
||||
<Input id="city" />
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Shipping Method</FieldLegend>
|
||||
<FieldDescription>
|
||||
Please select the shipping method for your order.
|
||||
</FieldDescription>
|
||||
<RadioGroup>
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value="standard" id="shipping-method-1" />
|
||||
<FieldLabel htmlFor="shipping-method-1" className="font-normal">
|
||||
Standard{" "}
|
||||
<Badge className="rounded-full py-px" variant="outline">
|
||||
Free
|
||||
</Badge>
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value="express" id="shipping-method-2" />
|
||||
<FieldLabel htmlFor="shipping-method-2" className="font-normal">
|
||||
Express
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="message">Message</FieldLabel>
|
||||
<Textarea id="message" />
|
||||
<FieldDescription>Anything else you want to add?</FieldDescription>
|
||||
</Field>
|
||||
<FieldSet>
|
||||
<FieldLegend>Additional Items</FieldLegend>
|
||||
<FieldDescription>
|
||||
Please select the additional items for your order.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<FieldLabel htmlFor="gift-wrapping">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
value="gift-wrapping"
|
||||
id="gift-wrapping"
|
||||
aria-label="Gift Wrapping"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Gift Wrapping</FieldTitle>
|
||||
<FieldDescription>
|
||||
Add elegant gift wrapping with a personalized message.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="insurance">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
value="insurance"
|
||||
id="insurance"
|
||||
aria-label="Package Insurance"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Package Insurance</FieldTitle>
|
||||
<FieldDescription>
|
||||
Protect your shipment with comprehensive insurance coverage.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="signature-confirmation">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
value="signature-confirmation"
|
||||
id="signature-confirmation"
|
||||
aria-label="Signature Confirmation"
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldTitle>Signature Confirmation</FieldTitle>
|
||||
<FieldDescription>
|
||||
Require recipient signature upon delivery for added
|
||||
security.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
55
apps/v4/app/(internal)/sink/(pages)/next-form/actions.ts
Normal file
55
apps/v4/app/(internal)/sink/(pages)/next-form/actions.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
"use server"
|
||||
|
||||
import { FormState } from "@/app/(internal)/sink/(pages)/next-form/example-form"
|
||||
import { exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
export async function subscriptionAction(
|
||||
_prevState: FormState,
|
||||
formData: FormData
|
||||
): Promise<FormState> {
|
||||
// Simulate server processing
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const values = {
|
||||
name: formData.get("name") as string,
|
||||
email: formData.get("email") as string,
|
||||
plan: formData.get("plan") as "basic" | "pro",
|
||||
billingPeriod: formData.get("billingPeriod") as string,
|
||||
addons: formData.getAll("addons") as string[],
|
||||
teamSize: parseInt(formData.get("teamSize") as string) || 1,
|
||||
emailNotifications: formData.get("emailNotifications") === "on",
|
||||
startDate: formData.get("startDate")
|
||||
? new Date(formData.get("startDate") as string)
|
||||
: new Date(),
|
||||
theme: formData.get("theme") as string,
|
||||
password: formData.get("password") as string,
|
||||
comments: formData.get("comments") as string,
|
||||
}
|
||||
|
||||
const result = exampleFormSchema.safeParse(values)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: result.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate some business logic validation
|
||||
if (result.data.email.includes("invalid")) {
|
||||
return {
|
||||
values,
|
||||
success: false,
|
||||
errors: {
|
||||
email: ["This email domain is not supported"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
values,
|
||||
errors: null,
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
391
apps/v4/app/(internal)/sink/(pages)/next-form/example-form.tsx
Normal file
391
apps/v4/app/(internal)/sink/(pages)/next-form/example-form.tsx
Normal file
@@ -0,0 +1,391 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Form from "next/form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
import { subscriptionAction } from "./actions"
|
||||
|
||||
export type FormState = {
|
||||
values: z.infer<typeof exampleFormSchema>
|
||||
errors: null | Partial<
|
||||
Record<keyof z.infer<typeof exampleFormSchema>, string[]>
|
||||
>
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export function ExampleForm() {
|
||||
const formId = React.useId()
|
||||
const [formKey, setFormKey] = React.useState(formId)
|
||||
const [showResults, setShowResults] = React.useState(false)
|
||||
const [formState, formAction, pending] = React.useActionState<
|
||||
FormState,
|
||||
FormData
|
||||
>(subscriptionAction, {
|
||||
values: {
|
||||
name: "",
|
||||
email: "",
|
||||
plan: "basic",
|
||||
billingPeriod: "",
|
||||
addons: ["analytics"],
|
||||
teamSize: 1,
|
||||
emailNotifications: false,
|
||||
comments: "",
|
||||
startDate: new Date(),
|
||||
theme: "system",
|
||||
password: "",
|
||||
},
|
||||
errors: null,
|
||||
success: false,
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (formState.success) {
|
||||
setShowResults(true)
|
||||
}
|
||||
}, [formState.success])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Subscription Form</CardTitle>
|
||||
<CardDescription>
|
||||
Create your subscription using server actions and useActionState.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form action={formAction} id="subscription-form" key={formKey}>
|
||||
<FieldGroup>
|
||||
<Field data-invalid={!!formState.errors?.name?.length}>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
defaultValue={formState.values.name}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.name?.length}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your name</FieldDescription>
|
||||
{formState.errors?.name && (
|
||||
<FieldError>{formState.errors.name[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<Field data-invalid={!!formState.errors?.email?.length}>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
defaultValue={formState.values.email}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.email?.length}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your email address</FieldDescription>
|
||||
{formState.errors?.email && (
|
||||
<FieldError>{formState.errors.email[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldSet data-invalid={!!formState.errors?.plan?.length}>
|
||||
<FieldLegend>Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name="plan"
|
||||
defaultValue={formState.values.plan}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.plan?.length}
|
||||
>
|
||||
<FieldLabel htmlFor="basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="basic" id="basic" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value="pro" id="pro" />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{formState.errors?.plan && (
|
||||
<FieldError>{formState.errors.plan[0]}</FieldError>
|
||||
)}
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.billingPeriod?.length}>
|
||||
<FieldLabel htmlFor="billingPeriod">Billing Period</FieldLabel>
|
||||
<Select
|
||||
name="billingPeriod"
|
||||
defaultValue={formState.values.billingPeriod}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.billingPeriod?.length}
|
||||
>
|
||||
<SelectTrigger id="billingPeriod">
|
||||
<SelectValue placeholder="Select billing period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{formState.errors?.billingPeriod && (
|
||||
<FieldError>{formState.errors.billingPeriod[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldSet data-invalid={!!formState.errors?.addons?.length}>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field key={addon.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={addon.id}
|
||||
name="addons"
|
||||
value={addon.id}
|
||||
defaultChecked={formState.values.addons.includes(
|
||||
addon.id
|
||||
)}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.addons?.length}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={addon.id}>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>{addon.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{formState.errors?.addons && (
|
||||
<FieldError>{formState.errors.addons[0]}</FieldError>
|
||||
)}
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.teamSize?.length}>
|
||||
<FieldLabel htmlFor="teamSize">Team Size</FieldLabel>
|
||||
<Input
|
||||
id="teamSize"
|
||||
name="teamSize"
|
||||
type="number"
|
||||
min="1"
|
||||
max="50"
|
||||
defaultValue={formState.values.teamSize.toString()}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.teamSize?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
How many people will be using the subscription? (1-50)
|
||||
</FieldDescription>
|
||||
{formState.errors?.teamSize && (
|
||||
<FieldError>{formState.errors.teamSize[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="emailNotifications">
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="emailNotifications"
|
||||
name="emailNotifications"
|
||||
defaultChecked={formState.values.emailNotifications}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.emailNotifications?.length}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.startDate?.length}>
|
||||
<FieldLabel htmlFor="startDate">Start Date</FieldLabel>
|
||||
<Input
|
||||
id="startDate"
|
||||
name="startDate"
|
||||
type="date"
|
||||
defaultValue={
|
||||
formState.values.startDate.toISOString().split("T")[0]
|
||||
}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.startDate?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Choose when your subscription should start
|
||||
</FieldDescription>
|
||||
{formState.errors?.startDate && (
|
||||
<FieldError>{formState.errors.startDate[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.theme?.length}>
|
||||
<FieldLabel htmlFor="theme">Theme Preference</FieldLabel>
|
||||
<Select
|
||||
name="theme"
|
||||
defaultValue={formState.values.theme}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.theme?.length}
|
||||
>
|
||||
<SelectTrigger id="theme">
|
||||
<SelectValue placeholder="Select theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose your preferred color theme
|
||||
</FieldDescription>
|
||||
{formState.errors?.theme && (
|
||||
<FieldError>{formState.errors.theme[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.password?.length}>
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
defaultValue={formState.values.password}
|
||||
placeholder="Enter your password"
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.password?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Must contain uppercase, lowercase, number, and be 8+
|
||||
characters
|
||||
</FieldDescription>
|
||||
{formState.errors?.password && (
|
||||
<FieldError>{formState.errors.password[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field data-invalid={!!formState.errors?.comments?.length}>
|
||||
<FieldLabel htmlFor="comments">Additional Comments</FieldLabel>
|
||||
<Textarea
|
||||
id="comments"
|
||||
name="comments"
|
||||
defaultValue={formState.values.comments}
|
||||
placeholder="Tell us more about your needs..."
|
||||
rows={3}
|
||||
disabled={pending}
|
||||
aria-invalid={!!formState.errors?.comments?.length}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Share any additional requirements or feedback (10-240
|
||||
characters)
|
||||
</FieldDescription>
|
||||
{formState.errors?.comments && (
|
||||
<FieldError>{formState.errors.comments[0]}</FieldError>
|
||||
)}
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={pending}
|
||||
form="subscription-form"
|
||||
onClick={() => setFormKey(formKey + 1)}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" disabled={pending} form="subscription-form">
|
||||
{pending && <Spinner />}
|
||||
Create Subscription
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={showResults} onOpenChange={setShowResults}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Subscription Created!</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here are the details of your subscription.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
|
||||
<code>{JSON.stringify(formState.values, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
9
apps/v4/app/(internal)/sink/(pages)/next-form/page.tsx
Normal file
9
apps/v4/app/(internal)/sink/(pages)/next-form/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ExampleForm } from "@/app/(internal)/sink/(pages)/next-form/example-form"
|
||||
|
||||
export default function NextFormPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<ExampleForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import z from "zod"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
export function ExampleForm() {
|
||||
const [values, setValues] = useState<z.infer<typeof exampleFormSchema>>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const form = useForm<z.infer<typeof exampleFormSchema>>({
|
||||
resolver: zodResolver(exampleFormSchema),
|
||||
mode: "onChange",
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
plan: "basic" as const,
|
||||
billingPeriod: "",
|
||||
addons: ["analytics"],
|
||||
emailNotifications: false,
|
||||
teamSize: 1,
|
||||
comments: "",
|
||||
startDate: new Date(),
|
||||
theme: "system",
|
||||
password: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof exampleFormSchema>) {
|
||||
setValues(data)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>React Hook Form</CardTitle>
|
||||
<CardDescription>
|
||||
This form uses React Hook Form with Zod validation.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="subscription-form" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FieldGroup>
|
||||
<Controller
|
||||
name="name"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
id={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your name</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
id={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your email address
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="plan"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<FieldLabel htmlFor="basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="basic"
|
||||
id="basic"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="pro"
|
||||
id="pro"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="billingPeriod"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Billing Period
|
||||
</FieldLabel>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<SelectTrigger id={field.name}>
|
||||
<SelectValue placeholder="Select billing period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="addons"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field key={addon.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={addon.id}
|
||||
name={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
checked={field.value.includes(addon.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newValue = checked
|
||||
? [...field.value, addon.id]
|
||||
: field.value.filter(
|
||||
(value) => value !== addon.id
|
||||
)
|
||||
field.onChange(newValue)
|
||||
field.onBlur()
|
||||
}}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={addon.id}>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
{addon.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="teamSize"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Team Size</FieldTitle>
|
||||
<FieldDescription>
|
||||
How many people will be using the subscription?
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={[field.value]}
|
||||
onValueChange={field.onChange}
|
||||
min={1}
|
||||
max={50}
|
||||
step={1}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="emailNotifications"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Start Date</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id={field.name}
|
||||
variant="outline"
|
||||
className="justify-start"
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
required
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FieldDescription>
|
||||
Choose when your subscription should start
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="theme"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Theme Preference</FieldTitle>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
variant="outline"
|
||||
value={field.value}
|
||||
onValueChange={(value) =>
|
||||
value && field.onChange(value)
|
||||
}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
||||
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<FieldDescription>
|
||||
Choose your preferred color theme
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
id={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Must contain uppercase, lowercase, number, and be 8+
|
||||
characters
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<Controller
|
||||
name="comments"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => {
|
||||
const isInvalid = fieldState.invalid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Additional Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
{...field}
|
||||
id={field.name}
|
||||
placeholder="Tell us more about your needs..."
|
||||
rows={3}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Share any additional requirements or feedback (10-240
|
||||
characters)
|
||||
</FieldDescription>
|
||||
{isInvalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => form.reset()}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="subscription-form">
|
||||
Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submitted Values</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here are the values you submitted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
|
||||
<code>{JSON.stringify(values, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ExampleForm } from "@/app/(internal)/sink/(pages)/react-hook-form/example-form"
|
||||
|
||||
export default function ReactHookFormPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<ExampleForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
97
apps/v4/app/(internal)/sink/(pages)/schema.ts
Normal file
97
apps/v4/app/(internal)/sink/(pages)/schema.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const addons = [
|
||||
{
|
||||
id: "analytics",
|
||||
title: "Analytics",
|
||||
description: "Advanced analytics and reporting",
|
||||
},
|
||||
{
|
||||
id: "backup",
|
||||
title: "Backup",
|
||||
description: "Automated daily backups",
|
||||
},
|
||||
{
|
||||
id: "support",
|
||||
title: "Priority Support",
|
||||
description: "24/7 premium customer support",
|
||||
},
|
||||
] as const
|
||||
|
||||
export const exampleFormSchema = z.object({
|
||||
name: z
|
||||
.string({
|
||||
required_error: "Name is required",
|
||||
invalid_type_error: "Name must be a string",
|
||||
})
|
||||
.min(2, "Name must be at least 2 characters")
|
||||
.max(50, "Name must be less than 50 characters")
|
||||
.refine((value) => !/\d/.test(value), {
|
||||
message: "Name must not contain numbers",
|
||||
}),
|
||||
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Email is required",
|
||||
})
|
||||
.email("Please enter a valid email address"),
|
||||
|
||||
plan: z
|
||||
.string({
|
||||
required_error: "Please select a subscription plan",
|
||||
})
|
||||
.min(1, "Please select a subscription plan")
|
||||
.refine((value) => value === "basic" || value === "pro", {
|
||||
message: "Invalid plan selection. Please choose Basic or Pro",
|
||||
}),
|
||||
|
||||
billingPeriod: z
|
||||
.string({
|
||||
required_error: "Please select a billing period",
|
||||
})
|
||||
.min(1, "Please select a billing period"),
|
||||
|
||||
addons: z
|
||||
.array(z.string())
|
||||
.min(1, "Please select at least one add-on")
|
||||
.max(3, "You can select up to 3 add-ons"),
|
||||
|
||||
teamSize: z.number().min(1).max(10),
|
||||
emailNotifications: z.boolean({
|
||||
required_error: "Please choose email notification preference",
|
||||
}),
|
||||
comments: z
|
||||
.string()
|
||||
.min(10, "Comments must be at least 10 characters")
|
||||
.max(240, "Comments must not exceed 240 characters"),
|
||||
startDate: z
|
||||
.date({
|
||||
required_error: "Please select a start date",
|
||||
invalid_type_error: "Invalid date format",
|
||||
})
|
||||
.min(new Date(), "Start date cannot be in the past")
|
||||
.refine(
|
||||
(date) => {
|
||||
const now = new Date()
|
||||
const oneWeekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
|
||||
return date <= oneWeekFromNow
|
||||
},
|
||||
{
|
||||
message: "Start date must be within the current week",
|
||||
}
|
||||
),
|
||||
theme: z
|
||||
.string({
|
||||
required_error: "Please select a theme",
|
||||
})
|
||||
.min(1, "Please select a theme"),
|
||||
password: z
|
||||
.string({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8, "Password must be at least 8 characters")
|
||||
.regex(
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
|
||||
"Password must contain at least one uppercase letter, one lowercase letter, and one number"
|
||||
),
|
||||
})
|
||||
@@ -0,0 +1,532 @@
|
||||
/* eslint-disable react/no-children-prop */
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useForm } from "@tanstack/react-form"
|
||||
import { format } from "date-fns"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Calendar } from "@/registry/new-york-v4/ui/calendar"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
import { addons, exampleFormSchema } from "@/app/(internal)/sink/(pages)/schema"
|
||||
|
||||
export function ExampleForm() {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
plan: "",
|
||||
billingPeriod: "",
|
||||
addons: ["analytics"],
|
||||
emailNotifications: false,
|
||||
teamSize: 1,
|
||||
comments: "",
|
||||
startDate: new Date(),
|
||||
theme: "system",
|
||||
password: "",
|
||||
},
|
||||
validators: {
|
||||
onChange: exampleFormSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
setValues(value)
|
||||
setOpen(true)
|
||||
},
|
||||
})
|
||||
const [values, setValues] = React.useState<typeof form.state.values>()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Example Form</CardTitle>
|
||||
<CardDescription>
|
||||
This is an example form using TanStack Form.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
id="example-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
void form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="name"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>Enter your name</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="email"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="email"
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Enter your email address
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="plan"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<FieldLabel htmlFor="basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="basic"
|
||||
id="basic"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="pro"
|
||||
id="pro"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="billingPeriod"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Billing Period
|
||||
</FieldLabel>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={field.handleChange}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<SelectTrigger id={field.name}>
|
||||
<SelectValue placeholder="Select billing period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="addons"
|
||||
mode="array"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<FieldSet data-invalid={isInvalid}>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field key={addon.id} orientation="horizontal">
|
||||
<Checkbox
|
||||
id={addon.id}
|
||||
name={field.name}
|
||||
aria-invalid={isInvalid}
|
||||
checked={field.state.value.includes(addon.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
field.pushValue(addon.id)
|
||||
} else {
|
||||
const index = field.state.value.indexOf(
|
||||
addon.id
|
||||
)
|
||||
if (index > -1) {
|
||||
field.removeValue(index)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={addon.id}>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
{addon.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="teamSize"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Team Size</FieldTitle>
|
||||
<FieldDescription>
|
||||
How many people will be using the subscription?
|
||||
</FieldDescription>
|
||||
<Slider
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={[field.state.value]}
|
||||
onValueChange={(value) => field.handleChange(value[0])}
|
||||
min={1}
|
||||
max={50}
|
||||
step={10}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="emailNotifications"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
checked={field.state.value}
|
||||
onCheckedChange={field.handleChange}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="startDate"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Start Date</FieldLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id={field.name}
|
||||
variant="outline"
|
||||
className="justify-start"
|
||||
>
|
||||
{field.state.value ? (
|
||||
format(field.state.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
required
|
||||
mode="single"
|
||||
selected={field.state.value}
|
||||
onSelect={field.handleChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FieldDescription>
|
||||
Choose when your subscription should start
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="theme"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldTitle>Theme Preference</FieldTitle>
|
||||
<ToggleGroup
|
||||
id={field.name}
|
||||
type="single"
|
||||
variant="outline"
|
||||
value={field.state.value}
|
||||
onValueChange={(value) =>
|
||||
value && field.handleChange(value)
|
||||
}
|
||||
aria-invalid={isInvalid}
|
||||
>
|
||||
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
||||
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<FieldDescription>
|
||||
Choose your preferred color theme
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="password"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type="password"
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
placeholder="Enter your password"
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Must contain uppercase, lowercase, number, and be 8+
|
||||
characters
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldSeparator />
|
||||
<form.Field
|
||||
name="comments"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
Additional Comments
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
placeholder="Tell us more about your needs..."
|
||||
rows={3}
|
||||
aria-invalid={isInvalid}
|
||||
/>
|
||||
<FieldDescription>
|
||||
Share any additional requirements or feedback (10-240
|
||||
characters)
|
||||
</FieldDescription>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => form.reset()}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="example-form">
|
||||
Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submitted Values</DialogTitle>
|
||||
<DialogDescription>
|
||||
Here are the values you submitted.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="overflow-x-auto rounded-md bg-black p-4 font-mono text-sm text-white">
|
||||
<code>{JSON.stringify(values, null, 2)}</code>
|
||||
</pre>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ExampleForm } from "@/app/(internal)/sink/(pages)/tanstack-form/example-form"
|
||||
|
||||
export default function TanstackFormPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<ExampleForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
apps/v4/app/(internal)/sink/[name]/page.tsx
Normal file
54
apps/v4/app/(internal)/sink/[name]/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(componentRegistry).map((name) => ({
|
||||
name,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ name: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { name } = await params
|
||||
const component = componentRegistry[name as keyof typeof componentRegistry]
|
||||
|
||||
if (!component) {
|
||||
return {
|
||||
title: "Component Not Found",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${component.name} - Kitchen Sink`,
|
||||
description: `Demo page for ${component.name} component`,
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ComponentPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ name: string }>
|
||||
}) {
|
||||
const { name } = await params
|
||||
const component = componentRegistry[name as keyof typeof componentRegistry]
|
||||
|
||||
if (!component) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const Component = component.component
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Component />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
429
apps/v4/app/(internal)/sink/component-registry.ts
Normal file
429
apps/v4/app/(internal)/sink/component-registry.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import FormsPage from "@/app/(internal)/sink/(pages)/forms/page"
|
||||
|
||||
import NextFormPage from "./(pages)/next-form/page"
|
||||
import ReactHookFormPage from "./(pages)/react-hook-form/page"
|
||||
import TanstackFormPage from "./(pages)/tanstack-form/page"
|
||||
import { AccordionDemo } from "./components/accordion-demo"
|
||||
import { AlertDemo } from "./components/alert-demo"
|
||||
import { AlertDialogDemo } from "./components/alert-dialog-demo"
|
||||
import { AspectRatioDemo } from "./components/aspect-ratio-demo"
|
||||
import { AvatarDemo } from "./components/avatar-demo"
|
||||
import { BadgeDemo } from "./components/badge-demo"
|
||||
import { BreadcrumbDemo } from "./components/breadcrumb-demo"
|
||||
import { ButtonDemo } from "./components/button-demo"
|
||||
import { ButtonGroupDemo } from "./components/button-group-demo"
|
||||
import { CalendarDemo } from "./components/calendar-demo"
|
||||
import { CardDemo } from "./components/card-demo"
|
||||
import { CarouselDemo } from "./components/carousel-demo"
|
||||
import { ChartDemo } from "./components/chart-demo"
|
||||
import { CheckboxDemo } from "./components/checkbox-demo"
|
||||
import { CollapsibleDemo } from "./components/collapsible-demo"
|
||||
import { ComboboxDemo } from "./components/combobox-demo"
|
||||
import { CommandDemo } from "./components/command-demo"
|
||||
import { ContextMenuDemo } from "./components/context-menu-demo"
|
||||
import { DatePickerDemo } from "./components/date-picker-demo"
|
||||
import { DialogDemo } from "./components/dialog-demo"
|
||||
import { DrawerDemo } from "./components/drawer-demo"
|
||||
import { DropdownMenuDemo } from "./components/dropdown-menu-demo"
|
||||
import { EmptyDemo } from "./components/empty-demo"
|
||||
import { FieldDemo } from "./components/field-demo"
|
||||
import { FormDemo } from "./components/form-demo"
|
||||
import { HoverCardDemo } from "./components/hover-card-demo"
|
||||
import { InputDemo } from "./components/input-demo"
|
||||
import { InputGroupDemo } from "./components/input-group-demo"
|
||||
import { InputOTPDemo } from "./components/input-otp-demo"
|
||||
import { ItemDemo } from "./components/item-demo"
|
||||
import { KbdDemo } from "./components/kbd-demo"
|
||||
import { LabelDemo } from "./components/label-demo"
|
||||
import { MenubarDemo } from "./components/menubar-demo"
|
||||
import { NavigationMenuDemo } from "./components/navigation-menu-demo"
|
||||
import { PaginationDemo } from "./components/pagination-demo"
|
||||
import { PopoverDemo } from "./components/popover-demo"
|
||||
import { ProgressDemo } from "./components/progress-demo"
|
||||
import { RadioGroupDemo } from "./components/radio-group-demo"
|
||||
import { ResizableDemo } from "./components/resizable-demo"
|
||||
import { ScrollAreaDemo } from "./components/scroll-area-demo"
|
||||
import { SelectDemo } from "./components/select-demo"
|
||||
import { SeparatorDemo } from "./components/separator-demo"
|
||||
import { SheetDemo } from "./components/sheet-demo"
|
||||
import { SkeletonDemo } from "./components/skeleton-demo"
|
||||
import { SliderDemo } from "./components/slider-demo"
|
||||
import { SonnerDemo } from "./components/sonner-demo"
|
||||
import { SpinnerDemo } from "./components/spinner-demo"
|
||||
import { SwitchDemo } from "./components/switch-demo"
|
||||
import { TableDemo } from "./components/table-demo"
|
||||
import { TabsDemo } from "./components/tabs-demo"
|
||||
import { TextareaDemo } from "./components/textarea-demo"
|
||||
import { ToggleDemo } from "./components/toggle-demo"
|
||||
import { ToggleGroupDemo } from "./components/toggle-group-demo"
|
||||
import { TooltipDemo } from "./components/tooltip-demo"
|
||||
|
||||
type ComponentConfig = {
|
||||
name: string
|
||||
component: React.ComponentType
|
||||
className?: string
|
||||
type: "registry:ui" | "registry:page" | "registry:block"
|
||||
href: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const componentRegistry: Record<string, ComponentConfig> = {
|
||||
accordion: {
|
||||
name: "Accordion",
|
||||
component: AccordionDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/accordion",
|
||||
},
|
||||
alert: {
|
||||
name: "Alert",
|
||||
component: AlertDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/alert",
|
||||
},
|
||||
"alert-dialog": {
|
||||
name: "Alert Dialog",
|
||||
component: AlertDialogDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/alert-dialog",
|
||||
},
|
||||
"aspect-ratio": {
|
||||
name: "Aspect Ratio",
|
||||
component: AspectRatioDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/aspect-ratio",
|
||||
},
|
||||
avatar: {
|
||||
name: "Avatar",
|
||||
component: AvatarDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/avatar",
|
||||
},
|
||||
badge: {
|
||||
name: "Badge",
|
||||
component: BadgeDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/badge",
|
||||
},
|
||||
breadcrumb: {
|
||||
name: "Breadcrumb",
|
||||
component: BreadcrumbDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/breadcrumb",
|
||||
},
|
||||
button: {
|
||||
name: "Button",
|
||||
component: ButtonDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/button",
|
||||
},
|
||||
"button-group": {
|
||||
name: "Button Group",
|
||||
component: ButtonGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/button-group",
|
||||
label: "New",
|
||||
},
|
||||
calendar: {
|
||||
name: "Calendar",
|
||||
component: CalendarDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/calendar",
|
||||
},
|
||||
card: {
|
||||
name: "Card",
|
||||
component: CardDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/card",
|
||||
},
|
||||
carousel: {
|
||||
name: "Carousel",
|
||||
component: CarouselDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/carousel",
|
||||
},
|
||||
chart: {
|
||||
name: "Chart",
|
||||
component: ChartDemo,
|
||||
className: "w-full",
|
||||
type: "registry:ui",
|
||||
href: "/sink/chart",
|
||||
},
|
||||
checkbox: {
|
||||
name: "Checkbox",
|
||||
component: CheckboxDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/checkbox",
|
||||
},
|
||||
collapsible: {
|
||||
name: "Collapsible",
|
||||
component: CollapsibleDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/collapsible",
|
||||
},
|
||||
combobox: {
|
||||
name: "Combobox",
|
||||
component: ComboboxDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/combobox",
|
||||
},
|
||||
command: {
|
||||
name: "Command",
|
||||
component: CommandDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/command",
|
||||
},
|
||||
"context-menu": {
|
||||
name: "Context Menu",
|
||||
component: ContextMenuDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/context-menu",
|
||||
},
|
||||
"date-picker": {
|
||||
name: "Date Picker",
|
||||
component: DatePickerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/date-picker",
|
||||
},
|
||||
dialog: {
|
||||
name: "Dialog",
|
||||
component: DialogDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/dialog",
|
||||
},
|
||||
drawer: {
|
||||
name: "Drawer",
|
||||
component: DrawerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/drawer",
|
||||
},
|
||||
"dropdown-menu": {
|
||||
name: "Dropdown Menu",
|
||||
component: DropdownMenuDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/dropdown-menu",
|
||||
},
|
||||
empty: {
|
||||
name: "Empty",
|
||||
component: EmptyDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/empty",
|
||||
label: "New",
|
||||
},
|
||||
field: {
|
||||
name: "Field",
|
||||
component: FieldDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/field",
|
||||
label: "New",
|
||||
},
|
||||
form: {
|
||||
name: "Form",
|
||||
component: FormDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/form",
|
||||
},
|
||||
"hover-card": {
|
||||
name: "Hover Card",
|
||||
component: HoverCardDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/hover-card",
|
||||
},
|
||||
input: {
|
||||
name: "Input",
|
||||
component: InputDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/input",
|
||||
},
|
||||
"input-group": {
|
||||
name: "Input Group",
|
||||
component: InputGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/input-group",
|
||||
label: "New",
|
||||
},
|
||||
"input-otp": {
|
||||
name: "Input OTP",
|
||||
component: InputOTPDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/input-otp",
|
||||
},
|
||||
item: {
|
||||
name: "Item",
|
||||
component: ItemDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/item",
|
||||
label: "New",
|
||||
},
|
||||
kbd: {
|
||||
name: "Kbd",
|
||||
component: KbdDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/kbd",
|
||||
label: "New",
|
||||
},
|
||||
label: {
|
||||
name: "Label",
|
||||
component: LabelDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/label",
|
||||
},
|
||||
menubar: {
|
||||
name: "Menubar",
|
||||
component: MenubarDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/menubar",
|
||||
},
|
||||
"navigation-menu": {
|
||||
name: "Navigation Menu",
|
||||
component: NavigationMenuDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/navigation-menu",
|
||||
},
|
||||
pagination: {
|
||||
name: "Pagination",
|
||||
component: PaginationDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/pagination",
|
||||
},
|
||||
popover: {
|
||||
name: "Popover",
|
||||
component: PopoverDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/popover",
|
||||
},
|
||||
progress: {
|
||||
name: "Progress",
|
||||
component: ProgressDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/progress",
|
||||
},
|
||||
"radio-group": {
|
||||
name: "Radio Group",
|
||||
component: RadioGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/radio-group",
|
||||
},
|
||||
resizable: {
|
||||
name: "Resizable",
|
||||
component: ResizableDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/resizable",
|
||||
},
|
||||
"scroll-area": {
|
||||
name: "Scroll Area",
|
||||
component: ScrollAreaDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/scroll-area",
|
||||
},
|
||||
select: {
|
||||
name: "Select",
|
||||
component: SelectDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/select",
|
||||
},
|
||||
separator: {
|
||||
name: "Separator",
|
||||
component: SeparatorDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/separator",
|
||||
},
|
||||
sheet: {
|
||||
name: "Sheet",
|
||||
component: SheetDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/sheet",
|
||||
},
|
||||
skeleton: {
|
||||
name: "Skeleton",
|
||||
component: SkeletonDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/skeleton",
|
||||
},
|
||||
slider: {
|
||||
name: "Slider",
|
||||
component: SliderDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/slider",
|
||||
},
|
||||
sonner: {
|
||||
name: "Sonner",
|
||||
component: SonnerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/sonner",
|
||||
},
|
||||
spinner: {
|
||||
name: "Spinner",
|
||||
component: SpinnerDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/spinner",
|
||||
label: "New",
|
||||
},
|
||||
switch: {
|
||||
name: "Switch",
|
||||
component: SwitchDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/switch",
|
||||
},
|
||||
table: {
|
||||
name: "Table",
|
||||
component: TableDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/table",
|
||||
},
|
||||
tabs: {
|
||||
name: "Tabs",
|
||||
component: TabsDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/tabs",
|
||||
},
|
||||
textarea: {
|
||||
name: "Textarea",
|
||||
component: TextareaDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/textarea",
|
||||
},
|
||||
toggle: {
|
||||
name: "Toggle",
|
||||
component: ToggleDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/toggle",
|
||||
},
|
||||
"toggle-group": {
|
||||
name: "Toggle Group",
|
||||
component: ToggleGroupDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/toggle-group",
|
||||
},
|
||||
tooltip: {
|
||||
name: "Tooltip",
|
||||
component: TooltipDemo,
|
||||
type: "registry:ui",
|
||||
href: "/sink/tooltip",
|
||||
},
|
||||
blocks: {
|
||||
name: "Forms",
|
||||
component: FormsPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/forms",
|
||||
},
|
||||
"next-form": {
|
||||
name: "Next.js Form",
|
||||
component: NextFormPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/next-form",
|
||||
},
|
||||
"tanstack-form": {
|
||||
name: "Tanstack Form",
|
||||
component: TanstackFormPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/tanstack-form",
|
||||
},
|
||||
"react-hook-form": {
|
||||
name: "React Hook Form",
|
||||
component: ReactHookFormPage,
|
||||
type: "registry:page",
|
||||
href: "/sink/react-hook-form",
|
||||
},
|
||||
}
|
||||
|
||||
export type ComponentKey = keyof typeof componentRegistry
|
||||
43
apps/v4/app/(internal)/sink/components/app-breadcrumbs.tsx
Normal file
43
apps/v4/app/(internal)/sink/components/app-breadcrumbs.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { useParams } from "next/navigation"
|
||||
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/registry/new-york-v4/ui/breadcrumb"
|
||||
|
||||
export function AppBreadcrumbs() {
|
||||
const params = useParams()
|
||||
const { name } = params
|
||||
|
||||
if (!name) {
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Kitchen Sink</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/sink">Kitchen Sink</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden sm:flex" />
|
||||
<BreadcrumbItem className="hidden sm:block">
|
||||
<BreadcrumbPage className="capitalize">{name}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import {
|
||||
AudioWaveform,
|
||||
BookOpen,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
Search,
|
||||
SearchIcon,
|
||||
Settings2,
|
||||
SquareTerminal,
|
||||
} from "lucide-react"
|
||||
@@ -22,6 +23,11 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/registry/new-york-v4/ui/collapsible"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -31,7 +37,6 @@ import {
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@@ -40,6 +45,7 @@ import {
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
@@ -163,8 +169,9 @@ const data = {
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const pathname = usePathname()
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<Sidebar side="left" collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
<SidebarGroup className="py-0 group-data-[collapsible=icon]:hidden">
|
||||
@@ -173,12 +180,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<Label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</Label>
|
||||
<SidebarInput
|
||||
id="search"
|
||||
placeholder="Search the docs..."
|
||||
className="pl-8"
|
||||
/>
|
||||
<Search className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" />
|
||||
<InputGroup className="bg-background h-8 shadow-none">
|
||||
<InputGroupInput
|
||||
id="search"
|
||||
placeholder="Search the docs..."
|
||||
className="h-7"
|
||||
data-slot="input-group-control"
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
@@ -221,17 +233,58 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Components</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{data.components.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={`/sink#${item.name}`}>
|
||||
<span>{getComponentName(item.name)}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{["registry:ui", "registry:page", "registry:block"].map((type) => {
|
||||
const typeComponents = Object.entries(componentRegistry).filter(
|
||||
([, item]) => item.type === type
|
||||
)
|
||||
if (typeComponents.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={type}
|
||||
asChild
|
||||
defaultOpen={pathname.includes("/sink/")}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton>
|
||||
<span>
|
||||
{type === "registry:ui"
|
||||
? "Components"
|
||||
: type === "registry:page"
|
||||
? "Pages"
|
||||
: "Blocks"}
|
||||
</span>
|
||||
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{typeComponents.map(([key, item]) => (
|
||||
<SidebarMenuSubItem key={key}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={pathname === item.href}
|
||||
>
|
||||
<Link href={item.href}>
|
||||
<span>{item.name}</span>
|
||||
{item.label && (
|
||||
<span className="flex size-2 rounded-full bg-blue-500" />
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
@@ -242,8 +295,3 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
function getComponentName(name: string) {
|
||||
// convert kebab-case to title case
|
||||
return name.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ export function AvatarDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
@@ -48,7 +51,10 @@ export function AvatarDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
@@ -65,7 +71,10 @@ export function AvatarDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
|
||||
581
apps/v4/app/(internal)/sink/components/button-group-demo.tsx
Normal file
581
apps/v4/app/(internal)/sink/components/button-group-demo.tsx
Normal file
@@ -0,0 +1,581 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconBrandGithubCopilot,
|
||||
IconChevronDown,
|
||||
IconCircleCheck,
|
||||
IconCloudCode,
|
||||
IconHeart,
|
||||
IconMinus,
|
||||
IconPin,
|
||||
IconPlus,
|
||||
IconUserCircle,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
AudioLinesIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
CopyIcon,
|
||||
FlipHorizontalIcon,
|
||||
FlipVerticalIcon,
|
||||
MoreHorizontalIcon,
|
||||
PercentIcon,
|
||||
RotateCwIcon,
|
||||
SearchIcon,
|
||||
ShareIcon,
|
||||
TrashIcon,
|
||||
UserRoundXIcon,
|
||||
VolumeOffIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
} from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldGroup } from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [currency, setCurrency] = useState("$")
|
||||
return (
|
||||
<div className="flex gap-12">
|
||||
<div className="flex max-w-sm flex-col gap-6">
|
||||
<ButtonGroup>
|
||||
<Button>Button</Button>
|
||||
<Button>
|
||||
Get Started <IconArrowRight />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button>Button</Button>
|
||||
<ButtonGroupSeparator className="bg-primary/80" />
|
||||
<Button>
|
||||
Get Started <IconArrowRight />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Input placeholder="Type something here..." />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Input placeholder="Type something here..." />
|
||||
<Button variant="outline">Button</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Button variant="outline">Another Button</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>Text</ButtonGroupText>
|
||||
<Button variant="outline">Another Button</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText asChild>
|
||||
<Label htmlFor="input">
|
||||
<IconCloudCode /> GPU Size
|
||||
</Label>
|
||||
</ButtonGroupText>
|
||||
<Input id="input" placeholder="Type something here..." />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>Prefix</ButtonGroupText>
|
||||
<Input id="input" placeholder="Type something here..." />
|
||||
<ButtonGroupText>Suffix</ButtonGroupText>
|
||||
</ButtonGroup>
|
||||
<div className="flex gap-4">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Update</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Disable</DropdownMenuItem>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Uninstall
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:9999px]">
|
||||
<Button variant="outline">Follow</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="!pl-2">
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="[--radius:0.95rem]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<VolumeOffIcon />
|
||||
Mute Conversation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CheckIcon />
|
||||
Mark as Read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<AlertTriangleIcon />
|
||||
Report Conversation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<UserRoundXIcon />
|
||||
Block User
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ShareIcon />
|
||||
Share Conversation
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CopyIcon />
|
||||
Copy Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<TrashIcon />
|
||||
Delete Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:0.9rem]">
|
||||
<Button variant="secondary">Actions</Button>
|
||||
<ButtonGroupSeparator />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary">
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="[--radius:0.9rem]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconCircleCheck />
|
||||
Select Messages
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPin />
|
||||
Edit Pins
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconUserCircle />
|
||||
Set Up Name & Photo
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<Field>
|
||||
<Label htmlFor="amount">Amount</Label>
|
||||
<ButtonGroup>
|
||||
<Select value={currency} onValueChange={setCurrency}>
|
||||
<SelectTrigger className="font-mono">{currency}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="$">$</SelectItem>
|
||||
<SelectItem value="€">€</SelectItem>
|
||||
<SelectItem value="£">£</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input placeholder="Enter amount to send" />
|
||||
<Button variant="outline">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex max-w-xs flex-col gap-6">
|
||||
<ButtonGroup className="[--spacing:0.2rem]">
|
||||
<Button variant="outline">
|
||||
<FlipHorizontalIcon />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<FlipVerticalIcon />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<RotateCwIcon />
|
||||
</Button>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="0.00" />
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<PercentIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
<div className="flex gap-2 [--radius:0.95rem] [--ring:var(--color-blue-300)] [--spacing:0.22rem] **:[.shadow-xs]:shadow-none">
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Type to search..." />
|
||||
<InputGroupAddon
|
||||
align="inline-start"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<IconBrandGithubCopilot />
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<IconCloudCode />
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="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 *:[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>
|
||||
</div>
|
||||
<FieldGroup className="grid grid-cols-2 gap-4 [--spacing:0.22rem]">
|
||||
<Field>
|
||||
<Label htmlFor="width">Width</Label>
|
||||
<ButtonGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="width" />
|
||||
<InputGroupAddon className="text-muted-foreground">
|
||||
W
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
px
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<Field className="w-full">
|
||||
<Label htmlFor="color">Color</Label>
|
||||
<ButtonGroup className="w-full">
|
||||
<InputGroup>
|
||||
<InputGroupInput id="color" />
|
||||
<InputGroupAddon align="inline-start">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupButton>
|
||||
<span className="size-4 rounded-xs bg-blue-600" />
|
||||
</InputGroupButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="max-w-48 rounded-lg p-2"
|
||||
alignOffset={-8}
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{[
|
||||
"#EA4335", // Red
|
||||
"#FBBC04", // Yellow
|
||||
"#34A853", // Green
|
||||
"#4285F4", // Blue
|
||||
"#9333EA", // Purple
|
||||
"#EC4899", // Pink
|
||||
"#10B981", // Emerald
|
||||
"#F97316", // Orange
|
||||
"#6366F1", // Indigo
|
||||
"#14B8A6", // Teal
|
||||
"#8B5CF6", // Violet
|
||||
"#F59E0B", // Amber
|
||||
].map((color) => (
|
||||
<div
|
||||
key={color}
|
||||
className="size-6 cursor-pointer rounded-sm transition-transform hover:scale-110"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon
|
||||
align="inline-end"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
%
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<IconHeart /> Like
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="text-muted-foreground pointer-events-none px-2"
|
||||
>
|
||||
<span>1.2K</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ExportButtonGroup />
|
||||
<ButtonGroup>
|
||||
<Select defaultValue="hours">
|
||||
<SelectTrigger id="duration">
|
||||
<SelectValue placeholder="Select duration" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
<SelectItem value="weeks">Weeks</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:9999rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Send a message..." />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<AudioLinesIcon />
|
||||
</InputGroupAddon>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Voice Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroup>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
<ArrowLeftIcon />
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
2
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
3
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
4
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
5
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Next
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="[--radius:0.9rem] [--spacing:0.22rem]">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">1</Button>
|
||||
<Button variant="outline">2</Button>
|
||||
<Button variant="outline">3</Button>
|
||||
<Button variant="outline">4</Button>
|
||||
<Button variant="outline">5</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup aria-label="Single navigation button">
|
||||
<Button variant="outline" size="icon">
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="flex max-w-xs flex-col gap-6">
|
||||
<Field>
|
||||
<Label id="alignment-label">Text Alignment</Label>
|
||||
<ButtonGroup aria-labelledby="alignment-label">
|
||||
<Button variant="outline" size="sm">
|
||||
Left
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Center
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Right
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Justify
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<div className="flex gap-6">
|
||||
<ButtonGroup
|
||||
orientation="vertical"
|
||||
aria-label="Media controls"
|
||||
className="h-fit"
|
||||
>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlus />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconMinus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical" aria-label="Design tools palette">
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline" size="icon">
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<CopyIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<ShareIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline" size="icon">
|
||||
<FlipHorizontalIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<FlipVerticalIcon />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<RotateCwIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline" size="sm">
|
||||
<IconPlus /> Increase
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<IconMinus /> Decrease
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="secondary" size="sm">
|
||||
<IconPlus /> Increase
|
||||
</Button>
|
||||
<ButtonGroupSeparator orientation="horizontal" />
|
||||
<Button variant="secondary" size="sm">
|
||||
<IconMinus /> Decrease
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ExportButtonGroup() {
|
||||
const [exportType, setExportType] = useState("pdf")
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Input />
|
||||
<Select value={exportType} onValueChange={setExportType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue asChild>
|
||||
<span>{exportType}</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="pdf">pdf</SelectItem>
|
||||
<SelectItem value="xlsx">xlsx</SelectItem>
|
||||
<SelectItem value="csv">csv</SelectItem>
|
||||
<SelectItem value="json">json</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -98,7 +98,10 @@ export function CardDemo() {
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="@leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
|
||||
@@ -62,7 +62,7 @@ const users = [
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
username: "leerob",
|
||||
username: "maxleiter",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
|
||||
@@ -274,7 +274,10 @@ function DropdownMenuAvatarOnly() {
|
||||
className="size-8 rounded-full border-none p-0"
|
||||
>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="maxleiter"
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
@@ -286,13 +289,16 @@ function DropdownMenuAvatarOnly() {
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/leerob.png" alt="leerob" />
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="maxleiter"
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">leerob</span>
|
||||
<span className="truncate font-semibold">maxleiter</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
leerob@example.com
|
||||
maxleiter@example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
250
apps/v4/app/(internal)/sink/components/empty-demo.tsx
Normal file
250
apps/v4/app/(internal)/sink/components/empty-demo.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { IconArrowUpRight, IconFolderCode } from "@tabler/icons-react"
|
||||
import { PlusIcon, SearchIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
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"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function EmptyDemo() {
|
||||
return (
|
||||
<div className="grid w-full gap-8">
|
||||
<Empty className="min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconFolderCode />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by creating
|
||||
your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button variant="link" asChild className="text-muted-foreground">
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Empty className="bg-muted min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>No results found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No results found for your search. Try adjusting your search terms.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Try again</Button>
|
||||
<Button variant="link" asChild className="text-muted-foreground">
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Empty className="min-h-[80svh] border">
|
||||
<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>
|
||||
<Empty className="min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Nothing to see here</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No posts have been created yet. Get started by{" "}
|
||||
<a href="#">creating your first post</a> to share with the
|
||||
community.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline">
|
||||
<PlusIcon />
|
||||
New Post
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<div className="bg-muted flex min-h-[800px] items-center justify-center rounded-lg p-20">
|
||||
<Card className="max-w-sm">
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="bg-muted flex min-h-[800px] items-center justify-center rounded-lg p-20">
|
||||
<Card className="max-w-sm">
|
||||
<CardContent>
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconFolderCode />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by
|
||||
creating your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
asChild
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<div className="flex gap-4">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
<DialogDescription>Dialog Description</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconFolderCode />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by
|
||||
creating your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
asChild
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<a href="#">
|
||||
Learn more <IconArrowUpRight />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Open Popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="rounded-2xl p-2">
|
||||
<Empty className="rounded-sm p-6">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Nothing to see here</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No posts have been created yet.{" "}
|
||||
<a href="#">Create your first post</a> to share with the
|
||||
community.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button variant="outline">
|
||||
<PlusIcon />
|
||||
New Post
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
4957
apps/v4/app/(internal)/sink/components/field-demo.tsx
Normal file
4957
apps/v4/app/(internal)/sink/components/field-demo.tsx
Normal file
File diff suppressed because it is too large
Load Diff
663
apps/v4/app/(internal)/sink/components/input-group-demo.tsx
Normal file
663
apps/v4/app/(internal)/sink/components/input-group-demo.tsx
Normal file
@@ -0,0 +1,663 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
IconBrandJavascript,
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
IconCopy,
|
||||
IconInfoCircle,
|
||||
IconLoader2,
|
||||
IconMicrophone,
|
||||
IconPlayerRecordFilled,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconServerSpark,
|
||||
IconStar,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpIcon,
|
||||
ChevronDownIcon,
|
||||
EyeClosedIcon,
|
||||
FlipVerticalIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
ButtonGroup,
|
||||
ButtonGroupText,
|
||||
} from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
const [country, setCountry] = useState("+1")
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-wrap gap-12 pb-72 *:[div]:w-full *:[div]:max-w-sm">
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-default-01">
|
||||
Default (No Input Group)
|
||||
</FieldLabel>
|
||||
<Input placeholder="Default" id="input-default-01" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-02">Input Group</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-02" placeholder="Default" />
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field data-disabled="true">
|
||||
<FieldLabel htmlFor="input-disabled-03">Disabled</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-disabled-03"
|
||||
placeholder="This field is disabled"
|
||||
disabled
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field data-invalid="true">
|
||||
<FieldLabel htmlFor="input-invalid-04">Invalid</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-invalid-04"
|
||||
placeholder="This field is invalid"
|
||||
aria-invalid="true"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-left-05">Icon (left)</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-left-05" />
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-left-06" />
|
||||
<InputGroupAddon>
|
||||
<FlipVerticalIcon className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-right-07">Icon (right)</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-right-07" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<EyeClosedIcon />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-right-08" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconLoader2 className="text-muted-foreground animate-spin" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-both-09">Icon (both)</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-both-09" />
|
||||
<InputGroupAddon>
|
||||
<IconMicrophone className="text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconPlayerRecordFilled className="animate-pulse text-red-500" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-icon-both-10">Multiple Icons</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-icon-both-10" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconStar />
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
onClick={() => toast.success("Copied to clipboard")}
|
||||
>
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon>
|
||||
<IconPlayerRecordFilled className="animate-pulse text-red-500" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-description-10">Description</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-description-10" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<FieldGroup className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-11">First Name</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-11" placeholder="First Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-12">Last Name</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-12" placeholder="Last Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-tooltip-20">Tooltip</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-tooltip-20" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InputGroupButton className="rounded-full" size="icon-xs">
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This is content in a tooltip.</TooltipContent>
|
||||
</Tooltip>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-dropdown-21">Dropdown</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-dropdown-21" />
|
||||
<InputGroupAddon>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton className="text-muted-foreground tabular-nums">
|
||||
{country} <ChevronDownIcon />
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="min-w-16"
|
||||
sideOffset={10}
|
||||
alignOffset={-8}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => setCountry("+1")}>
|
||||
+1
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setCountry("+44")}>
|
||||
+44
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setCountry("+46")}>
|
||||
+46
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-label-10">Label</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<FieldLabel htmlFor="input-label-10">Label</FieldLabel>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="input-label-10" />
|
||||
</InputGroup>
|
||||
<InputGroup className="gap-0">
|
||||
<InputGroupAddon>
|
||||
<FieldLabel
|
||||
htmlFor="input-prefix-11"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
example.com/
|
||||
</FieldLabel>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="input-prefix-11" />
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-optional-12" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>(optional)</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-button-13">Button</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-13" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton>Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-14" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="outline">Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-15" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="secondary">Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-16" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="secondary">Button</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-17" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton size="icon-xs">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-button-18" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="secondary" size="icon-xs">
|
||||
<IconTrash />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
<InputGroupButton variant="secondary" size="icon-xs">
|
||||
<IconInfoCircle />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
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="text-muted-foreground">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput id="input-secure-19" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
onClick={() => toast.success("Added to favorites")}
|
||||
>
|
||||
<IconStar />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-addon-20">Addon (block-start)</FieldLabel>
|
||||
<InputGroup className="h-auto">
|
||||
<InputGroupInput id="input-addon-20" />
|
||||
<InputGroupAddon align="block-start">
|
||||
<InputGroupText>First Name</InputGroupText>
|
||||
<IconInfoCircle className="text-muted-foreground ml-auto" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-addon-21">Addon (block-end)</FieldLabel>
|
||||
<InputGroup className="h-auto">
|
||||
<InputGroupInput id="input-addon-21" />
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupText>20/240 characters</InputGroupText>
|
||||
<IconInfoCircle className="text-muted-foreground ml-auto" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-comment-33">Default Button</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-comment-33"
|
||||
placeholder="Share your thoughts..."
|
||||
className="py-2.5"
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="sm">
|
||||
Button
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="size-8">
|
||||
<IconChevronDown />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button variant="ghost" className="ml-auto" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="default" size="sm">
|
||||
Post <ArrowRightIcon />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-kbd-22">Input Group with Kbd</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-kbd-22" />
|
||||
<InputGroupAddon>
|
||||
<Kbd>⌘K</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-kbd-23" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>⌘K</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-search-apps-24"
|
||||
placeholder="Search for Apps..."
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">Ask AI</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>Tab</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-search-type-25"
|
||||
placeholder="Type to search..."
|
||||
/>
|
||||
<InputGroupAddon align="inline-start">
|
||||
<IconServerSpark />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>C</Kbd>
|
||||
</KbdGroup>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-username-26">Username</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-username-26" defaultValue="shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-green-500 dark:bg-green-800">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription className="text-green-700">
|
||||
This username is available.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-search-docs-27"
|
||||
placeholder="Search documentation..."
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<IconSearch />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup data-disabled="true">
|
||||
<InputGroupInput
|
||||
id="input-search-disabled-28"
|
||||
placeholder="Search documentation..."
|
||||
disabled
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<IconSearch />
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">Disabled</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="url">With Button Group</FieldLabel>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>https://</ButtonGroupText>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="url" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<IconInfoCircle />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<ButtonGroupText>.com</ButtonGroupText>
|
||||
</ButtonGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field data-disabled="true">
|
||||
<FieldLabel htmlFor="input-group-29">Loading</FieldLabel>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="input-group-29"
|
||||
disabled
|
||||
defaultValue="shadcn"
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Spinner />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-code-32">Code Editor</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-code-32"
|
||||
placeholder="console.log('Hello, world!');"
|
||||
className="min-h-[300px] py-3"
|
||||
/>
|
||||
<InputGroupAddon align="block-start" className="border-b">
|
||||
<InputGroupText className="font-mono font-medium">
|
||||
<IconBrandJavascript />
|
||||
script.js
|
||||
</InputGroupText>
|
||||
<InputGroupButton size="icon-xs" className="ml-auto">
|
||||
<IconRefresh />
|
||||
</InputGroupButton>
|
||||
<InputGroupButton size="icon-xs" variant="ghost">
|
||||
<IconCopy />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="block-end" className="border-t">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupText className="ml-auto">JavaScript</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-header-footer-12">Default</FieldLabel>
|
||||
<Textarea
|
||||
id="textarea-header-footer-12"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-header-footer-13">
|
||||
Input Group
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-13"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field data-invalid="true">
|
||||
<FieldLabel htmlFor="textarea-header-footer-14">Invalid</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-14"
|
||||
placeholder="Enter your text here..."
|
||||
aria-invalid="true"
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field data-disabled="true">
|
||||
<FieldLabel htmlFor="textarea-header-footer-15">Disabled</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-15"
|
||||
placeholder="Enter your text here..."
|
||||
disabled
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-header-footer-30">Textarea</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-header-footer-30"
|
||||
placeholder="Enter your text here..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupText>0/280 characters</InputGroupText>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
size="icon-xs"
|
||||
className="ml-auto rounded-full"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="prompt-31">Enter your prompt</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="prompt-31"
|
||||
placeholder="Ask, Search or Chat..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<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">
|
||||
12 messages left
|
||||
</InputGroupText>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
size="icon-xs"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
This is a description of the input group.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="textarea-comment-31">Comment Box</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="textarea-comment-31"
|
||||
placeholder="Share your thoughts..."
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton variant="ghost" className="ml-auto" size="sm">
|
||||
Cancel
|
||||
</InputGroupButton>
|
||||
<InputGroupButton variant="default" size="sm">
|
||||
Post Comment
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
392
apps/v4/app/(internal)/sink/components/item-demo.tsx
Normal file
392
apps/v4/app/(internal)/sink/components/item-demo.tsx
Normal file
@@ -0,0 +1,392 @@
|
||||
import * as React from "react"
|
||||
import Image from "next/image"
|
||||
import { IconChevronRight, IconDownload } from "@tabler/icons-react"
|
||||
import { PlusIcon, TicketIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemFooter,
|
||||
ItemGroup,
|
||||
ItemHeader,
|
||||
ItemMedia,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
import { Progress } from "@/registry/new-york-v4/ui/progress"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
const people = [
|
||||
{
|
||||
username: "shadcn",
|
||||
avatar: "https://github.com/shadcn.png",
|
||||
message: "Just shipped a component that fixes itself",
|
||||
},
|
||||
{
|
||||
username: "pranathip",
|
||||
avatar: "https://github.com/pranathip.png",
|
||||
message: "My code is so clean, it does its own laundry",
|
||||
},
|
||||
{
|
||||
username: "evilrabbit",
|
||||
avatar: "https://github.com/evilrabbit.png",
|
||||
message:
|
||||
"Debugging is like being a detective in a crime movie where you're also the murderer",
|
||||
},
|
||||
{
|
||||
username: "maxleiter",
|
||||
avatar: "https://github.com/maxleiter.png",
|
||||
message:
|
||||
"I don't always test my code, but when I do, I test it in production",
|
||||
},
|
||||
]
|
||||
|
||||
const music = [
|
||||
{
|
||||
title: "Midnight City Lights",
|
||||
artist: "Neon Dreams",
|
||||
album: "Electric Nights",
|
||||
duration: "3:45",
|
||||
},
|
||||
{
|
||||
title: "Coffee Shop Conversations",
|
||||
artist: "The Morning Brew",
|
||||
album: "Urban Stories",
|
||||
duration: "4:05",
|
||||
},
|
||||
{
|
||||
title: "Digital Rain",
|
||||
artist: "Cyber Symphony",
|
||||
album: "Binary Beats",
|
||||
duration: "3:30",
|
||||
},
|
||||
{
|
||||
title: "Sunset Boulevard",
|
||||
artist: "Golden Hour",
|
||||
album: "California Dreams",
|
||||
duration: "3:55",
|
||||
},
|
||||
{
|
||||
title: "Neon Sign Romance",
|
||||
artist: "Retro Wave",
|
||||
album: "80s Forever",
|
||||
duration: "4:10",
|
||||
},
|
||||
{
|
||||
title: "Ocean Depths",
|
||||
artist: "Deep Blue",
|
||||
album: "Underwater Symphony",
|
||||
duration: "3:40",
|
||||
},
|
||||
{
|
||||
title: "Space Station Alpha",
|
||||
artist: "Cosmic Explorers",
|
||||
album: "Galactic Journey",
|
||||
duration: "3:50",
|
||||
},
|
||||
{
|
||||
title: "Forest Whispers",
|
||||
artist: "Nature's Choir",
|
||||
album: "Woodland Tales",
|
||||
duration: "3:35",
|
||||
},
|
||||
]
|
||||
|
||||
const issues = [
|
||||
{
|
||||
number: 1247,
|
||||
date: "March 15, 2024",
|
||||
title:
|
||||
"Button component doesn't respect disabled state when using custom variants",
|
||||
description:
|
||||
"When applying custom variants to the Button component, the disabled prop is ignored and the button remains clickable. This affects accessibility and user experience.",
|
||||
},
|
||||
{
|
||||
number: 892,
|
||||
date: "February 8, 2024",
|
||||
title: "Dialog component causes scroll lock on mobile devices",
|
||||
description:
|
||||
"The Dialog component prevents scrolling on the background content but doesn't restore scroll position properly on mobile Safari and Chrome, causing layout shifts.",
|
||||
},
|
||||
{
|
||||
number: 1156,
|
||||
date: "January 22, 2024",
|
||||
title: "TypeScript errors with Select component in strict mode",
|
||||
description:
|
||||
"Using the Select component with TypeScript strict mode enabled throws type errors related to generic constraints and value prop typing.",
|
||||
},
|
||||
{
|
||||
number: 734,
|
||||
date: "December 3, 2023",
|
||||
title: "Dark mode toggle causes flash of unstyled content",
|
||||
description:
|
||||
"When switching between light and dark themes, there's a brief moment where components render with incorrect styling before the theme transition completes.",
|
||||
},
|
||||
{
|
||||
number: 1389,
|
||||
date: "April 2, 2024",
|
||||
title: "Form validation messages overlap with floating labels",
|
||||
description:
|
||||
"Error messages in Form components with floating labels appear underneath the label text, making them difficult to read. Need better positioning logic for validation feedback.",
|
||||
},
|
||||
]
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
<div className="@container w-full">
|
||||
<div className="flex flex-wrap gap-6 2xl:gap-12">
|
||||
<div className="flex max-w-sm flex-col gap-6">
|
||||
<Item>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline">Button</Button>
|
||||
<Button variant="outline">Button</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia variant="icon">
|
||||
<TicketIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Purchase</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="muted">
|
||||
<ItemMedia variant="icon">
|
||||
<TicketIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Item Title</ItemTitle>
|
||||
<ItemDescription>Item Description</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button size="sm">Upgrade</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<FieldLabel>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Field Title</FieldTitle>
|
||||
<FieldDescription>Field Description</FieldDescription>
|
||||
</FieldContent>
|
||||
<Button variant="outline">Button</Button>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
<div className="flex max-w-sm flex-col gap-6">
|
||||
<ItemGroup>
|
||||
{people.map((person, index) => (
|
||||
<React.Fragment key={person.username}>
|
||||
<Item>
|
||||
<ItemMedia>
|
||||
<Avatar>
|
||||
<AvatarImage src={person.avatar} />
|
||||
<AvatarFallback>
|
||||
{person.username.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>{person.username}</ItemTitle>
|
||||
<ItemDescription>{person.message}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8 rounded-full"
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
{index !== people.length - 1 && <ItemSeparator />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]: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>
|
||||
</div>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Design Department</ItemTitle>
|
||||
<ItemDescription>
|
||||
Meet our team of designers, engineers, and researchers.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions className="self-start">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8 rounded-full"
|
||||
>
|
||||
<IconChevronRight />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemHeader>Your download has started.</ItemHeader>
|
||||
<ItemMedia variant="icon">
|
||||
<Spinner />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Downloading...</ItemTitle>
|
||||
<ItemDescription>129 MB / 1000 MB</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</ItemActions>
|
||||
<ItemFooter>
|
||||
<Progress value={50} />
|
||||
</ItemFooter>
|
||||
</Item>
|
||||
</div>
|
||||
<div className="flex max-w-lg flex-col gap-6">
|
||||
<ItemGroup className="gap-4">
|
||||
{music.map((song) => (
|
||||
<Item key={song.title} variant="outline" asChild role="listitem">
|
||||
<a href="#">
|
||||
<ItemMedia variant="image">
|
||||
<Image
|
||||
src={`https://avatar.vercel.sh/${song.title}`}
|
||||
alt={song.title}
|
||||
width={32}
|
||||
height={32}
|
||||
className="grayscale"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle className="line-clamp-1">
|
||||
{song.title} -{" "}
|
||||
<span className="text-muted-foreground">
|
||||
{song.album}
|
||||
</span>
|
||||
</ItemTitle>
|
||||
<ItemDescription>{song.artist}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemContent className="flex-none text-center">
|
||||
<ItemDescription>{song.duration}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 rounded-full"
|
||||
aria-label="Download"
|
||||
>
|
||||
<IconDownload />
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</a>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</div>
|
||||
<div className="flex max-w-lg flex-col gap-6">
|
||||
<ItemGroup>
|
||||
{issues.map((issue) => (
|
||||
<React.Fragment key={issue.number}>
|
||||
<Item asChild className="rounded-none">
|
||||
<a href="#">
|
||||
<ItemContent>
|
||||
<ItemTitle className="line-clamp-1">
|
||||
{issue.title}
|
||||
</ItemTitle>
|
||||
<ItemDescription>{issue.description}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemContent className="self-start">
|
||||
#{issue.number}
|
||||
</ItemContent>
|
||||
</a>
|
||||
</Item>
|
||||
<ItemSeparator />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
apps/v4/app/(internal)/sink/components/kbd-demo.tsx
Normal file
103
apps/v4/app/(internal)/sink/components/kbd-demo.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"
|
||||
import { CommandIcon, WavesIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function KbdDemo() {
|
||||
return (
|
||||
<div className="flex max-w-xs flex-col items-start gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>⌘K</Kbd>
|
||||
<Kbd>Ctrl + B</Kbd>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Kbd>⌘</Kbd>
|
||||
<Kbd>C</Kbd>
|
||||
</div>
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>Shift</Kbd>
|
||||
<Kbd>P</Kbd>
|
||||
</KbdGroup>
|
||||
<div className="flex items-center gap-2">
|
||||
<Kbd>↑</Kbd>
|
||||
<Kbd>↓</Kbd>
|
||||
<Kbd>←</Kbd>
|
||||
<Kbd>→</Kbd>
|
||||
</div>
|
||||
<KbdGroup>
|
||||
<Kbd>
|
||||
<CommandIcon />
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<IconArrowLeft />
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<IconArrowRight />
|
||||
</Kbd>
|
||||
</KbdGroup>
|
||||
<KbdGroup>
|
||||
<Kbd>
|
||||
<IconArrowLeft />
|
||||
Left
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<WavesIcon />
|
||||
Voice Enabled
|
||||
</Kbd>
|
||||
</KbdGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput />
|
||||
<InputGroupAddon>
|
||||
<Kbd>Space</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<ButtonGroup>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
Save
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="flex items-center gap-2">
|
||||
Save Changes <Kbd>S</Kbd>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
Print
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="flex items-center gap-2">
|
||||
Print Document{" "}
|
||||
<KbdGroup>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Kbd>P</Kbd>
|
||||
</KbdGroup>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
<Kbd>
|
||||
<samp>File</samp>
|
||||
</Kbd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
106
apps/v4/app/(internal)/sink/components/spinner-demo.tsx
Normal file
106
apps/v4/app/(internal)/sink/components/spinner-demo.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerDemo() {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-12">
|
||||
<div className="flex items-center gap-6">
|
||||
<Spinner />
|
||||
<Spinner className="size-8" />
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<Button>
|
||||
<Spinner /> Submit
|
||||
</Button>
|
||||
<Button disabled>
|
||||
<Spinner /> Disabled
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<Spinner /> Small
|
||||
</Button>
|
||||
<Button variant="outline" disabled>
|
||||
<Spinner /> Outline
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" disabled>
|
||||
<Spinner />
|
||||
<span className="sr-only">Loading...</span>
|
||||
</Button>
|
||||
<Button variant="destructive" disabled>
|
||||
<Spinner />
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<Badge>
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="destructive">
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
<Spinner />
|
||||
Badge
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex max-w-xs items-center gap-6">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="input-group-spinner">Input Group</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput id="input-group-spinner" />
|
||||
<InputGroupAddon>
|
||||
<Spinner />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<Empty className="min-h-[80svh]">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Spinner />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You haven't created any projects yet. Get started by creating
|
||||
your first project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild>
|
||||
<a href="#">Create project</a>
|
||||
</Button>
|
||||
<Button variant="outline">Import project</Button>
|
||||
</div>
|
||||
<Button variant="link" asChild className="text-muted-foreground">
|
||||
<a href="#">
|
||||
Learn more <ArrowRightIcon />
|
||||
</a>
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
147
apps/v4/app/(internal)/sink/components/theme-selector.tsx
Normal file
147
apps/v4/app/(internal)/sink/components/theme-selector.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useThemeConfig } from "@/components/active-theme"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
|
||||
const THEMES = {
|
||||
sizes: [
|
||||
{
|
||||
name: "Default",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Scaled",
|
||||
value: "scaled",
|
||||
},
|
||||
{
|
||||
name: "Mono",
|
||||
value: "mono",
|
||||
},
|
||||
],
|
||||
colors: [
|
||||
{
|
||||
name: "Blue",
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
value: "green",
|
||||
},
|
||||
{
|
||||
name: "Amber",
|
||||
value: "amber",
|
||||
},
|
||||
{
|
||||
name: "Rose",
|
||||
value: "rose",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
value: "purple",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
value: "orange",
|
||||
},
|
||||
{
|
||||
name: "Teal",
|
||||
value: "teal",
|
||||
},
|
||||
],
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
value: "inter",
|
||||
},
|
||||
{
|
||||
name: "Noto Sans",
|
||||
value: "noto-sans",
|
||||
},
|
||||
{
|
||||
name: "Nunito Sans",
|
||||
value: "nunito-sans",
|
||||
},
|
||||
{
|
||||
name: "Figtree",
|
||||
value: "figtree",
|
||||
},
|
||||
],
|
||||
radius: [
|
||||
{
|
||||
name: "None",
|
||||
value: "rounded-none",
|
||||
},
|
||||
{
|
||||
name: "Small",
|
||||
value: "rounded-small",
|
||||
},
|
||||
{
|
||||
name: "Medium",
|
||||
value: "rounded-medium",
|
||||
},
|
||||
{
|
||||
name: "Large",
|
||||
value: "rounded-large",
|
||||
},
|
||||
{
|
||||
name: "Full",
|
||||
value: "rounded-full",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export function ThemeSelector({ className }: React.ComponentProps<"div">) {
|
||||
const { activeTheme, setActiveTheme } = useThemeConfig()
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<Label htmlFor="theme-selector" className="sr-only">
|
||||
Theme
|
||||
</Label>
|
||||
<Select value={activeTheme} onValueChange={setActiveTheme}>
|
||||
<SelectTrigger
|
||||
id="theme-selector"
|
||||
size="sm"
|
||||
className="bg-secondary text-secondary-foreground border-secondary justify-start shadow-none *:data-[slot=select-value]:w-16"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{Object.entries(THEMES).map(
|
||||
([key, themes], index) =>
|
||||
themes.length > 0 && (
|
||||
<div key={key}>
|
||||
{index > 0 && <SelectSeparator />}
|
||||
<SelectGroup>
|
||||
<SelectLabel>
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</SelectLabel>
|
||||
{themes.map((theme) => (
|
||||
<SelectItem
|
||||
key={theme.name}
|
||||
value={theme.value}
|
||||
className="data-[state=checked]:opacity-50"
|
||||
>
|
||||
{theme.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
66
apps/v4/app/(internal)/sink/layout.tsx
Normal file
66
apps/v4/app/(internal)/sink/layout.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Figtree, Inter, Noto_Sans, Nunito_Sans } from "next/font/google"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ModeSwitcher } from "@/components/mode-switcher"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { AppBreadcrumbs } from "@/app/(internal)/sink/components/app-breadcrumbs"
|
||||
import { AppSidebar } from "@/app/(internal)/sink/components/app-sidebar"
|
||||
import { ThemeSelector } from "@/app/(internal)/sink/components/theme-selector"
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
export default async function SinkLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<SidebarProvider
|
||||
defaultOpen={true}
|
||||
className={cn(
|
||||
"theme-container",
|
||||
inter.variable,
|
||||
notoSans.variable,
|
||||
nunitoSans.variable,
|
||||
figtree.variable
|
||||
)}
|
||||
>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="bg-background sticky top-0 z-10 flex h-14 items-center border-b p-4">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" className="mr-4 ml-2 !h-4" />
|
||||
<AppBreadcrumbs />
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<ModeSwitcher />
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +1,7 @@
|
||||
import { Metadata } from "next"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
import { ThemeSelector } from "@/components/theme-selector"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { AccordionDemo } from "@/app/(internal)/sink/components/accordion-demo"
|
||||
import { AlertDemo } from "@/app/(internal)/sink/components/alert-demo"
|
||||
import { AlertDialogDemo } from "@/app/(internal)/sink/components/alert-dialog-demo"
|
||||
import { AppSidebar } from "@/app/(internal)/sink/components/app-sidebar"
|
||||
import { AspectRatioDemo } from "@/app/(internal)/sink/components/aspect-ratio-demo"
|
||||
import { AvatarDemo } from "@/app/(internal)/sink/components/avatar-demo"
|
||||
import { BadgeDemo } from "@/app/(internal)/sink/components/badge-demo"
|
||||
import { BreadcrumbDemo } from "@/app/(internal)/sink/components/breadcrumb-demo"
|
||||
import { ButtonDemo } from "@/app/(internal)/sink/components/button-demo"
|
||||
import { CalendarDemo } from "@/app/(internal)/sink/components/calendar-demo"
|
||||
import { CardDemo } from "@/app/(internal)/sink/components/card-demo"
|
||||
import { CarouselDemo } from "@/app/(internal)/sink/components/carousel-demo"
|
||||
import { ChartDemo } from "@/app/(internal)/sink/components/chart-demo"
|
||||
import { CheckboxDemo } from "@/app/(internal)/sink/components/checkbox-demo"
|
||||
import { CollapsibleDemo } from "@/app/(internal)/sink/components/collapsible-demo"
|
||||
import { ComboboxDemo } from "@/app/(internal)/sink/components/combobox-demo"
|
||||
import { CommandDemo } from "@/app/(internal)/sink/components/command-demo"
|
||||
import { componentRegistry } from "@/app/(internal)/sink/component-registry"
|
||||
import { ComponentWrapper } from "@/app/(internal)/sink/components/component-wrapper"
|
||||
import { ContextMenuDemo } from "@/app/(internal)/sink/components/context-menu-demo"
|
||||
import { DatePickerDemo } from "@/app/(internal)/sink/components/date-picker-demo"
|
||||
import { DialogDemo } from "@/app/(internal)/sink/components/dialog-demo"
|
||||
import { DrawerDemo } from "@/app/(internal)/sink/components/drawer-demo"
|
||||
import { DropdownMenuDemo } from "@/app/(internal)/sink/components/dropdown-menu-demo"
|
||||
import { FormDemo } from "@/app/(internal)/sink/components/form-demo"
|
||||
import { HoverCardDemo } from "@/app/(internal)/sink/components/hover-card-demo"
|
||||
import { InputDemo } from "@/app/(internal)/sink/components/input-demo"
|
||||
import { InputOTPDemo } from "@/app/(internal)/sink/components/input-otp-demo"
|
||||
import { LabelDemo } from "@/app/(internal)/sink/components/label-demo"
|
||||
import { MenubarDemo } from "@/app/(internal)/sink/components/menubar-demo"
|
||||
import { NavigationMenuDemo } from "@/app/(internal)/sink/components/navigation-menu-demo"
|
||||
import { PaginationDemo } from "@/app/(internal)/sink/components/pagination-demo"
|
||||
import { PopoverDemo } from "@/app/(internal)/sink/components/popover-demo"
|
||||
import { ProgressDemo } from "@/app/(internal)/sink/components/progress-demo"
|
||||
import { RadioGroupDemo } from "@/app/(internal)/sink/components/radio-group-demo"
|
||||
import { ResizableDemo } from "@/app/(internal)/sink/components/resizable-demo"
|
||||
import { ScrollAreaDemo } from "@/app/(internal)/sink/components/scroll-area-demo"
|
||||
import { SelectDemo } from "@/app/(internal)/sink/components/select-demo"
|
||||
import { SeparatorDemo } from "@/app/(internal)/sink/components/separator-demo"
|
||||
import { SheetDemo } from "@/app/(internal)/sink/components/sheet-demo"
|
||||
import { SkeletonDemo } from "@/app/(internal)/sink/components/skeleton-demo"
|
||||
import { SliderDemo } from "@/app/(internal)/sink/components/slider-demo"
|
||||
import { SonnerDemo } from "@/app/(internal)/sink/components/sonner-demo"
|
||||
import { SwitchDemo } from "@/app/(internal)/sink/components/switch-demo"
|
||||
import { TableDemo } from "@/app/(internal)/sink/components/table-demo"
|
||||
import { TabsDemo } from "@/app/(internal)/sink/components/tabs-demo"
|
||||
import { TextareaDemo } from "@/app/(internal)/sink/components/textarea-demo"
|
||||
import { ToggleDemo } from "@/app/(internal)/sink/components/toggle-demo"
|
||||
import { ToggleGroupDemo } from "@/app/(internal)/sink/components/toggle-group-demo"
|
||||
import { TooltipDemo } from "@/app/(internal)/sink/components/tooltip-demo"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
@@ -66,164 +11,25 @@ export const metadata: Metadata = {
|
||||
description: "A page with all components for testing purposes.",
|
||||
}
|
||||
|
||||
export default async function SinkPage() {
|
||||
const cookieStore = await cookies()
|
||||
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
|
||||
|
||||
export default function SinkPage() {
|
||||
return (
|
||||
<SidebarProvider defaultOpen={defaultOpen} className="theme-container">
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="bg-background sticky top-0 z-10 flex h-14 items-center border-b p-4">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" className="mr-4 ml-2 !h-4" />
|
||||
<h1 className="text-base font-medium">Kitchen Sink</h1>
|
||||
<ThemeSelector className="ml-auto" />
|
||||
</header>
|
||||
<div className="@container grid flex-1 gap-4 p-4">
|
||||
<ComponentWrapper name="accordion">
|
||||
<AccordionDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="alert">
|
||||
<AlertDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="alert-dialog">
|
||||
<AlertDialogDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="aspect-ratio">
|
||||
<AspectRatioDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="avatar">
|
||||
<AvatarDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="badge">
|
||||
<BadgeDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="breadcrumb">
|
||||
<BreadcrumbDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="button">
|
||||
<ButtonDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="calendar">
|
||||
<CalendarDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="card">
|
||||
<CardDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="carousel">
|
||||
<CarouselDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="chart" className="w-full">
|
||||
<ChartDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="checkbox">
|
||||
<CheckboxDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="collapsible">
|
||||
<CollapsibleDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="combobox">
|
||||
<ComboboxDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="command">
|
||||
<CommandDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="context-menu">
|
||||
<ContextMenuDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="date-picker">
|
||||
<DatePickerDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="dialog">
|
||||
<DialogDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="drawer">
|
||||
<DrawerDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="dropdown-menu">
|
||||
<DropdownMenuDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="form">
|
||||
<FormDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="hover-card">
|
||||
<HoverCardDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="input">
|
||||
<InputDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="input-otp">
|
||||
<InputOTPDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="label">
|
||||
<LabelDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="menubar">
|
||||
<MenubarDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="navigation-menu">
|
||||
<NavigationMenuDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="pagination">
|
||||
<PaginationDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="popover">
|
||||
<PopoverDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="progress">
|
||||
<ProgressDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="radio-group">
|
||||
<RadioGroupDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="resizable">
|
||||
<ResizableDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="scroll-area">
|
||||
<ScrollAreaDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="select">
|
||||
<SelectDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="separator">
|
||||
<SeparatorDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="sheet">
|
||||
<SheetDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="skeleton">
|
||||
<SkeletonDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="slider">
|
||||
<SliderDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="sonner">
|
||||
<SonnerDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="switch">
|
||||
<SwitchDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="table">
|
||||
<TableDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="tabs">
|
||||
<TabsDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="textarea">
|
||||
<TextareaDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="toggle">
|
||||
<ToggleDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="toggle-group">
|
||||
<ToggleGroupDemo />
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper name="tooltip">
|
||||
<TooltipDemo />
|
||||
</ComponentWrapper>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<div className="@container grid flex-1 gap-4 p-4">
|
||||
{Object.entries(componentRegistry)
|
||||
.filter(([, component]) => {
|
||||
return component.type === "registry:ui"
|
||||
})
|
||||
.map(([key, component]) => {
|
||||
const Component = component.component
|
||||
return (
|
||||
<ComponentWrapper
|
||||
key={key}
|
||||
name={key}
|
||||
className={component.className || ""}
|
||||
>
|
||||
<Component />
|
||||
</ComponentWrapper>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
import { registryItemSchema } from "shadcn/registry"
|
||||
import { registryItemSchema } from "shadcn/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
|
||||
@@ -84,13 +84,13 @@ export default function RootLayout({
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
"text-foreground group/body overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
|
||||
"text-foreground group/body theme-blue overscroll-none font-sans antialiased [--footer-height:calc(var(--spacing)*14)] [--header-height:calc(var(--spacing)*14)] xl:[--footer-height:calc(var(--spacing)*24)]",
|
||||
fontVariables
|
||||
)}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<LayoutProvider>
|
||||
<ActiveThemeProvider>
|
||||
<ActiveThemeProvider initialTheme="blue">
|
||||
{children}
|
||||
<TailwindIndicator />
|
||||
<Toaster position="top-center" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from "react"
|
||||
|
||||
const DEFAULT_THEME = "default"
|
||||
const DEFAULT_THEME = "blue"
|
||||
|
||||
type ThemeContextType = {
|
||||
activeTheme: string
|
||||
|
||||
@@ -6,8 +6,9 @@ import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
export function Announcement() {
|
||||
return (
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<Link href="/docs/components/calendar">
|
||||
New Calendar Component <ArrowRightIcon />
|
||||
<Link href="/docs/changelog">
|
||||
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
|
||||
New Components: Field, Input Group, Item and more <ArrowRightIcon />
|
||||
</Link>
|
||||
</Badge>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react"
|
||||
import { registryItemFileSchema } from "shadcn/registry"
|
||||
import { registryItemFileSchema } from "shadcn/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
import { highlightCode } from "@/lib/highlight-code"
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Terminal,
|
||||
} from "lucide-react"
|
||||
import { ImperativePanelHandle } from "react-resizable-panels"
|
||||
import { registryItemFileSchema, registryItemSchema } from "shadcn/registry"
|
||||
import { registryItemFileSchema, registryItemSchema } from "shadcn/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
import { trackEvent } from "@/lib/events"
|
||||
|
||||
@@ -10,12 +10,17 @@ export function Callout({
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Alert> & { icon?: React.ReactNode }) {
|
||||
}: React.ComponentProps<typeof Alert> & {
|
||||
icon?: React.ReactNode
|
||||
variant?: "default" | "info" | "warning"
|
||||
}) {
|
||||
return (
|
||||
<Alert
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-4",
|
||||
"bg-background text-foreground mt-6 w-auto border md:-mx-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -32,7 +32,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -159,23 +164,25 @@ export function CardsChat() {
|
||||
}}
|
||||
className="relative w-full"
|
||||
>
|
||||
<Input
|
||||
id="message"
|
||||
placeholder="Type your message..."
|
||||
className="flex-1 pr-10"
|
||||
autoComplete="off"
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="icon"
|
||||
className="absolute top-1/2 right-2 size-6 -translate-y-1/2 rounded-full"
|
||||
disabled={inputLength === 0}
|
||||
>
|
||||
<ArrowUpIcon className="size-3.5" />
|
||||
<span className="sr-only">Send</span>
|
||||
</Button>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="message"
|
||||
placeholder="Type your message..."
|
||||
autoComplete="off"
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
type="submit"
|
||||
size="icon-xs"
|
||||
className="rounded-full"
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@@ -5,11 +5,15 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function CardsCookieSettings() {
|
||||
@@ -20,32 +24,20 @@ export function CardsCookieSettings() {
|
||||
<CardDescription>Manage your cookie settings here.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<Label htmlFor="necessary" className="flex flex-col items-start">
|
||||
<span>Strictly Necessary</span>
|
||||
<span className="text-muted-foreground leading-snug font-normal">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="necessary">Strictly Necessary</FieldLabel>
|
||||
<FieldDescription>
|
||||
These cookies are essential in order to use the website and use
|
||||
its features.
|
||||
</span>
|
||||
</Label>
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="necessary" defaultChecked aria-label="Necessary" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<Label htmlFor="functional" className="flex flex-col items-start">
|
||||
<span>Functional Cookies</span>
|
||||
<span className="text-muted-foreground leading-snug font-normal">
|
||||
These cookies allow the website to provide personalized
|
||||
functionality.
|
||||
</span>
|
||||
</Label>
|
||||
<Switch id="functional" aria-label="Functional" />
|
||||
</div>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button variant="outline">Save preferences</Button>
|
||||
</Field>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" className="w-full">
|
||||
Save preferences
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSeparator,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
|
||||
export function CardsCreateAccount() {
|
||||
return (
|
||||
@@ -21,53 +25,48 @@ export function CardsCreateAccount() {
|
||||
Enter your email below to create your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Button variant="outline">
|
||||
<svg viewBox="0 0 438.549 438.549">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
GitHub
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<svg role="img" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
/>
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card text-muted-foreground px-2">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="email-create-account">Email</Label>
|
||||
<Input
|
||||
id="email-create-account"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="password-create-account">Password</Label>
|
||||
<Input id="password-create-account" type="password" />
|
||||
</div>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field className="grid grid-cols-2 gap-6">
|
||||
<Button variant="outline">
|
||||
<svg viewBox="0 0 438.549 438.549">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
GitHub
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<svg role="img" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
/>
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
</Field>
|
||||
<FieldSeparator className="*:data-[slot=field-separator-content]:bg-card">
|
||||
Or continue with
|
||||
</FieldSeparator>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email-create-account">Email</FieldLabel>
|
||||
<Input
|
||||
id="email-create-account"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="password-create-account">Password</FieldLabel>
|
||||
<Input id="password-create-account" type="password" />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button>Create Account</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Create account</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,21 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
@@ -22,7 +30,7 @@ const plans = [
|
||||
{
|
||||
id: "starter",
|
||||
name: "Starter Plan",
|
||||
description: "Perfect for small businesses.",
|
||||
description: "For small businesses.",
|
||||
price: "$10",
|
||||
},
|
||||
{
|
||||
@@ -37,91 +45,96 @@ export function CardsForms() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Upgrade your subscription</CardTitle>
|
||||
<CardTitle className="text-lg">Upgrade your Subscription</CardTitle>
|
||||
<CardDescription className="text-balance">
|
||||
You are currently on the free plan. Upgrade to the pro plan to get
|
||||
access to all features.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3 md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" placeholder="Evil Rabbit" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" placeholder="example@acme.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="card-number">Card Number</Label>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-[1fr_80px_60px]">
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="1234 1234 1234 1234"
|
||||
className="col-span-2 md:col-span-1"
|
||||
/>
|
||||
<Input id="card-number-expiry" placeholder="MM/YY" />
|
||||
<Input id="card-number-cvc" placeholder="CVC" />
|
||||
</div>
|
||||
</div>
|
||||
<fieldset className="flex flex-col gap-3">
|
||||
<legend className="text-sm font-medium">Plan</legend>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Select the plan that best fits your needs.
|
||||
</p>
|
||||
<RadioGroup
|
||||
defaultValue="starter"
|
||||
className="grid gap-3 md:grid-cols-2"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<Label
|
||||
className="has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-input/20 flex items-start gap-3 rounded-lg border p-3"
|
||||
key={plan.id}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={plan.id}
|
||||
id={plan.name}
|
||||
className="data-[state=checked]:border-primary"
|
||||
/>
|
||||
<div className="grid gap-1 font-normal">
|
||||
<div className="font-medium">{plan.name}</div>
|
||||
<div className="text-muted-foreground text-xs leading-snug text-balance">
|
||||
{plan.description}
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="notes">Notes</Label>
|
||||
<Textarea id="notes" placeholder="Enter notes" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="terms" />
|
||||
<Label htmlFor="terms" className="font-normal">
|
||||
I agree to the terms and conditions
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="newsletter" defaultChecked />
|
||||
<Label htmlFor="newsletter" className="font-normal">
|
||||
Allow us to send you emails
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldGroup className="grid grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input id="name" placeholder="Max Leiter" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" placeholder="mail@acme.com" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup className="grid grid-cols-2 gap-3 md:grid-cols-[1fr_80px_60px]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="card-number">Card Number</FieldLabel>
|
||||
<Input
|
||||
id="card-number"
|
||||
placeholder="1234 1234 1234 1234"
|
||||
className="col-span-2 md:col-span-1"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="card-number-expiry">
|
||||
Expiry Date
|
||||
</FieldLabel>
|
||||
<Input id="card-number-expiry" placeholder="MM/YY" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="card-number-cvc">CVC</FieldLabel>
|
||||
<Input id="card-number-cvc" placeholder="CVC" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the plan that best fits your needs.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
defaultValue="starter"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<FieldLabel key={plan.id}>
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>{plan.name}</FieldTitle>
|
||||
<FieldDescription className="text-xs">
|
||||
{plan.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem value={plan.id} id={plan.name} />
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notes">Notes</FieldLabel>
|
||||
<Textarea id="notes" placeholder="Enter notes" />
|
||||
</Field>
|
||||
<Field>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="terms" />
|
||||
<FieldLabel htmlFor="terms" className="font-normal">
|
||||
I agree to the terms and conditions
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="newsletter" defaultChecked />
|
||||
<FieldLabel htmlFor="newsletter" className="font-normal">
|
||||
Allow us to send you emails
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</Field>
|
||||
<Field orientation="horizontal">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Upgrade Plan</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Upgrade Plan</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ export function CardsPayments() {
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="rounded-md border">
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
|
||||
@@ -7,12 +7,11 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -33,65 +32,69 @@ export function CardsReportIssue() {
|
||||
What area are you having problems with?
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`area-${id}`}>Area</Label>
|
||||
<Select defaultValue="billing">
|
||||
<SelectTrigger
|
||||
id={`area-${id}`}
|
||||
aria-label="Area"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
<SelectItem value="account">Account</SelectItem>
|
||||
<SelectItem value="deployments">Deployments</SelectItem>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`security-level-${id}`}>Security Level</Label>
|
||||
<Select defaultValue="2">
|
||||
<SelectTrigger
|
||||
id={`security-level-${id}`}
|
||||
className="w-full [&_span]:!block [&_span]:truncate"
|
||||
aria-label="Security Level"
|
||||
>
|
||||
<SelectValue placeholder="Select level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Severity 1 (Highest)</SelectItem>
|
||||
<SelectItem value="2">Severity 2</SelectItem>
|
||||
<SelectItem value="3">Severity 3</SelectItem>
|
||||
<SelectItem value="4">Severity 4 (Lowest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`subject-${id}`}>Subject</Label>
|
||||
<Input id={`subject-${id}`} placeholder="I need help with..." />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor={`description-${id}`}>Description</Label>
|
||||
<Textarea
|
||||
id={`description-${id}`}
|
||||
placeholder="Please include all information relevant to your issue."
|
||||
className="min-h-28"
|
||||
/>
|
||||
</div>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldGroup className="grid gap-4 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`area-${id}`}>Area</FieldLabel>
|
||||
<Select defaultValue="billing">
|
||||
<SelectTrigger
|
||||
id={`area-${id}`}
|
||||
aria-label="Area"
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="team">Team</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
<SelectItem value="account">Account</SelectItem>
|
||||
<SelectItem value="deployments">Deployments</SelectItem>
|
||||
<SelectItem value="support">Support</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`security-level-${id}`}>
|
||||
Security Level
|
||||
</FieldLabel>
|
||||
<Select defaultValue="2">
|
||||
<SelectTrigger
|
||||
id={`security-level-${id}`}
|
||||
className="w-full [&_span]:!block [&_span]:truncate"
|
||||
aria-label="Security Level"
|
||||
>
|
||||
<SelectValue placeholder="Select level" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">Severity 1 (Highest)</SelectItem>
|
||||
<SelectItem value="2">Severity 2</SelectItem>
|
||||
<SelectItem value="3">Severity 3</SelectItem>
|
||||
<SelectItem value="4">Severity 4 (Lowest)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`subject-${id}`}>Subject</FieldLabel>
|
||||
<Input id={`subject-${id}`} placeholder="I need help with..." />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`description-${id}`}>Description</FieldLabel>
|
||||
<Textarea
|
||||
id={`description-${id}`}
|
||||
placeholder="Please include all information relevant to your issue."
|
||||
className="min-h-24"
|
||||
/>
|
||||
</Field>
|
||||
<Field orientation="horizontal" className="justify-end">
|
||||
<Button variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Submit</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end gap-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Submit</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@ import {
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Select,
|
||||
@@ -73,42 +81,35 @@ export function CardsShare() {
|
||||
<Separator className="my-4" />
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-sm font-medium">People with access</div>
|
||||
<div className="grid gap-6">
|
||||
<ItemGroup>
|
||||
{people.map((person) => (
|
||||
<div
|
||||
key={person.email}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar>
|
||||
<AvatarImage src={person.avatar} alt="Image" />
|
||||
<AvatarFallback>{person.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="text-sm leading-none font-medium">
|
||||
{person.name}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{person.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Select defaultValue="edit">
|
||||
<SelectTrigger
|
||||
className="ml-auto pr-2"
|
||||
aria-label="Edit"
|
||||
size="sm"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="edit">Can edit</SelectItem>
|
||||
<SelectItem value="view">Can view</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Item key={person.email} className="px-0 py-2">
|
||||
<Avatar>
|
||||
<AvatarImage src={person.avatar} alt="Image" />
|
||||
<AvatarFallback>{person.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<ItemContent>
|
||||
<ItemTitle>{person.name}</ItemTitle>
|
||||
<ItemDescription>{person.email}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Select defaultValue="edit">
|
||||
<SelectTrigger
|
||||
className="ml-auto pr-2"
|
||||
aria-label="Edit"
|
||||
size="sm"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="edit">Can edit</SelectItem>
|
||||
<SelectItem value="view">Can view</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
))}
|
||||
</div>
|
||||
</ItemGroup>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -23,6 +23,13 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -71,63 +78,58 @@ const roles = [
|
||||
|
||||
export function CardsTeamMembers() {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="gap-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Team Members</CardTitle>
|
||||
<CardDescription>
|
||||
Invite your team members to collaborate.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6">
|
||||
<CardContent>
|
||||
{teamMembers.map((member) => (
|
||||
<div
|
||||
key={member.name}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="border">
|
||||
<AvatarImage src={member.avatar} alt="Image" />
|
||||
<AvatarFallback>{member.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<p className="text-sm leading-none font-medium">
|
||||
{member.name}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">{member.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
>
|
||||
{member.role} <ChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Select role..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No roles found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{roles.map((role) => (
|
||||
<CommandItem key={role.name}>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm font-medium">{role.name}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<Item key={member.name} size="sm" className="gap-4 px-0">
|
||||
<Avatar className="shrink-0 self-start border">
|
||||
<AvatarImage src={member.avatar} alt="Image" />
|
||||
<AvatarFallback>{member.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<ItemContent>
|
||||
<ItemTitle>{member.name}</ItemTitle>
|
||||
<ItemDescription>{member.email}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
>
|
||||
{member.role} <ChevronDown />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Select role..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No roles found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{roles.map((role) => (
|
||||
<CommandItem key={role.name}>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm font-medium">{role.name}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react"
|
||||
import { registryItemSchema } from "shadcn/registry"
|
||||
import { registryItemSchema } from "shadcn/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
import { highlightCode } from "@/lib/highlight-code"
|
||||
|
||||
@@ -22,7 +22,7 @@ export function CodeCollapsibleWrapper({
|
||||
<Collapsible
|
||||
open={isOpened}
|
||||
onOpenChange={setIsOpened}
|
||||
className={cn("group/collapsible relative md:-mx-4", className)}
|
||||
className={cn("group/collapsible relative md:-mx-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IconArrowRight } from "@tabler/icons-react"
|
||||
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
|
||||
|
||||
import { type Color, type ColorPalette } from "@/lib/colors"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { source } from "@/lib/source"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
@@ -30,17 +31,20 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import { Kbd, KbdGroup } from "@/registry/new-york-v4/ui/kbd"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
|
||||
export function CommandMenu({
|
||||
tree,
|
||||
colors,
|
||||
blocks,
|
||||
navItems,
|
||||
...props
|
||||
}: DialogProps & {
|
||||
tree: typeof source.pageTree
|
||||
colors: ColorPalette[]
|
||||
blocks?: { name: string; description: string; categories: string[] }[]
|
||||
navItems?: { href: string; label: string }[]
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const isMac = useIsMac()
|
||||
@@ -141,7 +145,7 @@ export function CommandMenu({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
"bg-surface text-surface-foreground/60 dark:bg-card relative h-8 w-full justify-start pl-2.5 font-normal shadow-none sm:pr-12 md:w-40 lg:w-56 xl:w-64"
|
||||
"bg-surface text-foreground dark:bg-card relative h-8 w-full justify-start pl-3 font-medium shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64"
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
{...props}
|
||||
@@ -149,8 +153,10 @@ export function CommandMenu({
|
||||
<span className="hidden lg:inline-flex">Search documentation...</span>
|
||||
<span className="inline-flex lg:hidden">Search...</span>
|
||||
<div className="absolute top-1.5 right-1.5 hidden gap-1 sm:flex">
|
||||
<CommandMenuKbd>{isMac ? "⌘" : "Ctrl"}</CommandMenuKbd>
|
||||
<CommandMenuKbd className="aspect-square">K</CommandMenuKbd>
|
||||
<KbdGroup>
|
||||
<Kbd className="border">{isMac ? "⌘" : "Ctrl"}</Kbd>
|
||||
<Kbd className="border">K</Kbd>
|
||||
</KbdGroup>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -162,12 +168,45 @@ export function CommandMenu({
|
||||
<DialogTitle>Search documentation...</DialogTitle>
|
||||
<DialogDescription>Search for a command to run...</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Command className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border">
|
||||
<Command
|
||||
className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border"
|
||||
filter={(value, search, keywords) => {
|
||||
const extendValue = value + " " + (keywords?.join(" ") || "")
|
||||
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}}
|
||||
>
|
||||
<CommandInput placeholder="Search documentation..." />
|
||||
<CommandList className="no-scrollbar min-h-80 scroll-pt-2 scroll-pb-1.5">
|
||||
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
|
||||
No results found.
|
||||
</CommandEmpty>
|
||||
{navItems && navItems.length > 0 && (
|
||||
<CommandGroup
|
||||
heading="Pages"
|
||||
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
|
||||
>
|
||||
{navItems.map((item) => (
|
||||
<CommandMenuItem
|
||||
key={item.href}
|
||||
value={`Navigation ${item.label}`}
|
||||
keywords={["nav", "navigation", item.label.toLowerCase()]}
|
||||
onHighlight={() => {
|
||||
setSelectedType("page")
|
||||
setCopyPayload("")
|
||||
}}
|
||||
onSelect={() => {
|
||||
runCommand(() => router.push(item.href))
|
||||
}}
|
||||
>
|
||||
<IconArrowRight />
|
||||
{item.label}
|
||||
</CommandMenuItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
{tree.children.map((group) => (
|
||||
<CommandGroup
|
||||
key={group.$id}
|
||||
@@ -179,6 +218,10 @@ export function CommandMenu({
|
||||
if (item.type === "page") {
|
||||
const isComponent = item.url.includes("/components/")
|
||||
|
||||
if (!showMcpDocs && item.url.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandMenuItem
|
||||
key={item.url}
|
||||
|
||||
@@ -9,12 +9,14 @@ export function ComponentPreviewTabs({
|
||||
className,
|
||||
align = "center",
|
||||
hideCode = false,
|
||||
chromeLessOnMobile = false,
|
||||
component,
|
||||
source,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
align?: "center" | "start" | "end"
|
||||
hideCode?: boolean
|
||||
chromeLessOnMobile?: boolean
|
||||
component: React.ReactNode
|
||||
source: React.ReactNode
|
||||
}) {
|
||||
@@ -51,7 +53,8 @@ export function ComponentPreviewTabs({
|
||||
</Tabs>
|
||||
<div
|
||||
data-tab={tab}
|
||||
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-4"
|
||||
data-chrome-less-on-mobile={chromeLessOnMobile}
|
||||
className="data-[tab=code]:border-code relative rounded-lg border data-[chrome-less-on-mobile=true]:border-0 sm:data-[chrome-less-on-mobile=true]:border md:-mx-1"
|
||||
>
|
||||
<div
|
||||
data-slot="preview"
|
||||
@@ -61,7 +64,8 @@ export function ComponentPreviewTabs({
|
||||
<div
|
||||
data-align={align}
|
||||
className={cn(
|
||||
"preview flex h-[450px] w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start"
|
||||
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
|
||||
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
|
||||
)}
|
||||
>
|
||||
{component}
|
||||
|
||||
@@ -10,6 +10,7 @@ export function ComponentPreview({
|
||||
className,
|
||||
align = "center",
|
||||
hideCode = false,
|
||||
chromeLessOnMobile = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
name: string
|
||||
@@ -17,12 +18,13 @@ export function ComponentPreview({
|
||||
description?: string
|
||||
hideCode?: boolean
|
||||
type?: "block" | "component" | "example"
|
||||
chromeLessOnMobile?: boolean
|
||||
}) {
|
||||
const Component = Index[name]?.component
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-muted-foreground mt-6 text-sm">
|
||||
Component{" "}
|
||||
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
||||
{name}
|
||||
@@ -34,7 +36,7 @@ export function ComponentPreview({
|
||||
|
||||
if (type === "block") {
|
||||
return (
|
||||
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-4">
|
||||
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
|
||||
<Image
|
||||
src={`/r/styles/new-york-v4/${name}-light.png`}
|
||||
alt={name}
|
||||
@@ -63,6 +65,7 @@ export function ComponentPreview({
|
||||
hideCode={hideCode}
|
||||
component={<Component />}
|
||||
source={<ComponentSource name={name} collapsible={false} />}
|
||||
chromeLessOnMobile={chromeLessOnMobile}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -43,6 +43,14 @@ export async function ComponentSource({
|
||||
return null
|
||||
}
|
||||
|
||||
// Fix imports.
|
||||
// Replace @/registry/new-york-v4/ with @/components/.
|
||||
code = code.replaceAll("@/registry/new-york-v4/", "@/components/")
|
||||
|
||||
// Replace export default with export.
|
||||
code = code.replaceAll("export default", "export")
|
||||
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
|
||||
|
||||
const lang = language ?? title?.split(".").pop() ?? "tsx"
|
||||
const highlightedCode = await highlightCode(code, lang)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { source } from "@/lib/source"
|
||||
|
||||
export function ComponentsList() {
|
||||
@@ -21,9 +22,15 @@ export function ComponentsList() {
|
||||
<Link
|
||||
key={component.$id}
|
||||
href={component.url}
|
||||
className="text-lg font-medium underline-offset-4 hover:underline md:text-base"
|
||||
className="inline-flex items-center gap-2 text-lg font-medium underline-offset-4 hover:underline md:text-base"
|
||||
>
|
||||
{component.name}
|
||||
{PAGES_NEW.includes(component.url) && (
|
||||
<span
|
||||
className="flex size-2 rounded-full bg-blue-500"
|
||||
title="New"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,33 +1,156 @@
|
||||
"use client"
|
||||
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react"
|
||||
import { IconCheck, IconChevronDown, IconCopy } from "@tabler/icons-react"
|
||||
|
||||
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
|
||||
export function DocsCopyPage({ page }: { page: string }) {
|
||||
function getPromptUrl(baseURL: string, url: string) {
|
||||
return `${baseURL}?q=${encodeURIComponent(
|
||||
`I’m looking at this shadcn/ui documentation: ${url}.
|
||||
Help me understand how to use it. Be ready to explain concepts, give examples, or help debug based on it.
|
||||
`
|
||||
)}`
|
||||
}
|
||||
|
||||
const menuItems = {
|
||||
markdown: (url: string) => (
|
||||
<a href={`${url}.md`} target="_blank" rel="noopener noreferrer">
|
||||
<svg strokeLinejoin="round" viewBox="0 0 22 16">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M19.5 2.25H2.5C1.80964 2.25 1.25 2.80964 1.25 3.5V12.5C1.25 13.1904 1.80964 13.75 2.5 13.75H19.5C20.1904 13.75 20.75 13.1904 20.75 12.5V3.5C20.75 2.80964 20.1904 2.25 19.5 2.25ZM2.5 1C1.11929 1 0 2.11929 0 3.5V12.5C0 13.8807 1.11929 15 2.5 15H19.5C20.8807 15 22 13.8807 22 12.5V3.5C22 2.11929 20.8807 1 19.5 1H2.5ZM3 4.5H4H4.25H4.6899L4.98715 4.82428L7 7.02011L9.01285 4.82428L9.3101 4.5H9.75H10H11V5.5V11.5H9V7.79807L7.73715 9.17572L7 9.97989L6.26285 9.17572L5 7.79807V11.5H3V5.5V4.5ZM15 8V4.5H17V8H19.5L17 10.5L16 11.5L15 10.5L12.5 8H15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
View as Markdown
|
||||
</a>
|
||||
),
|
||||
v0: (url: string) => (
|
||||
<a
|
||||
href={getPromptUrl("https://v0.dev", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 147 70"
|
||||
className="size-4.5 -translate-x-px"
|
||||
>
|
||||
<path d="M56 50.203V14h14v46.156C70 65.593 65.593 70 60.156 70c-2.596 0-5.158-1-7-2.843L0 14h19.797L56 50.203ZM147 56h-14V23.953L100.953 56H133v14H96.687C85.814 70 77 61.186 77 50.312V14h14v32.156L123.156 14H91V0h36.312C138.186 0 147 8.814 147 19.688V56Z" />
|
||||
</svg>
|
||||
<span className="-translate-x-[2px]">Open in v0</span>
|
||||
</a>
|
||||
),
|
||||
chatgpt: (url: string) => (
|
||||
<a
|
||||
href={getPromptUrl("https://chatgpt.com", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Open in ChatGPT
|
||||
</a>
|
||||
),
|
||||
claude: (url: string) => (
|
||||
<a
|
||||
href={getPromptUrl("https://claude.ai/new", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Open in Claude
|
||||
</a>
|
||||
),
|
||||
}
|
||||
|
||||
export function DocsCopyPage({ page, url }: { page: string; url: string }) {
|
||||
const { copyToClipboard, isCopied } = useCopyToClipboard()
|
||||
|
||||
const trigger = (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="peer -ml-0.5 size-8 shadow-none md:size-7 md:text-[0.8rem]"
|
||||
>
|
||||
<IconChevronDown className="rotate-180 sm:rotate-0" />
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Popover>
|
||||
<div className="bg-secondary group/buttons relative flex rounded-lg *:[[data-slot=button]]:focus-visible:relative *:[[data-slot=button]]:focus-visible:z-10">
|
||||
<PopoverAnchor />
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-8 pl-1.5 md:h-7 [&>svg]:size-3.5"
|
||||
className="h-8 shadow-none md:h-7 md:text-[0.8rem]"
|
||||
onClick={() => copyToClipboard(page)}
|
||||
>
|
||||
{isCopied ? <IconCheck /> : <IconCopy />} Copy Page
|
||||
{isCopied ? <IconCheck /> : <IconCopy />}
|
||||
Copy Page
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Copy as Markdown</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="hidden sm:flex">
|
||||
{trigger}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="shadow-none">
|
||||
{Object.entries(menuItems).map(([key, value]) => (
|
||||
<DropdownMenuItem key={key} asChild>
|
||||
{value(url)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="!bg-foreground/10 absolute top-0 right-8 z-0 !h-8 peer-focus-visible:opacity-0 sm:right-7 sm:!h-7"
|
||||
/>
|
||||
<PopoverTrigger asChild className="flex sm:hidden">
|
||||
{trigger}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-sm backdrop-blur-sm"
|
||||
align="start"
|
||||
>
|
||||
{Object.entries(menuItems).map(([key, value]) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
asChild
|
||||
key={key}
|
||||
className="*:[svg]:text-muted-foreground w-full justify-start text-base font-normal"
|
||||
>
|
||||
{value(url)}
|
||||
</Button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import type { source } from "@/lib/source"
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -15,6 +17,32 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
const TOP_LEVEL_SECTIONS = [
|
||||
{ name: "Get Started", href: "/docs" },
|
||||
{
|
||||
name: "Components",
|
||||
href: "/docs/components",
|
||||
},
|
||||
{
|
||||
name: "Registry",
|
||||
href: "/docs/registry",
|
||||
},
|
||||
{
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/docs/changelog",
|
||||
},
|
||||
]
|
||||
const EXCLUDED_SECTIONS = ["installation", "dark-mode"]
|
||||
const EXCLUDED_PAGES = ["/docs", "/docs/changelog"]
|
||||
|
||||
export function DocsSidebar({
|
||||
tree,
|
||||
...props
|
||||
@@ -23,40 +51,96 @@ export function DocsSidebar({
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--header-height)-var(--footer-height))] bg-transparent lg:flex"
|
||||
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)+2rem)] bg-transparent lg:flex"
|
||||
collapsible="none"
|
||||
{...props}
|
||||
>
|
||||
<SidebarContent className="no-scrollbar px-2 pb-12">
|
||||
<SidebarContent className="no-scrollbar overflow-x-hidden px-2 pb-12">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
{tree.children.map((item) => (
|
||||
<SidebarGroup key={item.$id}>
|
||||
<SidebarGroupLabel className="text-muted-foreground font-medium">
|
||||
{item.name}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{item.type === "folder" && (
|
||||
<SidebarMenu className="gap-0.5">
|
||||
{item.children.map((item) => {
|
||||
return (
|
||||
item.type === "page" && (
|
||||
<SidebarMenuItem key={item.url}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={item.url === pathname}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
|
||||
>
|
||||
<Link href={item.url}>{item.name}</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel className="text-muted-foreground font-medium">
|
||||
Sections
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{TOP_LEVEL_SECTIONS.map(({ name, href }) => {
|
||||
if (!showMcpDocs && href.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<SidebarMenuItem key={name}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={
|
||||
href === "/docs"
|
||||
? pathname === href
|
||||
: pathname.startsWith(href)
|
||||
}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
|
||||
>
|
||||
<Link href={href}>
|
||||
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
|
||||
{name}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
{tree.children.map((item) => {
|
||||
if (EXCLUDED_SECTIONS.includes(item.$id ?? "")) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarGroup key={item.$id}>
|
||||
<SidebarGroupLabel className="text-muted-foreground font-medium">
|
||||
{item.name}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
{item.type === "folder" && (
|
||||
<SidebarMenu className="gap-0.5">
|
||||
{item.children.map((item) => {
|
||||
if (
|
||||
!showMcpDocs &&
|
||||
item.type === "page" &&
|
||||
item.url?.includes("/mcp")
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
item.type === "page" &&
|
||||
!EXCLUDED_PAGES.includes(item.url) && (
|
||||
<SidebarMenuItem key={item.url}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={item.url === pathname}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
|
||||
>
|
||||
<Link href={item.url}>
|
||||
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
|
||||
{item.name}
|
||||
{PAGES_NEW.includes(item.url) && (
|
||||
<span
|
||||
className="flex size-2 rounded-full bg-blue-500"
|
||||
title="New"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
)
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
)}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
))}
|
||||
})}
|
||||
</SidebarMenu>
|
||||
)}
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
})}
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ import * as React from "react"
|
||||
import Link, { LinkProps } from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { source } from "@/lib/source"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
@@ -13,6 +15,30 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
const TOP_LEVEL_SECTIONS = [
|
||||
{ name: "Get Started", href: "/docs" },
|
||||
{
|
||||
name: "Components",
|
||||
href: "/docs/components",
|
||||
},
|
||||
{
|
||||
name: "Registry",
|
||||
href: "/docs/registry",
|
||||
},
|
||||
{
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/docs/changelog",
|
||||
},
|
||||
]
|
||||
|
||||
export function MobileNav({
|
||||
tree,
|
||||
items,
|
||||
@@ -79,6 +105,23 @@ export function MobileNav({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-muted-foreground text-sm font-medium">
|
||||
Sections
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{TOP_LEVEL_SECTIONS.map(({ name, href }) => {
|
||||
if (!showMcpDocs && href.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<MobileLink key={name} href={href} onOpenChange={setOpen}>
|
||||
{name}
|
||||
</MobileLink>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
{tree?.children?.map((group, index) => {
|
||||
if (group.type === "folder") {
|
||||
@@ -90,13 +133,20 @@ export function MobileNav({
|
||||
<div className="flex flex-col gap-3">
|
||||
{group.children.map((item) => {
|
||||
if (item.type === "page") {
|
||||
if (!showMcpDocs && item.url.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<MobileLink
|
||||
key={`${item.url}-${index}`}
|
||||
href={item.url}
|
||||
onOpenChange={setOpen}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{item.name}
|
||||
{item.name}{" "}
|
||||
{PAGES_NEW.includes(item.url) && (
|
||||
<span className="flex size-2 rounded-full bg-blue-500" />
|
||||
)}
|
||||
</MobileLink>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user