mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 18:01:34 +00:00
Compare commits
402 Commits
shadcn@2.7
...
fix/cli-va
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbf7b3708a | ||
|
|
9da12c13f9 | ||
|
|
ccafdaf7c6 | ||
|
|
f0d147d581 | ||
|
|
df67e49aac | ||
|
|
c0de90e1a1 | ||
|
|
0447708efa | ||
|
|
4a470fc617 | ||
|
|
137b1c12b7 | ||
|
|
73296e79c0 | ||
|
|
78e5fa2a39 | ||
|
|
9cf47dd4a3 | ||
|
|
f53400f934 | ||
|
|
b3d6f872db | ||
|
|
2aa5e11f6f | ||
|
|
058ebc5acd | ||
|
|
a60683dea5 | ||
|
|
1dc1b8dbfb | ||
|
|
c6273cca03 | ||
|
|
b15d7e8221 | ||
|
|
46e3c26a6e | ||
|
|
f36e25f703 | ||
|
|
55f5d1c7cc | ||
|
|
db19605996 | ||
|
|
40012adb14 | ||
|
|
ad8104e473 | ||
|
|
5fb0c4d19a | ||
|
|
31c86f9fd5 | ||
|
|
aad175ff87 | ||
|
|
081c91c461 | ||
|
|
7dbf3688fb | ||
|
|
99ad18b389 | ||
|
|
fabb886de9 | ||
|
|
4b561cf050 | ||
|
|
0c2373f592 | ||
|
|
ff42c27d41 | ||
|
|
075b6aef97 | ||
|
|
f4e0f671de | ||
|
|
d3156c09ae | ||
|
|
46bf4a0f06 | ||
|
|
b61b718727 | ||
|
|
ee9b6b36ec | ||
|
|
33de348d41 | ||
|
|
edcc96fc73 | ||
|
|
ef90a97e72 | ||
|
|
86d9b00084 | ||
|
|
672f845322 | ||
|
|
d01074deed | ||
|
|
321ceaf1c4 | ||
|
|
32a972f4ce | ||
|
|
d28e02be1b | ||
|
|
6699158a22 | ||
|
|
142cd8ef13 | ||
|
|
bdedce2750 | ||
|
|
4cb283d68e | ||
|
|
480a6cdb37 | ||
|
|
8ba883738e | ||
|
|
b022c24825 | ||
|
|
3587477865 | ||
|
|
05143a80e6 | ||
|
|
728d2003b7 | ||
|
|
12c9e6b0b5 | ||
|
|
56cd757c45 | ||
|
|
9eb784054f | ||
|
|
824577692b | ||
|
|
6be68df08c | ||
|
|
cc48808a0d | ||
|
|
a56b3720d1 | ||
|
|
334db11234 | ||
|
|
8a5027a0cd | ||
|
|
803206305d | ||
|
|
d0fb73ac0e | ||
|
|
62218c1c0c | ||
|
|
dd1563d57d | ||
|
|
0538384860 | ||
|
|
d43b437abc | ||
|
|
8fbfacd243 | ||
|
|
778cee31ee | ||
|
|
73d8b8a817 | ||
|
|
55ab069aca | ||
|
|
c39925a9be | ||
|
|
51179ccd64 | ||
|
|
dcfa05e392 | ||
|
|
541f55df04 | ||
|
|
69010e0230 | ||
|
|
a8025c866e | ||
|
|
6e34ec7280 | ||
|
|
10ccb244a1 | ||
|
|
16fdb07ccc | ||
|
|
49da1fae79 | ||
|
|
a2244d42f7 | ||
|
|
c2075e2a8b | ||
|
|
dd2d8d7ead | ||
|
|
b6a93b7ec6 | ||
|
|
4899d3f0da | ||
|
|
3d04cb099a | ||
|
|
cde343916c | ||
|
|
c877df07b8 | ||
|
|
65e5c1c3cf | ||
|
|
8a7f05f670 | ||
|
|
db004ce4c0 | ||
|
|
e23698a897 | ||
|
|
5813ef20a3 | ||
|
|
515024b69e | ||
|
|
f7284c5cc3 | ||
|
|
c02d00aafc | ||
|
|
df497ad236 | ||
|
|
1e468e33ac | ||
|
|
ff91c31a71 | ||
|
|
25d6a18f6f | ||
|
|
c0309510b6 | ||
|
|
a3a1574668 | ||
|
|
65d581ea5a | ||
|
|
fdf80a1d49 | ||
|
|
86c494c452 | ||
|
|
eb158686b9 | ||
|
|
134cd46edb | ||
|
|
47b0efb20c | ||
|
|
bd4d09d33e | ||
|
|
14d6265580 | ||
|
|
68805d29a1 | ||
|
|
c100d5841a | ||
|
|
7a71da5218 | ||
|
|
e18902039a | ||
|
|
559af6c245 | ||
|
|
8971be484f | ||
|
|
ad6a3c6367 | ||
|
|
befa56b5be | ||
|
|
5d1770e36d | ||
|
|
653521725a | ||
|
|
7c0618bf43 | ||
|
|
854641cea1 | ||
|
|
3a72007f61 | ||
|
|
6b53b238fb | ||
|
|
b398fea304 | ||
|
|
f22174a77f | ||
|
|
c9a39f1007 | ||
|
|
a8ad21f81f | ||
|
|
504503c638 | ||
|
|
f8df5c95cb | ||
|
|
2bfc1c82ba | ||
|
|
84bd724d97 | ||
|
|
39fdf94550 | ||
|
|
08479cc3db | ||
|
|
02d5ce85ec | ||
|
|
c0329c86b9 | ||
|
|
3b1491f908 | ||
|
|
ca4c1c43ec | ||
|
|
1e840eb53c | ||
|
|
96ac92e63f | ||
|
|
e11546e692 | ||
|
|
0b4d62f95c | ||
|
|
dae80dad65 | ||
|
|
abc09809e8 | ||
|
|
8a40fe0ead | ||
|
|
b3ab304a00 | ||
|
|
bb45fd83c3 | ||
|
|
84678ee1c0 | ||
|
|
33ffb0419c | ||
|
|
a2f6c031e2 | ||
|
|
ac098d8cf0 | ||
|
|
8160610410 | ||
|
|
c7901e3a41 | ||
|
|
d73ac361b3 | ||
|
|
ebad2901ce | ||
|
|
4f617d59b8 | ||
|
|
ed0e103bd6 | ||
|
|
9cab0c9b18 | ||
|
|
d80e084814 | ||
|
|
efcf9728c2 | ||
|
|
8835bacc8b | ||
|
|
f2556d2386 | ||
|
|
75a0000075 | ||
|
|
ac306c60f5 | ||
|
|
5e2ef1f8bd | ||
|
|
7d9b8aefff | ||
|
|
58208e3802 | ||
|
|
a16a77446a | ||
|
|
39032bb390 | ||
|
|
d7e0dc3ec8 | ||
|
|
6bddba986d | ||
|
|
b70059b25b | ||
|
|
37bc2eec1f | ||
|
|
bb048fb532 | ||
|
|
9c373dbd27 | ||
|
|
d75b092c61 | ||
|
|
be49662bf5 | ||
|
|
b2b2e3fc98 | ||
|
|
188b746074 | ||
|
|
6f093a0f3f | ||
|
|
f18f1eaff7 | ||
|
|
9ac1b5c0a5 | ||
|
|
f63b70b413 | ||
|
|
54e725d986 | ||
|
|
62dbad36bb | ||
|
|
a707424fa2 | ||
|
|
e2bfa6bd85 | ||
|
|
6292464d90 | ||
|
|
6617167d6f | ||
|
|
ca28857d40 | ||
|
|
343bc941b1 | ||
|
|
c9311f26fa | ||
|
|
4e0871f426 | ||
|
|
cb769b7059 | ||
|
|
93037dca94 | ||
|
|
ed9d5939e6 | ||
|
|
b52ec12f1e | ||
|
|
2ab9bff4bb | ||
|
|
2f6b51fa0a | ||
|
|
8a4764ed91 | ||
|
|
e934d4645b | ||
|
|
08b8e499d8 | ||
|
|
69402b3579 | ||
|
|
679c852254 | ||
|
|
d478412e44 | ||
|
|
d5c8a25150 | ||
|
|
26433a651c | ||
|
|
c3da716e94 | ||
|
|
b2572d0287 | ||
|
|
b83f042416 | ||
|
|
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 |
@@ -7,5 +7,5 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["www", "v4"]
|
||||
"ignore": ["v4", "tests"]
|
||||
}
|
||||
|
||||
5
.changeset/rude-clowns-retire.md
Normal file
5
.changeset/rude-clowns-retire.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
validate app name on create
|
||||
13
.claude/settings.local.json
Normal file
13
.claude/settings.local.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm test:*)",
|
||||
"Bash(npm run typecheck:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(cat:*)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:github.com)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
Normal file
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Add registry to directory
|
||||
description: Add your registry to the directory
|
||||
title: "[Registry Directory]: "
|
||||
labels: ["registry", "directory"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: Name
|
||||
description: The name of your registry. This is also the namespace.
|
||||
placeholder: e.g., "@acme"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: URL
|
||||
description: The URL to your registry index. Use {name} placeholder.
|
||||
placeholder: https://ui.acme.com/r/{name}.json
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: homepage
|
||||
attributes:
|
||||
label: Homepage
|
||||
description: The URL to your registry homepage. This is where users can browse your registry.
|
||||
placeholder: https://ui.acme.com
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Briefly describe what is your registry and what type of components or code it distributes.
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logo
|
||||
attributes:
|
||||
label: Logo
|
||||
description: Add your SVG logo here.
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: requirements
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Verify that your registry meets the following requirements.
|
||||
options:
|
||||
- label: The registry must be open source and publicly accessible.
|
||||
- label: The registry must be a valid JSON file that conforms to the [registry schema](https://ui.shadcn.com/docs/registry/registry-json) specification.
|
||||
- label: The `files` array, if present on your registry items, must NOT include a `content` property.
|
||||
- label: I've attached a square SVG logo to this issue
|
||||
validations:
|
||||
required: true
|
||||
78
.github/workflows/deprecated.yml
vendored
Normal file
78
.github/workflows/deprecated.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
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:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
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/') &&
|
||||
file !== 'apps/www/package.json'
|
||||
);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
22
.github/workflows/prerelease.yml
vendored
22
.github/workflows/prerelease.yml
vendored
@@ -7,6 +7,11 @@ on:
|
||||
types: [labeled]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prerelease:
|
||||
if: |
|
||||
@@ -18,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -27,23 +32,22 @@ jobs:
|
||||
with:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-beta.js
|
||||
|
||||
- name: Authenticate to NPM
|
||||
run: echo "//registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN" >> packages/shadcn/.npmrc
|
||||
env:
|
||||
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
|
||||
- name: Publish Beta to NPM
|
||||
run: pnpm pub:beta
|
||||
|
||||
|
||||
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@@ -7,6 +7,11 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'shadcn-ui' }}
|
||||
@@ -23,13 +28,16 @@ jobs:
|
||||
with:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
version: 9.0.6
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -41,7 +49,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"
|
||||
@@ -49,5 +57,4 @@ jobs:
|
||||
publish: npx changeset publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
NODE_ENV: "production"
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.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:
|
||||
@@ -16,7 +19,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -39,3 +39,5 @@ tsconfig.tsbuildinfo
|
||||
.idea
|
||||
.fleet
|
||||
.vscode
|
||||
|
||||
.notes
|
||||
|
||||
@@ -3,5 +3,5 @@ node_modules
|
||||
.next
|
||||
build
|
||||
.contentlayer
|
||||
apps/www/pages/api/registry.json
|
||||
**/fixtures
|
||||
deprecated
|
||||
|
||||
17
.vscode/settings.json
vendored
17
.vscode/settings.json
vendored
@@ -3,15 +3,18 @@
|
||||
{ "pattern": "apps/*/" },
|
||||
{ "pattern": "packages/*/" }
|
||||
],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(((?:[^()]|\\([^()]*\\))*)\\)", "[\"'`]?([^\"'`]+)[\"'`]?"],
|
||||
["cn\\(((?:[^()]|\\([^()]*\\))*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
// "cva\\(([^)]*)\\)",
|
||||
// "[\"'`]([^\"'`]*).*?[\"'`]"
|
||||
],
|
||||
"tailwindCSS.classFunctions": ["cva", "cn"],
|
||||
"vitest.debugExclude": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/fixtures/**"
|
||||
]
|
||||
],
|
||||
"files.exclude": {
|
||||
"deprecated": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"apps/v4/registry/radix-*": true,
|
||||
"apps/v4/public/r/*": true,
|
||||
"packages/shadcn/test/fixtures/*": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,28 +20,25 @@ This repository is structured as follows:
|
||||
|
||||
```
|
||||
apps
|
||||
└── www
|
||||
└── v4
|
||||
├── app
|
||||
├── components
|
||||
├── content
|
||||
└── registry
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
└── new-york-v4
|
||||
├── example
|
||||
└── ui
|
||||
packages
|
||||
└── cli
|
||||
└── shadcn
|
||||
```
|
||||
|
||||
| Path | Description |
|
||||
| --------------------- | ---------------------------------------- |
|
||||
| `apps/www/app` | The Next.js application for the website. |
|
||||
| `apps/www/components` | The React components for the website. |
|
||||
| `apps/www/content` | The content for the website. |
|
||||
| `apps/www/registry` | The registry for the components. |
|
||||
| `packages/cli` | The `shadcn-ui` package. |
|
||||
| Path | Description |
|
||||
| -------------------- | ---------------------------------------- |
|
||||
| `apps/v4/app` | The Next.js application for the website. |
|
||||
| `apps/v4/components` | The React components for the website. |
|
||||
| `apps/v4/content` | The content for the website. |
|
||||
| `apps/v4/registry` | The registry for the components. |
|
||||
| `packages/shadcn` | The `shadcn` package. |
|
||||
|
||||
## Development
|
||||
|
||||
@@ -82,32 +79,26 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
|
||||
1. To run the `ui.shadcn.com` website:
|
||||
|
||||
```bash
|
||||
pnpm --filter=www dev
|
||||
pnpm --filter=v4 dev
|
||||
```
|
||||
|
||||
2. To run the `shadcn-ui` package:
|
||||
2. To run the `shadcn` package:
|
||||
|
||||
```bash
|
||||
pnpm --filter=shadcn-ui dev
|
||||
pnpm --filter=shadcn dev
|
||||
```
|
||||
|
||||
## Running the CLI Locally
|
||||
|
||||
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:
|
||||
1. Start by running the dev server:
|
||||
|
||||
```bash
|
||||
pnpm v4:dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
2. Run the development script for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm shadcn:dev
|
||||
```
|
||||
|
||||
3. In another terminal tab, test the CLI by running:
|
||||
2. In another terminal tab, test the CLI by running:
|
||||
|
||||
```bash
|
||||
pnpm shadcn
|
||||
@@ -119,36 +110,27 @@ To run the CLI locally, you can follow the workflow:
|
||||
pnpm shadcn <init | add | ...> -c ~/Desktop/my-app
|
||||
```
|
||||
|
||||
4. To run the tests for the CLI:
|
||||
|
||||
```bash
|
||||
pnpm --filter=shadcn test
|
||||
```
|
||||
|
||||
This workflow ensures that you are running the most recent version of the registry and testing the CLI properly in your local environment.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation for this project is located in the `www` workspace. You can run the documentation locally by running the following command:
|
||||
The documentation for this project is located in the `v4` workspace. You can run the documentation locally by running the following command:
|
||||
|
||||
```bash
|
||||
pnpm --filter=www dev
|
||||
pnpm --filter=v4 dev
|
||||
```
|
||||
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/www/content/docs` directory.
|
||||
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/v4/content/docs` directory.
|
||||
|
||||
## Components
|
||||
|
||||
We use a registry system for developing components. You can find the source code for the components under `apps/www/registry`. The components are organized by styles.
|
||||
We use a registry system for developing components. You can find the source code for the components under `apps/v4/registry`. The components are organized by styles.
|
||||
|
||||
```bash
|
||||
apps
|
||||
└── www
|
||||
└── v4
|
||||
└── registry
|
||||
├── default
|
||||
│ ├── example
|
||||
│ └── ui
|
||||
└── new-york
|
||||
└── new-york-v4
|
||||
├── example
|
||||
└── ui
|
||||
```
|
||||
@@ -157,7 +139,7 @@ When adding or modifying components, please ensure that:
|
||||
|
||||
1. You make the changes for every style.
|
||||
2. You update the documentation.
|
||||
3. You run `pnpm build:registry` to update the registry.
|
||||
3. You run `pnpm registry:build` to update the registry.
|
||||
|
||||
## Commit Convention
|
||||
|
||||
@@ -196,9 +178,9 @@ If you have a request for a new component, please open a discussion on GitHub. W
|
||||
|
||||
## CLI
|
||||
|
||||
The `shadcn-ui` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
|
||||
The `shadcn` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
|
||||
|
||||
Any changes to the CLI should be made in the `packages/cli` directory. If you can, it would be great if you could add tests for your changes.
|
||||
Any changes to the CLI should be made in the `packages/shadcn` directory. If you can, it would be great if you could add tests for your changes.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# shadcn/ui
|
||||
|
||||
Accessible and customizable components that you can copy and paste into your apps. Free. Open Source. **Use this to build your own component library**.
|
||||
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. **Use this to build your own component library**.
|
||||
|
||||

|
||||

|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@ We will investigate all legitimate reports and do our best to quickly fix the pr
|
||||
|
||||
Our preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software.
|
||||
|
||||
To do this, please visit the security tab of the repository and click the "Report a vulnerability" button.
|
||||
To do this, please visit the security tab of the repository and click the [Report a vulnerability](https://github.com/shadcn-ui/ui/security/advisories/new) button.
|
||||
|
||||
138
apps/v4/app/(app)/(root)/components/appearance-settings.tsx
Normal file
138
apps/v4/app/(app)/(root)/components/appearance-settings.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
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 {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const [gpuCount, setGpuCount] = React.useState(8)
|
||||
|
||||
const handleGpuAdjustment = React.useCallback((adjustment: number) => {
|
||||
setGpuCount((prevCount) =>
|
||||
Math.max(1, Math.min(99, prevCount + adjustment))
|
||||
)
|
||||
}, [])
|
||||
|
||||
const handleGpuInputChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
if (!isNaN(value) && value >= 1 && value <= 99) {
|
||||
setGpuCount(value)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend>Compute Environment</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select the compute environment for your cluster.
|
||||
</FieldDescription>
|
||||
<RadioGroup defaultValue="kubernetes">
|
||||
<FieldLabel htmlFor="kubernetes-r2h">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Kubernetes</FieldTitle>
|
||||
<FieldDescription>
|
||||
Run GPU workloads on a K8s configured cluster. This is the
|
||||
default.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="kubernetes"
|
||||
id="kubernetes-r2h"
|
||||
aria-label="Kubernetes"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="vm-z4k">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Virtual Machine</FieldTitle>
|
||||
<FieldDescription>
|
||||
Access a VM configured cluster to run workloads. (Coming
|
||||
soon)
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="vm"
|
||||
id="vm-z4k"
|
||||
aria-label="Virtual Machine"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="number-of-gpus-f6l">Number of GPUs</FieldLabel>
|
||||
<FieldDescription>You can add more later.</FieldDescription>
|
||||
</FieldContent>
|
||||
<ButtonGroup>
|
||||
<Input
|
||||
id="number-of-gpus-f6l"
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-8 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Decrement"
|
||||
onClick={() => handleGpuAdjustment(-1)}
|
||||
disabled={gpuCount <= 1}
|
||||
>
|
||||
<IconMinus />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
aria-label="Increment"
|
||||
onClick={() => handleGpuAdjustment(1)}
|
||||
disabled={gpuCount >= 99}
|
||||
>
|
||||
<IconPlus />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="tinting">Wallpaper Tinting</FieldLabel>
|
||||
<FieldDescription>
|
||||
Allow the wallpaper to be tinted.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch id="tinting" defaultChecked />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
)
|
||||
}
|
||||
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,
|
||||
ListFilterIcon,
|
||||
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>
|
||||
<ListFilterIcon />
|
||||
Add to List
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<TagIcon />
|
||||
Label As...
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={label}
|
||||
onValueChange={setLabel}
|
||||
>
|
||||
<DropdownMenuRadioItem value="personal">
|
||||
Personal
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="work">
|
||||
Work
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="other">
|
||||
Other
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<Trash2Icon />
|
||||
Trash
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
@@ -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 className="my-4">Appearance Settings</FieldSeparator>
|
||||
<AppearanceSettings />
|
||||
</div>
|
||||
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">
|
||||
<NotionPromptForm />
|
||||
<ButtonGroupDemo />
|
||||
<FieldCheckbox />
|
||||
<div className="flex justify-between gap-4">
|
||||
<ButtonGroupNested />
|
||||
<ButtonGroupPopover />
|
||||
</div>
|
||||
<FieldHear />
|
||||
<SpinnerEmpty />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Metadata } from "next"
|
||||
import { type Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { PlusSignIcon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import { CardsDemo } from "@/components/cards"
|
||||
import { ExamplesNav } from "@/components/examples-nav"
|
||||
import {
|
||||
PageActions,
|
||||
@@ -15,9 +16,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 +54,17 @@ 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 asChild size="sm" className="h-[31px] rounded-lg">
|
||||
<Link href="/create">
|
||||
<HugeiconsIcon icon={PlusSignIcon} />
|
||||
New Project
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost">
|
||||
<Link href="/blocks">Browse Blocks</Link>
|
||||
<Button asChild size="sm" variant="ghost" className="rounded-lg">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
@@ -87,7 +93,7 @@ export default function IndexPage() {
|
||||
/>
|
||||
</section>
|
||||
<section className="theme-container hidden md:block">
|
||||
<CardsDemo />
|
||||
<RootComponents />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getAllBlockIds } from "@/lib/blocks"
|
||||
import { registryCategories } from "@/lib/categories"
|
||||
import { BlockDisplay } from "@/components/block-display"
|
||||
import { registryCategories } from "@/registry/registry-categories"
|
||||
import { getActiveStyle } from "@/registry/_legacy-styles"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
@@ -17,13 +18,16 @@ export default async function BlocksPage({
|
||||
}: {
|
||||
params: Promise<{ categories?: string[] }>
|
||||
}) {
|
||||
const { categories = [] } = await params
|
||||
const [{ categories = [] }, activeStyle] = await Promise.all([
|
||||
params,
|
||||
getActiveStyle(),
|
||||
])
|
||||
const blocks = await getAllBlockIds(["registry:block"], categories)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 md:gap-24">
|
||||
{blocks.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Metadata } from "next"
|
||||
import { type Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { BlockDisplay } from "@/components/block-display"
|
||||
import { getActiveStyle } from "@/registry/_legacy-styles"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export const dynamic = "force-static"
|
||||
@@ -15,10 +16,12 @@ const FEATURED_BLOCKS = [
|
||||
]
|
||||
|
||||
export default async function BlocksPage() {
|
||||
const activeStyle = await getActiveStyle()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12 md:gap-24">
|
||||
{FEATURED_BLOCKS.map((name) => (
|
||||
<BlockDisplay name={name} key={name} />
|
||||
<BlockDisplay name={name} key={name} styleName={activeStyle.name} />
|
||||
))}
|
||||
<div className="container-wrapper">
|
||||
<div className="container flex justify-center py-6">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChartDisplay } from "@/components/chart-display"
|
||||
import { getActiveStyle } from "@/registry/_legacy-styles"
|
||||
import { charts } from "@/app/(app)/charts/charts"
|
||||
|
||||
export const revalidate = false
|
||||
@@ -41,6 +42,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
|
||||
|
||||
const chartType = type as ChartType
|
||||
const chartList = charts[chartType]
|
||||
const activeStyle = await getActiveStyle()
|
||||
|
||||
return (
|
||||
<div className="grid flex-1 gap-12 lg:gap-24">
|
||||
@@ -54,6 +56,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
|
||||
<ChartDisplay
|
||||
key={chart.id}
|
||||
name={chart.id}
|
||||
styleName={activeStyle.name}
|
||||
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
|
||||
>
|
||||
<chart.component />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from "react"
|
||||
import type * as React from "react"
|
||||
|
||||
import { ChartAreaAxes } from "@/registry/new-york-v4/charts/chart-area-axes"
|
||||
import { ChartAreaDefault } from "@/registry/new-york-v4/charts/chart-area-default"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Metadata } from "next"
|
||||
import { type Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Metadata } from "next"
|
||||
import { type Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
|
||||
@@ -6,10 +6,13 @@ import {
|
||||
IconArrowRight,
|
||||
IconArrowUpRight,
|
||||
} from "@tabler/icons-react"
|
||||
import { findNeighbour } from "fumadocs-core/server"
|
||||
import fm from "front-matter"
|
||||
import { findNeighbour } from "fumadocs-core/page-tree"
|
||||
import z from "zod"
|
||||
|
||||
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"
|
||||
@@ -24,7 +27,7 @@ export function generateStaticParams() {
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ slug?: string[] }>
|
||||
params: Promise<{ slug: string[] }>
|
||||
}) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug)
|
||||
@@ -72,7 +75,7 @@ export async function generateMetadata(props: {
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ slug?: string[] }>
|
||||
params: Promise<{ slug: string[] }>
|
||||
}) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug)
|
||||
@@ -81,18 +84,24 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const doc = page.data
|
||||
// @ts-expect-error - revisit fumadocs types.
|
||||
const MDX = doc.body
|
||||
const neighbours = await findNeighbour(source.pageTree, page.url)
|
||||
const neighbours = findNeighbour(source.pageTree, page.url)
|
||||
|
||||
// @ts-expect-error - revisit fumadocs types.
|
||||
const links = doc.links
|
||||
const raw = await page.data.getText("raw")
|
||||
const { attributes } = fm(raw)
|
||||
const { links } = z
|
||||
.object({
|
||||
links: z
|
||||
.object({
|
||||
doc: z.string().optional(),
|
||||
api: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.parse(attributes)
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="docs"
|
||||
className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full"
|
||||
>
|
||||
<div className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full">
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
<div className="mx-auto flex w-full max-w-2xl min-w-0 flex-1 flex-col gap-8 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
|
||||
@@ -102,12 +111,13 @@ 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 page={raw} 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 +148,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 +170,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,12 +197,10 @@ 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 ? (
|
||||
<div className="no-scrollbar overflow-y-auto px-8">
|
||||
{/* @ts-expect-error - revisit fumadocs types. */}
|
||||
<DocsTableOfContents toc={doc.toc} />
|
||||
<div className="h-12" />
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Metadata } from "next"
|
||||
import { type Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
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>
|
||||
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
import {
|
||||
Select,
|
||||
@@ -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)
|
||||
|
||||
@@ -35,8 +35,6 @@ import {
|
||||
IconTrendingUp,
|
||||
} from "@tabler/icons-react"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
@@ -44,10 +42,12 @@ import {
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
Row,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
VisibilityState,
|
||||
type ColumnDef,
|
||||
type ColumnFiltersState,
|
||||
type Row,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table"
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||
import { toast } from "sonner"
|
||||
@@ -57,10 +57,10 @@ import { useIsMobile } from "@/registry/new-york-v4/hooks/use-mobile"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/new-york-v4/ui/chart"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Metadata } from "next"
|
||||
import { type Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
@@ -16,8 +16,9 @@ import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
export const dynamic = "force-static"
|
||||
export const revalidate = false
|
||||
|
||||
const title = "Examples"
|
||||
const description = "Check out some examples app built using the components."
|
||||
const title = "The Foundation for your Design System"
|
||||
const description =
|
||||
"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 metadata: Metadata = {
|
||||
title,
|
||||
@@ -52,24 +53,20 @@ export default function ExamplesLayout({
|
||||
<>
|
||||
<PageHeader>
|
||||
<Announcement />
|
||||
<PageHeaderHeading>Build your Component Library</PageHeaderHeading>
|
||||
<PageHeaderDescription>
|
||||
A set of beautifully-designed, accessible components and a code
|
||||
distribution platform. Works with your favorite frameworks. Open
|
||||
Source. Open Code.
|
||||
</PageHeaderDescription>
|
||||
<PageHeaderHeading className="max-w-4xl">{title}</PageHeaderHeading>
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm">
|
||||
<Link href="/docs">Get Started</Link>
|
||||
<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>
|
||||
<PageNav id="examples">
|
||||
<PageNav id="examples" className="hidden md:flex">
|
||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ThemeSelector className="mr-4 hidden md:block" />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
|
||||
<div className="theme-container container flex flex-1 scroll-mt-20 flex-col">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { SliderProps } from "@radix-ui/react-slider"
|
||||
import { type SliderProps } from "@radix-ui/react-slider"
|
||||
|
||||
import {
|
||||
HoverCard,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { PopoverProps } from "@radix-ui/react-popover"
|
||||
import { type PopoverProps } from "@radix-ui/react-popover"
|
||||
import { Check, ChevronsUpDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
import { Model, ModelType } from "../data/models"
|
||||
import { type Model, type ModelType } from "../data/models"
|
||||
|
||||
interface ModelSelectorProps extends PopoverProps {
|
||||
types: readonly ModelType[]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { PopoverProps } from "@radix-ui/react-popover"
|
||||
import { type PopoverProps } from "@radix-ui/react-popover"
|
||||
import { Check, ChevronsUpDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
import { Preset } from "../data/presets"
|
||||
import { type Preset } from "../data/presets"
|
||||
|
||||
interface PresetSelectorProps extends PopoverProps {
|
||||
presets: Preset[]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { SliderProps } from "@radix-ui/react-slider"
|
||||
import { type SliderProps } from "@radix-ui/react-slider"
|
||||
|
||||
import {
|
||||
HoverCard,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { SliderProps } from "@radix-ui/react-slider"
|
||||
import { type SliderProps } from "@radix-ui/react-slider"
|
||||
|
||||
import {
|
||||
HoverCard,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Metadata } from "next"
|
||||
import { type Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import { RotateCcw } from "lucide-react"
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { type ColumnDef } from "@tanstack/react-table"
|
||||
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
|
||||
import { labels, priorities, statuses } from "../data/data"
|
||||
import { Task } from "../data/schema"
|
||||
import { type Task } from "../data/schema"
|
||||
import { DataTableColumnHeader } from "./data-table-column-header"
|
||||
import { DataTableRowActions } from "./data-table-row-actions"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Column } from "@tanstack/react-table"
|
||||
import { type Column } from "@tanstack/react-table"
|
||||
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react"
|
||||
import { Column } from "@tanstack/react-table"
|
||||
import { type Column } from "@tanstack/react-table"
|
||||
import { Check, PlusCircle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Table } from "@tanstack/react-table"
|
||||
import { type Table } from "@tanstack/react-table"
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { Row } from "@tanstack/react-table"
|
||||
import { type Row } from "@tanstack/react-table"
|
||||
import { MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { Table } from "@tanstack/react-table"
|
||||
import { type Table } from "@tanstack/react-table"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
|
||||
import { Table } from "@tanstack/react-table"
|
||||
import { type Table } from "@tanstack/react-table"
|
||||
import { Settings2 } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
@@ -11,9 +9,11 @@ import {
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
VisibilityState,
|
||||
type ColumnDef,
|
||||
type ColumnFiltersState,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import {
|
||||
@@ -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) => (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { Metadata } from "next"
|
||||
import { type Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import { z } from "zod"
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import { SiteHeader } from "@/components/site-header"
|
||||
|
||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-background relative z-10 flex min-h-svh flex-col">
|
||||
<div
|
||||
data-slot="layout"
|
||||
className="bg-background relative z-10 flex min-h-svh flex-col"
|
||||
>
|
||||
<SiteHeader />
|
||||
<main className="flex flex-1 flex-col">{children}</main>
|
||||
<SiteFooter />
|
||||
|
||||
36
apps/v4/app/(app)/llm/[[...slug]]/route.ts
Normal file
36
apps/v4/app/(app)/llm/[[...slug]]/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { NextResponse, type NextRequest } from "next/server"
|
||||
|
||||
import { processMdxForLLMs } from "@/lib/llm"
|
||||
import { source } from "@/lib/source"
|
||||
import { getActiveStyle } from "@/registry/_legacy-styles"
|
||||
|
||||
export const revalidate = false
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ slug?: string[] }> }
|
||||
) {
|
||||
const [{ slug }, activeStyle] = await Promise.all([params, getActiveStyle()])
|
||||
|
||||
const page = source.getPage(slug)
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const processedContent = processMdxForLLMs(
|
||||
await page.data.getText("raw"),
|
||||
activeStyle.name
|
||||
)
|
||||
|
||||
return new NextResponse(processedContent, {
|
||||
headers: {
|
||||
"Content-Type": "text/markdown; charset=utf-8",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.generateParams()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Metadata } from "next"
|
||||
import { type Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
|
||||
95
apps/v4/app/(create)/components/accent-picker.tsx
Normal file
95
apps/v4/app/(create)/components/accent-picker.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
"use client"
|
||||
|
||||
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function MenuAccentPicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentAccent = MENU_ACCENTS.find(
|
||||
(accent) => accent.value === params.menuAccent
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Menu Accent</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentAccent?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
d="M19 12.1294L12.9388 18.207C11.1557 19.9949 10.2641 20.8889 9.16993 20.9877C8.98904 21.0041 8.80705 21.0041 8.62616 20.9877C7.53195 20.8889 6.64039 19.9949 4.85726 18.207L2.83687 16.1811C1.72104 15.0622 1.72104 13.2482 2.83687 12.1294M19 12.1294L10.9184 4.02587M19 12.1294H2.83687M10.9184 4.02587L2.83687 12.1294M10.9184 4.02587L8.89805 2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-accent={currentAccent?.value}
|
||||
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
|
||||
></path>
|
||||
<path
|
||||
d="M22 20C22 21.1046 21.1046 22 20 22C18.8954 22 18 21.1046 18 20C18 18.8954 20 17 20 17C20 17 22 18.8954 22 20Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-accent={currentAccent?.value}
|
||||
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentAccent?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuAccent: value as MenuAccentValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_ACCENTS.map((accent) => (
|
||||
<PickerRadioItem key={accent.value} value={accent.value}>
|
||||
{accent.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuAccent"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
125
apps/v4/app/(create)/components/base-color-picker.tsx
Normal file
125
apps/v4/app/(create)/components/base-color-picker.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerItem,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function BaseColorPicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentBaseColor = React.useMemo(
|
||||
() => BASE_COLORS.find((baseColor) => baseColor.name === params.baseColor),
|
||||
[params.baseColor]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Base Color</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentBaseColor?.title}
|
||||
</div>
|
||||
</div>
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
currentBaseColor?.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBaseColor?.name}
|
||||
onValueChange={(value) => {
|
||||
if (value === "dark") {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
return
|
||||
}
|
||||
|
||||
setParams({ baseColor: value as BaseColorName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASE_COLORS.map((baseColor) => (
|
||||
<PickerRadioItem key={baseColor.name} value={baseColor.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
baseColor.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{baseColor.title}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerItem
|
||||
onClick={() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>
|
||||
Switch to {resolvedTheme === "dark" ? "Light" : "Dark"} Mode
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Base colors are easier to see in dark mode.
|
||||
</div>
|
||||
</div>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="baseColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
88
apps/v4/app/(create)/components/base-picker.tsx
Normal file
88
apps/v4/app/(create)/components/base-picker.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { BASES } from "@/registry/config"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function BasePicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentBase = React.useMemo(
|
||||
() => BASES.find((base) => base.name === params.base),
|
||||
[params.base]
|
||||
)
|
||||
|
||||
const handleValueChange = React.useCallback(
|
||||
(value: string) => {
|
||||
const newBase = BASES.find((base) => base.name === value)
|
||||
if (!newBase) {
|
||||
return
|
||||
}
|
||||
|
||||
setParams({ base: newBase.name })
|
||||
},
|
||||
[setParams]
|
||||
)
|
||||
|
||||
return (
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Component Library</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentBase?.title}
|
||||
</div>
|
||||
</div>
|
||||
{currentBase?.meta?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 select-none *:[svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentBase.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBase?.name}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASES.map((base) => (
|
||||
<PickerRadioItem key={base.name} value={base.name}>
|
||||
{base.meta?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! size-4 shrink-0 [&_svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: base.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{base.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
197
apps/v4/app/(create)/components/customizer-controls.tsx
Normal file
197
apps/v4/app/(create)/components/customizer-controls.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Script from "next/script"
|
||||
import { DiceFaces05Icon, Undo02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
BASE_COLORS,
|
||||
DEFAULT_CONFIG,
|
||||
getThemesForBaseColor,
|
||||
iconLibraries,
|
||||
MENU_ACCENTS,
|
||||
MENU_COLORS,
|
||||
RADII,
|
||||
STYLES,
|
||||
} from "@/registry/config"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
import { useLocks } from "@/app/(create)/hooks/use-locks"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import {
|
||||
applyBias,
|
||||
RANDOMIZE_BIASES,
|
||||
type RandomizeContext,
|
||||
} from "@/app/(create)/lib/randomize-biases"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
|
||||
|
||||
function randomItem<T>(array: readonly T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
export function CustomizerControls({ className }: { className?: string }) {
|
||||
const { locks } = useLocks()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const handleReset = React.useCallback(() => {
|
||||
setParams({
|
||||
base: params.base, // Keep the current base value
|
||||
style: DEFAULT_CONFIG.style,
|
||||
baseColor: DEFAULT_CONFIG.baseColor,
|
||||
theme: DEFAULT_CONFIG.theme,
|
||||
iconLibrary: DEFAULT_CONFIG.iconLibrary,
|
||||
font: DEFAULT_CONFIG.font,
|
||||
menuAccent: DEFAULT_CONFIG.menuAccent,
|
||||
menuColor: DEFAULT_CONFIG.menuColor,
|
||||
radius: DEFAULT_CONFIG.radius,
|
||||
template: DEFAULT_CONFIG.template,
|
||||
item: "preview",
|
||||
})
|
||||
}, [setParams, params.base])
|
||||
|
||||
const handleRandomize = React.useCallback(() => {
|
||||
// Use current value if locked, otherwise randomize.
|
||||
const baseColor = locks.has("baseColor")
|
||||
? params.baseColor
|
||||
: randomItem(BASE_COLORS).name
|
||||
const selectedStyle = locks.has("style")
|
||||
? params.style
|
||||
: randomItem(STYLES).name
|
||||
|
||||
// Build context for bias application.
|
||||
const context: RandomizeContext = {
|
||||
style: selectedStyle,
|
||||
baseColor,
|
||||
}
|
||||
|
||||
const availableThemes = getThemesForBaseColor(baseColor)
|
||||
const availableFonts = applyBias(FONTS, context, RANDOMIZE_BIASES.fonts)
|
||||
const availableRadii = applyBias(RADII, context, RANDOMIZE_BIASES.radius)
|
||||
|
||||
const selectedTheme = locks.has("theme")
|
||||
? params.theme
|
||||
: randomItem(availableThemes).name
|
||||
const selectedFont = locks.has("font")
|
||||
? params.font
|
||||
: randomItem(availableFonts).value
|
||||
const selectedRadius = locks.has("radius")
|
||||
? params.radius
|
||||
: randomItem(availableRadii).name
|
||||
const selectedIconLibrary = locks.has("iconLibrary")
|
||||
? params.iconLibrary
|
||||
: randomItem(Object.values(iconLibraries)).name
|
||||
const selectedMenuAccent = locks.has("menuAccent")
|
||||
? params.menuAccent
|
||||
: randomItem(MENU_ACCENTS).value
|
||||
const selectedMenuColor = locks.has("menuColor")
|
||||
? params.menuColor
|
||||
: randomItem(MENU_COLORS).value
|
||||
|
||||
// Update context with selected values for potential future biases.
|
||||
context.theme = selectedTheme
|
||||
context.font = selectedFont
|
||||
context.radius = selectedRadius
|
||||
|
||||
setParams({
|
||||
style: selectedStyle,
|
||||
baseColor,
|
||||
theme: selectedTheme,
|
||||
iconLibrary: selectedIconLibrary,
|
||||
font: selectedFont,
|
||||
menuAccent: selectedMenuAccent,
|
||||
menuColor: selectedMenuColor,
|
||||
radius: selectedRadius,
|
||||
})
|
||||
}, [setParams, locks, params])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if ((e.key === "r" || e.key === "R") && !e.metaKey && !e.ctrlKey) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
handleRandomize()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [handleRandomize])
|
||||
|
||||
return (
|
||||
<div className={cn("items-center gap-0", className)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRandomize}
|
||||
className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
|
||||
>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Shuffle</div>
|
||||
<div className="text-foreground text-sm font-medium">Try Random</div>
|
||||
</div>
|
||||
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
|
||||
<Kbd className="bg-foreground/10 text-foreground hidden md:flex">R</Kbd>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
className="border-foreground/10 bg-muted/50 hidden h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:flex md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
|
||||
>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Reset</div>
|
||||
<div className="text-foreground text-sm font-medium">Start Over</div>
|
||||
</div>
|
||||
<HugeiconsIcon icon={Undo02Icon} className="-translate-x-0.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function RandomizeScript() {
|
||||
return (
|
||||
<Script
|
||||
id="randomize-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
// Forward R key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'r' || e.key === 'R') && !e.metaKey && !e.ctrlKey) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: '${RANDOMIZE_FORWARD_TYPE}',
|
||||
key: e.key
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
83
apps/v4/app/(create)/components/customizer.tsx
Normal file
83
apps/v4/app/(create)/components/customizer.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Settings05Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config"
|
||||
import { FieldGroup } from "@/registry/new-york-v4/ui/field"
|
||||
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
|
||||
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
|
||||
import { BasePicker } from "@/app/(create)/components/base-picker"
|
||||
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
|
||||
import { FontPicker } from "@/app/(create)/components/font-picker"
|
||||
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
|
||||
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
|
||||
import { PresetPicker } from "@/app/(create)/components/preset-picker"
|
||||
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
|
||||
import { StylePicker } from "@/app/(create)/components/style-picker"
|
||||
import { ThemePicker } from "@/app/(create)/components/theme-picker"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function Customizer() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isMobile = useIsMobile()
|
||||
const anchorRef = React.useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const availableThemes = React.useMemo(
|
||||
() => getThemesForBaseColor(params.baseColor),
|
||||
[params.baseColor]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="no-scrollbar -mx-2.5 flex flex-col overflow-y-auto p-1 md:mx-0 md:h-[calc(100svh-var(--header-height)-2rem)] md:w-48 md:gap-0 md:py-0"
|
||||
ref={anchorRef}
|
||||
>
|
||||
<div className="hidden items-center gap-2 px-[calc(--spacing(2.5))] pb-1 md:flex md:flex-col md:items-start">
|
||||
<HugeiconsIcon
|
||||
icon={Settings05Icon}
|
||||
className="size-4"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<div className="relative flex flex-col gap-1 rounded-lg text-[13px]/snug">
|
||||
<div className="flex items-center gap-1 font-medium text-balance">
|
||||
Build your own shadcn/ui
|
||||
</div>
|
||||
<div className="hidden md:flex">
|
||||
When you're done, click Create Project to start a new project.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="no-scrollbar h-14 overflow-x-auto overflow-y-hidden p-px md:h-full md:overflow-x-hidden md:overflow-y-auto">
|
||||
<FieldGroup className="flex h-full flex-1 flex-row gap-2 md:flex-col md:gap-0">
|
||||
<PresetPicker
|
||||
presets={PRESETS}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<BasePicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<StylePicker
|
||||
styles={STYLES}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<ThemePicker
|
||||
themes={availableThemes}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<CustomizerControls className="mt-auto hidden w-full flex-col md:flex" />
|
||||
</FieldGroup>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
174
apps/v4/app/(create)/components/design-system-provider.tsx
Normal file
174
apps/v4/app/(create)/components/design-system-provider.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
buildRegistryTheme,
|
||||
DEFAULT_CONFIG,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function DesignSystemProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [
|
||||
{ style, theme, font, baseColor, menuAccent, menuColor, radius },
|
||||
setSearchParams,
|
||||
] = useDesignSystemSearchParams({
|
||||
shallow: true, // No need to go through the server…
|
||||
history: "replace", // …or push updates into the iframe history.
|
||||
})
|
||||
useIframeMessageListener("design-system-params", setSearchParams)
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
|
||||
// Use useLayoutEffect for synchronous style updates to prevent flash.
|
||||
React.useLayoutEffect(() => {
|
||||
if (!style || !theme || !font || !baseColor) {
|
||||
return
|
||||
}
|
||||
|
||||
const body = document.body
|
||||
|
||||
// Update style class in place (remove old, add new).
|
||||
body.classList.forEach((className) => {
|
||||
if (className.startsWith("style-")) {
|
||||
body.classList.remove(className)
|
||||
}
|
||||
})
|
||||
body.classList.add(`style-${style}`)
|
||||
|
||||
// Update base color class in place.
|
||||
body.classList.forEach((className) => {
|
||||
if (className.startsWith("base-color-")) {
|
||||
body.classList.remove(className)
|
||||
}
|
||||
})
|
||||
body.classList.add(`base-color-${baseColor}`)
|
||||
|
||||
// Update font.
|
||||
const selectedFont = FONTS.find((f) => f.value === font)
|
||||
if (selectedFont) {
|
||||
const fontFamily = selectedFont.font.style.fontFamily
|
||||
document.documentElement.style.setProperty("--font-sans", fontFamily)
|
||||
}
|
||||
|
||||
setIsReady(true)
|
||||
}, [style, theme, font, baseColor])
|
||||
|
||||
const registryTheme = React.useMemo(() => {
|
||||
if (!baseColor || !theme || !menuAccent || !radius) {
|
||||
return null
|
||||
}
|
||||
|
||||
const config: DesignSystemConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
baseColor,
|
||||
theme,
|
||||
menuAccent,
|
||||
radius,
|
||||
}
|
||||
|
||||
return buildRegistryTheme(config)
|
||||
}, [baseColor, theme, menuAccent, radius])
|
||||
|
||||
// Use useLayoutEffect for synchronous CSS var updates.
|
||||
React.useLayoutEffect(() => {
|
||||
if (!registryTheme || !registryTheme.cssVars) {
|
||||
return
|
||||
}
|
||||
|
||||
const styleId = "design-system-theme-vars"
|
||||
let styleElement = document.getElementById(
|
||||
styleId
|
||||
) as HTMLStyleElement | null
|
||||
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement("style")
|
||||
styleElement.id = styleId
|
||||
document.head.appendChild(styleElement)
|
||||
}
|
||||
|
||||
const {
|
||||
light: lightVars,
|
||||
dark: darkVars,
|
||||
theme: themeVars,
|
||||
} = registryTheme.cssVars
|
||||
|
||||
let cssText = ":root {\n"
|
||||
// Add theme vars (shared across light/dark).
|
||||
if (themeVars) {
|
||||
Object.entries(themeVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
cssText += ` --${key}: ${value};\n`
|
||||
}
|
||||
})
|
||||
}
|
||||
// Add light mode vars.
|
||||
if (lightVars) {
|
||||
Object.entries(lightVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
cssText += ` --${key}: ${value};\n`
|
||||
}
|
||||
})
|
||||
}
|
||||
cssText += "}\n\n"
|
||||
|
||||
cssText += ".dark {\n"
|
||||
if (darkVars) {
|
||||
Object.entries(darkVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
cssText += ` --${key}: ${value};\n`
|
||||
}
|
||||
})
|
||||
}
|
||||
cssText += "}\n"
|
||||
|
||||
styleElement.textContent = cssText
|
||||
}, [registryTheme])
|
||||
|
||||
// Handle menu color inversion by adding/removing dark class to elements with cn-menu-target.
|
||||
React.useEffect(() => {
|
||||
if (!menuColor) {
|
||||
return
|
||||
}
|
||||
|
||||
const updateMenuElements = () => {
|
||||
const menuElements = document.querySelectorAll(".cn-menu-target")
|
||||
menuElements.forEach((element) => {
|
||||
if (menuColor === "inverted") {
|
||||
element.classList.add("dark")
|
||||
} else {
|
||||
element.classList.remove("dark")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Update existing menu elements.
|
||||
updateMenuElements()
|
||||
|
||||
// Watch for new menu elements being added to the DOM.
|
||||
const observer = new MutationObserver(() => {
|
||||
updateMenuElements()
|
||||
})
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [menuColor])
|
||||
|
||||
if (!isReady) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
103
apps/v4/app/(create)/components/font-picker.tsx
Normal file
103
apps/v4/app/(create)/components/font-picker.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { type FontValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { type Font } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function FontPicker({
|
||||
fonts,
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
fonts: readonly Font[]
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentFont = React.useMemo(
|
||||
() => fonts.find((font) => font.value === params.font),
|
||||
[fonts, params.font]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Font</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentFont?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none"
|
||||
style={{ fontFamily: currentFont?.font.style.fontFamily }}
|
||||
>
|
||||
Aa
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-80 md:w-72"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentFont?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ font: value as FontValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{fonts.map((font, index) => (
|
||||
<React.Fragment key={font.value}>
|
||||
<PickerRadioItem value={font.value}>
|
||||
<Item size="xs">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemTitle className="text-muted-foreground text-xs font-medium">
|
||||
{font.name}
|
||||
</ItemTitle>
|
||||
<ItemDescription
|
||||
style={{ fontFamily: font.font.style.fontFamily }}
|
||||
>
|
||||
Designers love packing quirky glyphs into test
|
||||
phrases.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</PickerRadioItem>
|
||||
{index < fonts.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="font"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
331
apps/v4/app/(create)/components/icon-library-picker.tsx
Normal file
331
apps/v4/app/(create)/components/icon-library-picker.tsx
Normal file
@@ -0,0 +1,331 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { lazy, memo, Suspense } from "react"
|
||||
|
||||
import { Item, ItemContent, ItemTitle } from "@/registry/bases/radix/ui/item"
|
||||
import {
|
||||
iconLibraries,
|
||||
type IconLibrary,
|
||||
type IconLibraryName,
|
||||
} from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const IconLucide = lazy(() =>
|
||||
import("@/registry/icons/icon-lucide").then((mod) => ({
|
||||
default: mod.IconLucide,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconTabler = lazy(() =>
|
||||
import("@/registry/icons/icon-tabler").then((mod) => ({
|
||||
default: mod.IconTabler,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconHugeicons = lazy(() =>
|
||||
import("@/registry/icons/icon-hugeicons").then((mod) => ({
|
||||
default: mod.IconHugeicons,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconPhosphor = lazy(() =>
|
||||
import("@/registry/icons/icon-phosphor").then((mod) => ({
|
||||
default: mod.IconPhosphor,
|
||||
}))
|
||||
)
|
||||
|
||||
const PREVIEW_ICONS = {
|
||||
lucide: [
|
||||
"CopyIcon",
|
||||
"CircleAlertIcon",
|
||||
"TrashIcon",
|
||||
"ShareIcon",
|
||||
"ShoppingBagIcon",
|
||||
"MoreHorizontalIcon",
|
||||
"Loader2Icon",
|
||||
"PlusIcon",
|
||||
"MinusIcon",
|
||||
"ArrowLeftIcon",
|
||||
"ArrowRightIcon",
|
||||
"CheckIcon",
|
||||
"ChevronDownIcon",
|
||||
"ChevronRightIcon",
|
||||
],
|
||||
tabler: [
|
||||
"IconCopy",
|
||||
"IconExclamationCircle",
|
||||
"IconTrash",
|
||||
"IconShare",
|
||||
"IconShoppingBag",
|
||||
"IconDots",
|
||||
"IconLoader",
|
||||
"IconPlus",
|
||||
"IconMinus",
|
||||
"IconArrowLeft",
|
||||
"IconArrowRight",
|
||||
"IconCheck",
|
||||
"IconChevronDown",
|
||||
"IconChevronRight",
|
||||
],
|
||||
hugeicons: [
|
||||
"Copy01Icon",
|
||||
"AlertCircleIcon",
|
||||
"Delete02Icon",
|
||||
"Share03Icon",
|
||||
"ShoppingBag01Icon",
|
||||
"MoreHorizontalCircle01Icon",
|
||||
"Loading03Icon",
|
||||
"PlusSignIcon",
|
||||
"MinusSignIcon",
|
||||
"ArrowLeft02Icon",
|
||||
"ArrowRight02Icon",
|
||||
"Tick02Icon",
|
||||
"ArrowDown01Icon",
|
||||
"ArrowRight01Icon",
|
||||
],
|
||||
phosphor: [
|
||||
"CopyIcon",
|
||||
"WarningCircleIcon",
|
||||
"TrashIcon",
|
||||
"ShareIcon",
|
||||
"BagIcon",
|
||||
"DotsThreeIcon",
|
||||
"SpinnerIcon",
|
||||
"PlusIcon",
|
||||
"MinusIcon",
|
||||
"ArrowLeftIcon",
|
||||
"ArrowRightIcon",
|
||||
"CheckIcon",
|
||||
"CaretDownIcon",
|
||||
"CaretRightIcon",
|
||||
],
|
||||
}
|
||||
|
||||
const logos = {
|
||||
lucide: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
d="M14 12a4 4 0 0 0-8 0 8 8 0 1 0 16 0 11.97 11.97 0 0 0-4-8.944"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
d="M10 12a4 4 0 0 0 8 0 8 8 0 1 0-16 0 11.97 11.97 0 0 0 4.063 9"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
tabler: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="32"
|
||||
fill="none"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M31.288 7.107A8.83 8.83 0 0 0 24.893.712a55.9 55.9 0 0 0-17.786 0A8.83 8.83 0 0 0 .712 7.107a55.9 55.9 0 0 0 0 17.786 8.83 8.83 0 0 0 6.395 6.395c5.895.95 11.89.95 17.786 0a8.83 8.83 0 0 0 6.395-6.395c.95-5.895.95-11.89 0-17.786"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="m17.884 9.076 1.5-2.488 6.97 6.977-2.492 1.494zm-7.96 3.127 7.814-.909 3.91 3.66-.974 7.287-9.582 2.159a3.06 3.06 0 0 1-2.17-.329l5.244-4.897c.91.407 2.003.142 2.587-.626.584-.77.488-1.818-.226-2.484s-1.84-.755-2.664-.21c-.823.543-1.107 1.562-.67 2.412l-5.245 4.89a2.53 2.53 0 0 1-.339-2.017z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
hugeicons: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M2 9.5H22" stroke="currentColor"></path>
|
||||
<path
|
||||
d="M20.5 9.5H3.5L4.23353 15.3682C4.59849 18.2879 4.78097 19.7477 5.77343 20.6239C6.76589 21.5 8.23708 21.5 11.1795 21.5H12.8205C15.7629 21.5 17.2341 21.5 18.2266 20.6239C19.219 19.7477 19.4015 18.2879 19.7665 15.3682L20.5 9.5Z"
|
||||
stroke="currentColor"
|
||||
></path>
|
||||
<path
|
||||
d="M5 9C5 5.41015 8.13401 2.5 12 2.5C15.866 2.5 19 5.41015 19 9"
|
||||
stroke="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
phosphor: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
width="32"
|
||||
height="32"
|
||||
>
|
||||
<path fill="none" d="M0 0h32v32H0z" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5h9v16H9zm9 16v9a9 9 0 0 1-9-9M9 5l9 16m0 0h1a8 8 0 0 0 0-16h-1"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export function IconLibraryPicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentIconLibrary = React.useMemo(
|
||||
() => iconLibraries[params.iconLibrary as keyof typeof iconLibraries],
|
||||
[params.iconLibrary]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Icon Library</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentIconLibrary?.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
{logos[currentIconLibrary?.name as keyof typeof logos]}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentIconLibrary?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ iconLibrary: value as IconLibraryName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{Object.values(iconLibraries).map((iconLibrary, index) => (
|
||||
<React.Fragment key={iconLibrary.name}>
|
||||
<IconLibraryPickerItem
|
||||
iconLibrary={iconLibrary}
|
||||
value={iconLibrary.name}
|
||||
/>
|
||||
{index < Object.values(iconLibraries).length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="iconLibrary"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function IconLibraryPickerItem({
|
||||
iconLibrary,
|
||||
value,
|
||||
}: {
|
||||
iconLibrary: IconLibrary
|
||||
value: string
|
||||
}) {
|
||||
return (
|
||||
<PickerRadioItem
|
||||
value={value}
|
||||
className="pr-2 *:data-[slot=dropdown-menu-radio-item-indicator]:hidden"
|
||||
>
|
||||
<Item size="xs">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemTitle className="text-muted-foreground text-xs font-medium">
|
||||
{iconLibrary.title}
|
||||
</ItemTitle>
|
||||
<IconLibraryPreview iconLibrary={iconLibrary.name} />
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
const IconLibraryPreview = memo(function IconLibraryPreview({
|
||||
iconLibrary,
|
||||
}: {
|
||||
iconLibrary: IconLibraryName
|
||||
}) {
|
||||
const previewIcons = PREVIEW_ICONS[iconLibrary]
|
||||
|
||||
if (!previewIcons) {
|
||||
return null
|
||||
}
|
||||
|
||||
const IconRenderer =
|
||||
iconLibrary === "lucide"
|
||||
? IconLucide
|
||||
: iconLibrary === "tabler"
|
||||
? IconTabler
|
||||
: iconLibrary === "hugeicons"
|
||||
? IconHugeicons
|
||||
: IconPhosphor
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="-mx-1 grid w-full grid-cols-7 gap-2">
|
||||
{previewIcons.map((iconName) => (
|
||||
<div
|
||||
key={iconName}
|
||||
className="bg-muted size-6 animate-pulse rounded"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="-mx-1 grid w-full grid-cols-7 gap-2">
|
||||
{previewIcons.map((iconName) => (
|
||||
<div
|
||||
key={iconName}
|
||||
className="flex size-6 items-center justify-center *:[svg]:size-5"
|
||||
>
|
||||
<IconRenderer name={iconName} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Suspense>
|
||||
)
|
||||
})
|
||||
57
apps/v4/app/(create)/components/icon-placeholder.tsx
Normal file
57
apps/v4/app/(create)/components/icon-placeholder.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import { lazy, Suspense } from "react"
|
||||
import { SquareIcon } from "lucide-react"
|
||||
import type { IconLibraryName } from "shadcn/icons"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const IconLucide = lazy(() =>
|
||||
import("@/registry/icons/icon-lucide").then((mod) => ({
|
||||
default: mod.IconLucide,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconTabler = lazy(() =>
|
||||
import("@/registry/icons/icon-tabler").then((mod) => ({
|
||||
default: mod.IconTabler,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconHugeicons = lazy(() =>
|
||||
import("@/registry/icons/icon-hugeicons").then((mod) => ({
|
||||
default: mod.IconHugeicons,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconPhosphor = lazy(() =>
|
||||
import("@/registry/icons/icon-phosphor").then((mod) => ({
|
||||
default: mod.IconPhosphor,
|
||||
}))
|
||||
)
|
||||
|
||||
export function IconPlaceholder({
|
||||
...props
|
||||
}: {
|
||||
[K in IconLibraryName]: string
|
||||
} & React.ComponentProps<"svg">) {
|
||||
const [{ iconLibrary }] = useDesignSystemSearchParams()
|
||||
const iconName = props[iconLibrary]
|
||||
|
||||
if (!iconName) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<SquareIcon {...props} />}>
|
||||
{iconLibrary === "lucide" && <IconLucide name={iconName} {...props} />}
|
||||
{iconLibrary === "tabler" && <IconTabler name={iconName} {...props} />}
|
||||
{iconLibrary === "hugeicons" && (
|
||||
<IconHugeicons name={iconName} {...props} />
|
||||
)}
|
||||
{iconLibrary === "phosphor" && (
|
||||
<IconPhosphor name={iconName} {...props} />
|
||||
)}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
108
apps/v4/app/(create)/components/item-explorer.tsx
Normal file
108
apps/v4/app/(create)/components/item-explorer.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import { ChevronRightIcon } from "lucide-react"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type Base } from "@/registry/bases"
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/registry/new-york-v4/ui/collapsible"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
||||
|
||||
const cachedGroupedItems = React.cache(
|
||||
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
|
||||
return groupItemsByType(items)
|
||||
}
|
||||
)
|
||||
|
||||
export function ItemExplorer({
|
||||
base,
|
||||
items,
|
||||
}: {
|
||||
base: Base["name"]
|
||||
items: Pick<RegistryItem, "name" | "title" | "type">[]
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const groupedItems = React.useMemo(() => cachedGroupedItems(items), [items])
|
||||
|
||||
const currentItem = React.useMemo(
|
||||
() => items.find((item) => item.name === params.item) ?? null,
|
||||
[items, params.item]
|
||||
)
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
className="sticky z-30 hidden h-[calc(100svh-var(--header-height)-2rem)] overscroll-none bg-transparent xl:flex"
|
||||
collapsible="none"
|
||||
>
|
||||
<SidebarContent className="no-scrollbar -mx-1 overflow-x-hidden">
|
||||
{groupedItems.map((group) => (
|
||||
<Collapsible
|
||||
key={group.type}
|
||||
defaultOpen
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarGroup className="px-1 py-0">
|
||||
<CollapsibleTrigger className="flex w-full items-center gap-1 py-1.5 text-[0.8rem] font-medium [&[data-state=open]>svg]:rotate-90">
|
||||
<ChevronRightIcon className="text-muted-foreground size-3.5 transition-transform" />
|
||||
<span>{group.title}</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="border-border/50 relative ml-1.5 border-l pl-2">
|
||||
{group.items.map((item, index) => (
|
||||
<SidebarMenuItem key={item.name} className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 absolute top-1/2 -left-2 h-px w-2 border-t",
|
||||
index === group.items.length - 1 && "bg-sidebar"
|
||||
)}
|
||||
/>
|
||||
{index === group.items.length - 1 && (
|
||||
<div className="bg-sidebar absolute top-1/2 -bottom-1 -left-2.5 w-1" />
|
||||
)}
|
||||
<SidebarMenuButton
|
||||
onClick={() => setParams({ item: item.name })}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[26px] w-fit cursor-pointer overflow-visible border border-transparent text-[0.8rem] font-normal after:absolute after:inset-x-0 after:-inset-y-1 after:-z-0 after:rounded-md"
|
||||
data-active={item.name === currentItem?.name}
|
||||
isActive={item.name === currentItem?.name}
|
||||
>
|
||||
{item.title}
|
||||
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
|
||||
</SidebarMenuButton>
|
||||
<Link
|
||||
href={`/preview/${base}/${item.name}`}
|
||||
prefetch
|
||||
className="sr-only"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</SidebarGroup>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
193
apps/v4/app/(create)/components/item-picker.tsx
Normal file
193
apps/v4/app/(create)/components/item-picker.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Script from "next/script"
|
||||
import { Search01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxCollection,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxLabel,
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
} from "@/registry/new-york-v4/ui/combobox"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
||||
|
||||
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
||||
|
||||
const cachedGroupedItems = React.cache(
|
||||
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
|
||||
return groupItemsByType(items)
|
||||
}
|
||||
)
|
||||
|
||||
export function ItemPicker({
|
||||
items,
|
||||
}: {
|
||||
items: Pick<RegistryItem, "name" | "title" | "type">[]
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const groupedItems = React.useMemo(() => cachedGroupedItems(items), [items])
|
||||
|
||||
const currentItem = React.useMemo(
|
||||
() => items.find((item) => item.name === params.item) ?? null,
|
||||
[items, params.item]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if ((e.key === "k" || e.key === "p") && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
setOpen((open) => !open)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [])
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(item: Pick<RegistryItem, "name" | "title" | "type">) => {
|
||||
setParams({ item: item.name })
|
||||
setOpen(false)
|
||||
},
|
||||
[setParams]
|
||||
)
|
||||
|
||||
const comboboxValue = React.useMemo(() => {
|
||||
return currentItem ?? null
|
||||
}, [currentItem])
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
autoHighlight
|
||||
items={groupedItems}
|
||||
value={comboboxValue}
|
||||
onValueChange={(value) => {
|
||||
if (value) {
|
||||
handleSelect(value)
|
||||
}
|
||||
}}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
itemToStringValue={(item) => {
|
||||
if (!item) {
|
||||
return ""
|
||||
}
|
||||
// Handle both groups and items.
|
||||
if ("items" in item) {
|
||||
return item.title ?? ""
|
||||
}
|
||||
return item.title ?? item.name ?? ""
|
||||
}}
|
||||
>
|
||||
<ComboboxTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label="Select item"
|
||||
size="sm"
|
||||
className="data-popup-open:bg-muted dark:data-popup-open:bg-muted/50 bg-muted/50 sm:bg-background md:dark:bg-background border-foreground/10 dark:bg-muted/50 h-[calc(--spacing(13.5))] flex-1 touch-manipulation justify-between gap-2 rounded-xl pr-4! pl-2.5 text-left shadow-none select-none *:data-[slot=combobox-trigger-icon]:hidden sm:h-8 sm:max-w-56 sm:rounded-lg sm:pr-2! xl:max-w-md"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ComboboxValue>
|
||||
{(value) => (
|
||||
<>
|
||||
<div className="flex flex-col justify-start text-left sm:hidden">
|
||||
<div className="text-muted-foreground text-xs font-normal">
|
||||
Preview
|
||||
</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{value?.title || "Not Found"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground hidden flex-1 text-sm sm:flex">
|
||||
{value?.title || "Not Found"}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
<HugeiconsIcon icon={Search01Icon} />
|
||||
</ComboboxTrigger>
|
||||
<ComboboxContent
|
||||
className="ring-foreground/10 min-w-[calc(var(--available-width)---spacing(4))] translate-x-2 animate-none rounded-xl border-0 ring-1 data-open:animate-none sm:min-w-[calc(var(--anchor-width)+--spacing(7))] sm:translate-x-0"
|
||||
side="bottom"
|
||||
align="center"
|
||||
>
|
||||
<ComboboxInput
|
||||
showTrigger={false}
|
||||
placeholder="Search"
|
||||
className="bg-muted h-8 rounded-lg shadow-none has-focus-visible:border-inherit! has-focus-visible:ring-0! pointer-coarse:hidden"
|
||||
/>
|
||||
<ComboboxEmpty>No items found.</ComboboxEmpty>
|
||||
<ComboboxList className="no-scrollbar scroll-my-1 pb-1">
|
||||
{(group) => (
|
||||
<ComboboxGroup key={group.type} items={group.items}>
|
||||
<ComboboxLabel>{group.title}</ComboboxLabel>
|
||||
<ComboboxCollection>
|
||||
{(item) => (
|
||||
<ComboboxItem
|
||||
key={item.name}
|
||||
value={item}
|
||||
className="group/combobox-item rounded-lg pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base"
|
||||
>
|
||||
{item.title}
|
||||
<span className="text-muted-foreground ml-auto text-xs opacity-0 group-data-[selected=true]/combobox-item:opacity-100">
|
||||
{group.title}
|
||||
</span>
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxCollection>
|
||||
</ComboboxGroup>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
<div
|
||||
data-open={open}
|
||||
className="fixed inset-0 z-50 hidden bg-transparent data-[open=true]:block"
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemPickerScript() {
|
||||
return (
|
||||
<Script
|
||||
id="design-system-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
// Forward Cmd/Ctrl + K and Cmd/Ctrl + P
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'k' || e.key === 'p') && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: '${CMD_K_FORWARD_TYPE}',
|
||||
key: e.key
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
50
apps/v4/app/(create)/components/lock-button.tsx
Normal file
50
apps/v4/app/(create)/components/lock-button.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
SquareLock01Icon,
|
||||
SquareUnlock01Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
import { useLocks, type LockableParam } from "@/app/(create)/hooks/use-locks"
|
||||
|
||||
export function LockButton({
|
||||
param,
|
||||
className,
|
||||
}: {
|
||||
param: LockableParam
|
||||
className?: string
|
||||
}) {
|
||||
const { isLocked, toggleLock } = useLocks()
|
||||
const locked = isLocked(param)
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleLock(param)}
|
||||
data-locked={locked}
|
||||
className={cn(
|
||||
"flex size-4 cursor-pointer items-center justify-center rounded opacity-0 transition-opacity group-focus-within/picker:opacity-100 group-hover/picker:opacity-100 focus:opacity-100 data-[locked=true]:opacity-100 pointer-coarse:hidden",
|
||||
className
|
||||
)}
|
||||
aria-label={locked ? "Unlock" : "Lock"}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={locked ? SquareLock01Icon : SquareUnlock01Icon}
|
||||
strokeWidth={2}
|
||||
className="text-foreground size-5"
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{locked ? "Unlock" : "Lock"}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
163
apps/v4/app/(create)/components/menu-picker.tsx
Normal file
163
apps/v4/app/(create)/components/menu-picker.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import { type MenuColorValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const MENU_OPTIONS = [
|
||||
{
|
||||
value: "default" as const,
|
||||
label: "Default",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M8.5 11.5L14.5001 11.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.5 15H13.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 8H15.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "inverted" as const,
|
||||
label: "Inverted",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
className="fill-foreground text-foreground"
|
||||
>
|
||||
<path
|
||||
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M8.5 11.5L14.5001 11.5"
|
||||
stroke="var(--background)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.5 15H13.5"
|
||||
stroke="var(--background)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 8H15.5"
|
||||
stroke="var(--background)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
] as const
|
||||
|
||||
export function MenuColorPicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const currentMenu = MENU_OPTIONS.find(
|
||||
(menu) => menu.value === params.menuColor
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger disabled={mounted && resolvedTheme === "dark"}>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Menu Color</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentMenu?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
{currentMenu?.icon}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentMenu?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuColor: value as MenuColorValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_OPTIONS.map((menu) => (
|
||||
<PickerRadioItem key={menu.value} value={menu.value}>
|
||||
{menu.icon}
|
||||
{menu.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
287
apps/v4/app/(create)/components/picker.tsx
Normal file
287
apps/v4/app/(create)/components/picker.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
||||
|
||||
import { cn } from "@/registry/bases/base/lib/utils"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
function Picker({ ...props }: MenuPrimitive.Root.Props) {
|
||||
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function PickerPortal({ ...props }: MenuPrimitive.Portal.Props) {
|
||||
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
}
|
||||
|
||||
function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
|
||||
return (
|
||||
<MenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
className={cn(
|
||||
"hover:bg-muted data-popup-open:bg-muted border-foreground/10 bg-muted/50 relative w-[160px] shrink-0 touch-manipulation rounded-xl border p-2 select-none disabled:opacity-50 md:w-full md:rounded-lg md:border-transparent md:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerContent({
|
||||
align = "start",
|
||||
alignOffset = 0,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
anchor,
|
||||
className,
|
||||
...props
|
||||
}: MenuPrimitive.Popup.Props &
|
||||
Pick<
|
||||
MenuPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset" | "anchor"
|
||||
>) {
|
||||
return (
|
||||
<MenuPrimitive.Portal>
|
||||
<MenuPrimitive.Positioner
|
||||
className="isolate z-50 outline-none"
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
anchor={anchor}
|
||||
>
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground cn-menu-target ring-foreground/10 no-scrollbar z-50 max-h-(--available-height) w-[calc(var(--available-width)-(--spacing(3.5)))] min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border-0 p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden md:w-52",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
<div className="absolute inset-0 z-40 bg-transparent" />
|
||||
</MenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerGroup({ ...props }: MenuPrimitive.Group.Props) {
|
||||
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
}
|
||||
|
||||
function PickerLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.GroupLabel.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.GroupLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-muted-foreground px-2 py-1.5 text-xs font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: MenuPrimitive.Item.Props & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
|
||||
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function PickerSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: MenuPrimitive.SubmenuTrigger.Props & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.SubmenuTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<IconPlaceholder
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
className="ml-auto"
|
||||
/>
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerSubContent({
|
||||
align = "start",
|
||||
alignOffset = -3,
|
||||
side = "right",
|
||||
sideOffset = 0,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PickerContent>) {
|
||||
return (
|
||||
<PickerContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground w-auto min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100",
|
||||
className
|
||||
)}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: MenuPrimitive.CheckboxItem.Props) {
|
||||
return (
|
||||
<MenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
<IconPlaceholder
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
/>
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
|
||||
return (
|
||||
<MenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: MenuPrimitive.RadioItem.Props) {
|
||||
return (
|
||||
<MenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
<IconPlaceholder
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
className="size-4 pointer-coarse:size-5"
|
||||
/>
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerSeparator({
|
||||
className,
|
||||
...props
|
||||
}: MenuPrimitive.Separator.Props) {
|
||||
return (
|
||||
<MenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PickerShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Picker,
|
||||
PickerPortal,
|
||||
PickerTrigger,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerLabel,
|
||||
PickerItem,
|
||||
PickerCheckboxItem,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerShortcut,
|
||||
PickerSub,
|
||||
PickerSubTrigger,
|
||||
PickerSubContent,
|
||||
}
|
||||
122
apps/v4/app/(create)/components/preset-picker.tsx
Normal file
122
apps/v4/app/(create)/components/preset-picker.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { STYLES, type Preset } from "@/registry/config"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function PresetPicker({
|
||||
presets,
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
presets: readonly Preset[]
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentPreset = React.useMemo(() => {
|
||||
return presets.find(
|
||||
(preset) =>
|
||||
preset.base === params.base &&
|
||||
preset.style === params.style &&
|
||||
preset.baseColor === params.baseColor &&
|
||||
preset.theme === params.theme &&
|
||||
preset.iconLibrary === params.iconLibrary &&
|
||||
preset.font === params.font &&
|
||||
preset.menuAccent === params.menuAccent &&
|
||||
preset.menuColor === params.menuColor &&
|
||||
preset.radius === params.radius
|
||||
)
|
||||
}, [
|
||||
presets,
|
||||
params.base,
|
||||
params.style,
|
||||
params.baseColor,
|
||||
params.theme,
|
||||
params.iconLibrary,
|
||||
params.font,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
params.radius,
|
||||
])
|
||||
|
||||
// Filter presets for current base only
|
||||
const currentBasePresets = React.useMemo(() => {
|
||||
return presets.filter((preset) => preset.base === params.base)
|
||||
}, [presets, params.base])
|
||||
|
||||
const handlePresetChange = (value: string) => {
|
||||
const preset = presets.find((p) => p.title === value)
|
||||
if (!preset) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update all params including base.
|
||||
setParams({
|
||||
base: preset.base,
|
||||
style: preset.style,
|
||||
baseColor: preset.baseColor,
|
||||
theme: preset.theme,
|
||||
iconLibrary: preset.iconLibrary,
|
||||
font: preset.font,
|
||||
menuAccent: preset.menuAccent,
|
||||
menuColor: preset.menuColor,
|
||||
radius: preset.radius,
|
||||
custom: false,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Preset</div>
|
||||
<div className="text-foreground line-clamp-1 text-sm font-medium">
|
||||
{currentPreset?.description ?? "Custom"}
|
||||
</div>
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="md:w-72"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentPreset?.title ?? ""}
|
||||
onValueChange={handlePresetChange}
|
||||
>
|
||||
<PickerGroup>
|
||||
{currentBasePresets.map((preset) => {
|
||||
const style = STYLES.find((s) => s.name === preset.style)
|
||||
return (
|
||||
<PickerRadioItem key={preset.title} value={preset.title}>
|
||||
<div className="flex items-center gap-2">
|
||||
{style?.icon && (
|
||||
<div className="flex size-4 shrink-0 items-center justify-center">
|
||||
{React.cloneElement(style.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{preset.description}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
})}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
40
apps/v4/app/(create)/components/preview-controls.tsx
Normal file
40
apps/v4/app/(create)/components/preview-controls.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import { Monitor, Smartphone, Tablet } from "lucide-react"
|
||||
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function PreviewControls() {
|
||||
const [params, setParams] = useDesignSystemSearchParams({
|
||||
history: "replace",
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex h-8 items-center gap-1.5 rounded-md border p-1">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={params.size.toString()}
|
||||
onValueChange={(newValue) => {
|
||||
if (newValue) {
|
||||
setParams({ size: parseInt(newValue) })
|
||||
}
|
||||
}}
|
||||
className="gap-1 *:data-[slot=toggle-group-item]:!size-6 *:data-[slot=toggle-group-item]:!rounded-sm"
|
||||
>
|
||||
<ToggleGroupItem value="100" title="Desktop">
|
||||
<Monitor />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="60" title="Tablet">
|
||||
<Tablet />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30" title="Mobile">
|
||||
<Smartphone />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
apps/v4/app/(create)/components/preview-style.tsx
Normal file
16
apps/v4/app/(create)/components/preview-style.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client"
|
||||
|
||||
export function PreviewStyle() {
|
||||
return (
|
||||
<style jsx global>{`
|
||||
html {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
)
|
||||
}
|
||||
126
apps/v4/app/(create)/components/preview.tsx
Normal file
126
apps/v4/app/(create)/components/preview.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type ImperativePanelHandle } from "react-resizable-panels"
|
||||
|
||||
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/customizer-controls"
|
||||
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker"
|
||||
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import {
|
||||
serializeDesignSystemSearchParams,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function Preview() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
|
||||
|
||||
// Sync resizable panel with URL param changes.
|
||||
React.useEffect(() => {
|
||||
if (resizablePanelRef.current && params.size) {
|
||||
resizablePanelRef.current.resize(params.size)
|
||||
}
|
||||
}, [params.size])
|
||||
|
||||
React.useEffect(() => {
|
||||
const iframe = iframeRef.current
|
||||
if (!iframe) {
|
||||
return
|
||||
}
|
||||
|
||||
const sendParams = () => {
|
||||
sendToIframe(iframe, "design-system-params", params)
|
||||
}
|
||||
|
||||
if (iframe.contentWindow) {
|
||||
sendParams()
|
||||
}
|
||||
|
||||
iframe.addEventListener("load", sendParams)
|
||||
return () => {
|
||||
iframe.removeEventListener("load", sendParams)
|
||||
}
|
||||
}, [params])
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === CMD_K_FORWARD_TYPE) {
|
||||
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
|
||||
const key = event.data.key || "k"
|
||||
|
||||
const syntheticEvent = new KeyboardEvent("keydown", {
|
||||
key,
|
||||
metaKey: isMac,
|
||||
ctrlKey: !isMac,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
document.dispatchEvent(syntheticEvent)
|
||||
}
|
||||
|
||||
if (event.data.type === RANDOMIZE_FORWARD_TYPE) {
|
||||
const key = event.data.key || "r"
|
||||
|
||||
const syntheticEvent = new KeyboardEvent("keydown", {
|
||||
key,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
document.dispatchEvent(syntheticEvent)
|
||||
}
|
||||
|
||||
if (event.data.type === DARK_MODE_FORWARD_TYPE) {
|
||||
const key = event.data.key || "d"
|
||||
|
||||
const syntheticEvent = new KeyboardEvent("keydown", {
|
||||
key,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
document.dispatchEvent(syntheticEvent)
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const iframeSrc = React.useMemo(() => {
|
||||
// The iframe src needs to include the serialized design system params
|
||||
// for the initial load, but not be reactive to them as it would cause
|
||||
// full-iframe reloads on every param change (flashes & loss of state).
|
||||
// Further updates of the search params will be sent to the iframe
|
||||
// via a postMessage channel, for it to sync its own history onto the host's.
|
||||
return serializeDesignSystemSearchParams(
|
||||
`/preview/${params.base}/${params.item}`,
|
||||
params
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params.base, params.item])
|
||||
|
||||
return (
|
||||
<div className="relative -mx-1 flex flex-1 flex-col justify-center sm:mx-0">
|
||||
<div className="ring-foreground/15 3xl:max-h-[1200px] 3xl:max-w-[1800px] relative -z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden rounded-2xl ring-1">
|
||||
<div className="bg-muted dark:bg-muted/30 absolute inset-0 rounded-2xl" />
|
||||
<iframe
|
||||
key={params.base + params.item}
|
||||
ref={iframeRef}
|
||||
src={iframeSrc}
|
||||
className="z-10 size-full flex-1"
|
||||
title="Preview"
|
||||
/>
|
||||
<Badge
|
||||
className="absolute right-2 bottom-2 isolate z-10"
|
||||
variant="secondary"
|
||||
>
|
||||
Preview
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
101
apps/v4/app/(create)/components/radius-picker.tsx
Normal file
101
apps/v4/app/(create)/components/radius-picker.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import { RADII, type RadiusValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function RadiusPicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentRadius = RADII.find((radius) => radius.name === params.radius)
|
||||
const defaultRadius = RADII.find((radius) => radius.name === "default")
|
||||
const otherRadii = RADII.filter((radius) => radius.name !== "default")
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Radius</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentRadius?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base select-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 20v-5C4 8.925 8.925 4 15 4h5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentRadius?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ radius: value as RadiusValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{defaultRadius && (
|
||||
<PickerRadioItem
|
||||
key={defaultRadius.name}
|
||||
value={defaultRadius.name}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{defaultRadius.label}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Use radius from style
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{otherRadii.map((radius) => (
|
||||
<PickerRadioItem key={radius.name} value={radius.name}>
|
||||
{radius.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="radius"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
apps/v4/app/(create)/components/share-button.tsx
Normal file
73
apps/v4/app/(create)/components/share-button.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Share03Icon, Tick02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function ShareButton() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
const shareUrl = React.useMemo(() => {
|
||||
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
|
||||
return `${origin}/create?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&item=${params.item}`
|
||||
}, [
|
||||
params.base,
|
||||
params.style,
|
||||
params.baseColor,
|
||||
params.theme,
|
||||
params.iconLibrary,
|
||||
params.font,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
params.radius,
|
||||
params.item,
|
||||
])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
const timer = setTimeout(() => setHasCopied(false), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [hasCopied])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
copyToClipboardWithMeta(shareUrl, {
|
||||
name: "copy_create_share_url",
|
||||
properties: {
|
||||
url: shareUrl,
|
||||
},
|
||||
})
|
||||
setHasCopied(true)
|
||||
}, [shareUrl])
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="rounded-lg shadow-none"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
|
||||
)}
|
||||
Share
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Copy Link</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
96
apps/v4/app/(create)/components/style-picker.tsx
Normal file
96
apps/v4/app/(create)/components/style-picker.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { type Style, type StyleName } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function StylePicker({
|
||||
styles,
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
styles: readonly Style[]
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentStyle = styles.find((style) => style.name === params.style)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Style</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentStyle?.title}
|
||||
</div>
|
||||
</div>
|
||||
{currentStyle?.icon && (
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center select-none">
|
||||
{React.cloneElement(currentStyle.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="md:w-64"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentStyle?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ style: value as StyleName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{styles.map((style, index) => (
|
||||
<React.Fragment key={style.name}>
|
||||
<PickerRadioItem value={style.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{style.icon && (
|
||||
<div className="flex size-4 translate-y-0.5 items-center justify-center">
|
||||
{React.cloneElement(style.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{style.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
{style.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
{index < styles.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="style"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
94
apps/v4/app/(create)/components/template-picker.tsx
Normal file
94
apps/v4/app/(create)/components/template-picker.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const TEMPLATES = [
|
||||
{
|
||||
value: "next",
|
||||
title: "Next.js",
|
||||
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Next.js</title><path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z"/></svg>',
|
||||
},
|
||||
{
|
||||
value: "start",
|
||||
title: "TanStack Start",
|
||||
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TanStack</title><path d="M11.078.042c.316-.042.65-.014.97-.014 1.181 0 2.341.184 3.472.532a12.3 12.3 0 0 1 3.973 2.086 11.9 11.9 0 0 1 3.432 4.33c1.446 3.15 1.436 6.97-.046 10.107-.958 2.029-2.495 3.727-4.356 4.965-1.518 1.01-3.293 1.629-5.1 1.848-2.298.279-4.784-.129-6.85-1.188-3.88-1.99-6.518-5.994-6.57-10.382-.01-.846.003-1.697.17-2.534.273-1.365.748-2.683 1.463-3.88a12 12 0 0 1 2.966-3.36A12.3 12.3 0 0 1 9.357.3a12 12 0 0 1 1.255-.2l.133-.016zM7.064 19.99c-.535.057-1.098.154-1.557.454.103.025.222 0 .33 0 .258 0 .52-.01.778.002.647.028 1.32.131 1.945.303.8.22 1.505.65 2.275.942.813.307 1.622.402 2.484.402.435 0 .866-.001 1.287-.12-.22-.117-.534-.095-.778-.144a11 11 0 0 1-1.556-.416 12 12 0 0 1-1.093-.467l-.23-.108a15 15 0 0 0-1.012-.44c-.905-.343-1.908-.512-2.873-.408m.808-2.274c-1.059 0-2.13.187-3.083.667q-.346.177-.659.41c-.063.046-.175.106-.199.188s.061.151.11.204c.238-.127.464-.261.718-.357 1.64-.624 3.63-.493 5.268.078.817.285 1.569.712 2.365 1.046.89.374 1.798.616 2.753.74 1.127.147 2.412.028 3.442-.48.362-.179.865-.451 1.018-.847-.189.017-.36.098-.539.154a9 9 0 0 1-.868.222c-.994.2-2.052.24-3.053.06-.943-.17-1.82-.513-2.693-.873l-.111-.046-.223-.092-.112-.046a26 26 0 0 0-1.35-.527c-.89-.31-1.842-.5-2.784-.5M9.728 1.452c-1.27.28-2.407.826-3.502 1.514-.637.4-1.245.81-1.796 1.323-.82.765-1.447 1.695-1.993 2.666-.563 1-.924 2.166-1.098 3.297-.172 1.11-.2 2.277-.004 3.388.245 1.388.712 2.691 1.448 3.897.248-.116.424-.38.629-.557.414-.359.85-.691 1.317-.978a3.5 3.5 0 0 1 .539-.264c.07-.029.187-.055.22-.132.053-.124-.045-.34-.062-.468a7 7 0 0 1-.068-1.109 9.7 9.7 0 0 1 .61-3.177c.29-.76.73-1.45 1.254-2.069.177-.21.365-.405.56-.6.115-.114.258-.212.33-.359-.376 0-.751.108-1.108.218-.769.237-1.518.588-2.155 1.084-.291.226-.504.522-.779.76-.084.073-.235.17-.352.116-.176-.083-.149-.43-.169-.59-.078-.612.154-1.387.45-1.918.473-.852 1.348-1.58 2.376-1.555.444.011.833.166 1.257.266-.107-.153-.252-.264-.389-.39a5.4 5.4 0 0 0-1.107-.8c-.163-.085-.338-.136-.509-.2-.086-.03-.195-.074-.227-.17-.06-.177.26-.342.377-.417.453-.289 1.01-.527 1.556-.54.854-.021 1.688.452 2.04 1.258.123.284.16.583.184.885l.004.057.006.085.002.029.005.057.004.056c.268-.218.457-.54.718-.774.612-.547 1.45-.79 2.245-.544a2.97 2.97 0 0 1 1.71 1.378c.097.173.365.595.171.767-.152.134-.344.03-.504-.026a3 3 0 0 0-.372-.094l-.068-.014-.069-.013a3.9 3.9 0 0 0-1.377-.002c-.282.05-.557.15-.838.192v.06c.768.006 1.51.444 1.89 1.109.157.275.235.59.295.9.075.38.022.796-.082 1.168-.035.125-.098.336-.247.365-.106.02-.195-.085-.256-.155a4.6 4.6 0 0 0-.492-.522 20 20 0 0 0-1.467-1.14c-.267-.19-.56-.44-.868-.556.087.208.171.402.2.63.088.667-.192 1.296-.612 1.798a2.6 2.6 0 0 1-.426.427c-.067.05-.151.114-.24.1-.277-.044-.31-.463-.353-.677-.144-.726-.086-1.447.114-2.158-.178.09-.307.287-.418.45a5.3 5.3 0 0 0-.612 1.138c-.61 1.617-.604 3.51.186 5.066.088.174.221.15.395.15h.157a3 3 0 0 1 .472.018c.08.01.193 0 .257.06.077.072.036.194.018.282-.05.246-.066.469-.066.72.328-.051.419-.576.535-.84.131-.298.265-.597.387-.9.06-.148.14-.314.119-.479-.024-.185-.157-.381-.25-.54-.177-.298-.378-.606-.508-.929-.104-.258-.007-.58.286-.672.161-.05.334.049.439.166.22.244.363.609.523.896l1.249 2.248q.159.286.32.57c.043.074.086.188.173.219.077.028.182-.012.26-.027.198-.04.398-.083.598-.12.24-.043.605-.035.778-.222-.253-.08-.545-.075-.808-.057-.158.01-.333.067-.479-.025-.216-.137-.36-.455-.492-.667-.326-.525-.633-1.057-.945-1.59l-.05-.084-.1-.17q-.075-.126-.149-.255c-.037-.066-.092-.153-.039-.227.056-.076.179-.08.29-.081h.021q.066.001.117-.004a10 10 0 0 1 1.347-.107c-.035-.122-.135-.26-.103-.39.071-.292.49-.383.686-.174.131.14.207.334.292.504.113.223.24.44.361.66.211.383.441.757.658 1.138l.055.094.028.047c.093.156.187.314.238.489-.753-.035-1.318-.909-1.646-1.499-.027.095.016.179.05.27q.103.282.262.54c.152.244.326.495.556.673.408.315.945.317 1.436.283.315-.022.708-.165 1.018-.068s.434.438.25.7c-.138.196-.321.27-.55.3.162.346.373.667.527 1.02.064.146.13.37.283.448.102.051.248.003.358 0-.11-.292-.317-.54-.419-.839.31.015.61.176.898.28.567.202 1.128.424 1.687.648l.258.104c.23.092.462.183.689.283.083.037.198.123.29.07.074-.043.123-.146.169-.215a10.3 10.3 0 0 0 1.393-3.208c.75-2.989.106-6.287-1.695-8.783-.692-.96-1.562-1.789-2.522-2.476-2.401-1.718-5.551-2.407-8.44-1.768m4.908 14.904c-.636.166-1.292.317-1.945.401.086.293.296.577.45.84.059.101.122.237.24.281.132.05.292-.03.417-.072-.058-.158-.155-.3-.235-.45-.033-.06-.084-.133-.056-.206.05-.137.263-.13.381-.153.31-.063.617-.142.928-.204.114-.023.274-.085.389-.047.086.03.138.1.187.174l.022.033q.043.07.097.122c.125.113.313.13.472.162-.097-.219-.259-.41-.362-.63-.06-.127-.11-.315-.242-.388-.182-.102-.557.089-.743.137m-4.01-1.457c-.03.38-.147.689-.33 1.019.21.026.423.036.629.087.154.038.296.11.449.153-.082-.224-.233-.423-.35-.63-.12-.208-.226-.462-.398-.63"/></svg>',
|
||||
},
|
||||
{
|
||||
value: "vite",
|
||||
title: "Vite",
|
||||
logo: '<svg xmlns="http://www.w3.org/2000/svg" width="410" height="404" fill="none" viewBox="0 0 410 404"><path fill="var(--foreground)" d="m399.641 59.525-183.998 329.02c-3.799 6.793-13.559 6.833-17.415.073L10.582 59.556C6.38 52.19 12.68 43.266 21.028 44.76l184.195 32.923c1.175.21 2.378.208 3.553-.006l180.343-32.87c8.32-1.517 14.649 7.337 10.522 14.719"/><path fill="var(--background)" d="M292.965 1.574 156.801 28.255a5 5 0 0 0-4.03 4.611l-8.376 141.464c-.197 3.332 2.863 5.918 6.115 5.168l37.91-8.749c3.547-.818 6.752 2.306 6.023 5.873l-11.263 55.153c-.758 3.712 2.727 6.886 6.352 5.785l23.415-7.114c3.63-1.102 7.118 2.081 6.35 5.796l-17.899 86.633c-1.12 5.419 6.088 8.374 9.094 3.728l2.008-3.103 110.954-221.428c1.858-3.707-1.346-7.935-5.418-7.15l-39.022 7.532c-3.667.707-6.787-2.708-5.752-6.296l25.469-88.291c1.036-3.594-2.095-7.012-5.766-6.293"/></svg>',
|
||||
},
|
||||
] as const
|
||||
|
||||
export function TemplatePicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentTemplate = TEMPLATES.find(
|
||||
(template) => template.value === params.template
|
||||
)
|
||||
|
||||
return (
|
||||
<Picker>
|
||||
<PickerTrigger className="hidden md:flex">
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Template</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentTemplate?.title}
|
||||
</div>
|
||||
</div>
|
||||
{currentTemplate?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 select-none *:[svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentTemplate.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={params.template}
|
||||
onValueChange={(value) => {
|
||||
setParams({
|
||||
template: value as "next" | "start" | "vite",
|
||||
})
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{TEMPLATES.map((template) => (
|
||||
<PickerRadioItem key={template.value} value={template.value}>
|
||||
{template.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! size-4 shrink-0 [&_svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: template.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{template.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
166
apps/v4/app/(create)/components/theme-picker.tsx
Normal file
166
apps/v4/app/(create)/components/theme-picker.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function ThemePicker({
|
||||
themes,
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
themes: readonly Theme[]
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const currentTheme = React.useMemo(
|
||||
() => themes.find((theme) => theme.name === params.theme),
|
||||
[themes, params.theme]
|
||||
)
|
||||
|
||||
const currentThemeIsBaseColor = React.useMemo(
|
||||
() => BASE_COLORS.find((baseColor) => baseColor.name === params.theme),
|
||||
[params.theme]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentTheme && themes.length > 0) {
|
||||
setParams({ theme: themes[0].name })
|
||||
}
|
||||
}, [currentTheme, themes, setParams])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Theme</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentTheme?.title}
|
||||
</div>
|
||||
</div>
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
currentTheme?.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.[
|
||||
currentThemeIsBaseColor ? "muted-foreground" : "primary"
|
||||
],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-96"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentTheme?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ theme: value as ThemeName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter((theme) =>
|
||||
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
|
||||
)
|
||||
.map((theme) => {
|
||||
const isBaseColor = BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.[
|
||||
isBaseColor ? "muted-foreground" : "primary"
|
||||
],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 translate-y-1 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{theme.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Match base color
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
})}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter(
|
||||
(theme) =>
|
||||
!BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
)
|
||||
.map((theme) => {
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["primary"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{theme.title}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
})}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="theme"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
273
apps/v4/app/(create)/components/toolbar-controls.tsx
Normal file
273
apps/v4/app/(create)/components/toolbar-controls.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ComputerTerminal01Icon,
|
||||
Copy01Icon,
|
||||
Tick02Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const TEMPLATES = [
|
||||
{
|
||||
value: "next",
|
||||
title: "Next.js",
|
||||
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Next.js</title><path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" fill="currentColor"/></svg>',
|
||||
},
|
||||
{
|
||||
value: "start",
|
||||
title: "TanStack Start",
|
||||
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TanStack</title><path d="M11.078.042c.316-.042.65-.014.97-.014 1.181 0 2.341.184 3.472.532a12.3 12.3 0 0 1 3.973 2.086 11.9 11.9 0 0 1 3.432 4.33c1.446 3.15 1.436 6.97-.046 10.107-.958 2.029-2.495 3.727-4.356 4.965-1.518 1.01-3.293 1.629-5.1 1.848-2.298.279-4.784-.129-6.85-1.188-3.88-1.99-6.518-5.994-6.57-10.382-.01-.846.003-1.697.17-2.534.273-1.365.748-2.683 1.463-3.88a12 12 0 0 1 2.966-3.36A12.3 12.3 0 0 1 9.357.3a12 12 0 0 1 1.255-.2l.133-.016zM7.064 19.99c-.535.057-1.098.154-1.557.454.103.025.222 0 .33 0 .258 0 .52-.01.778.002.647.028 1.32.131 1.945.303.8.22 1.505.65 2.275.942.813.307 1.622.402 2.484.402.435 0 .866-.001 1.287-.12-.22-.117-.534-.095-.778-.144a11 11 0 0 1-1.556-.416 12 12 0 0 1-1.093-.467l-.23-.108a15 15 0 0 0-1.012-.44c-.905-.343-1.908-.512-2.873-.408m.808-2.274c-1.059 0-2.13.187-3.083.667q-.346.177-.659.41c-.063.046-.175.106-.199.188s.061.151.11.204c.238-.127.464-.261.718-.357 1.64-.624 3.63-.493 5.268.078.817.285 1.569.712 2.365 1.046.89.374 1.798.616 2.753.74 1.127.147 2.412.028 3.442-.48.362-.179.865-.451 1.018-.847-.189.017-.36.098-.539.154a9 9 0 0 1-.868.222c-.994.2-2.052.24-3.053.06-.943-.17-1.82-.513-2.693-.873l-.111-.046-.223-.092-.112-.046a26 26 0 0 0-1.35-.527c-.89-.31-1.842-.5-2.784-.5M9.728 1.452c-1.27.28-2.407.826-3.502 1.514-.637.4-1.245.81-1.796 1.323-.82.765-1.447 1.695-1.993 2.666-.563 1-.924 2.166-1.098 3.297-.172 1.11-.2 2.277-.004 3.388.245 1.388.712 2.691 1.448 3.897.248-.116.424-.38.629-.557.414-.359.85-.691 1.317-.978a3.5 3.5 0 0 1 .539-.264c.07-.029.187-.055.22-.132.053-.124-.045-.34-.062-.468a7 7 0 0 1-.068-1.109 9.7 9.7 0 0 1 .61-3.177c.29-.76.73-1.45 1.254-2.069.177-.21.365-.405.56-.6.115-.114.258-.212.33-.359-.376 0-.751.108-1.108.218-.769.237-1.518.588-2.155 1.084-.291.226-.504.522-.779.76-.084.073-.235.17-.352.116-.176-.083-.149-.43-.169-.59-.078-.612.154-1.387.45-1.918.473-.852 1.348-1.58 2.376-1.555.444.011.833.166 1.257.266-.107-.153-.252-.264-.389-.39a5.4 5.4 0 0 0-1.107-.8c-.163-.085-.338-.136-.509-.2-.086-.03-.195-.074-.227-.17-.06-.177.26-.342.377-.417.453-.289 1.01-.527 1.556-.54.854-.021 1.688.452 2.04 1.258.123.284.16.583.184.885l.004.057.006.085.002.029.005.057.004.056c.268-.218.457-.54.718-.774.612-.547 1.45-.79 2.245-.544a2.97 2.97 0 0 1 1.71 1.378c.097.173.365.595.171.767-.152.134-.344.03-.504-.026a3 3 0 0 0-.372-.094l-.068-.014-.069-.013a3.9 3.9 0 0 0-1.377-.002c-.282.05-.557.15-.838.192v.06c.768.006 1.51.444 1.89 1.109.157.275.235.59.295.9.075.38.022.796-.082 1.168-.035.125-.098.336-.247.365-.106.02-.195-.085-.256-.155a4.6 4.6 0 0 0-.492-.522 20 20 0 0 0-1.467-1.14c-.267-.19-.56-.44-.868-.556.087.208.171.402.2.63.088.667-.192 1.296-.612 1.798a2.6 2.6 0 0 1-.426.427c-.067.05-.151.114-.24.1-.277-.044-.31-.463-.353-.677-.144-.726-.086-1.447.114-2.158-.178.09-.307.287-.418.45a5.3 5.3 0 0 0-.612 1.138c-.61 1.617-.604 3.51.186 5.066.088.174.221.15.395.15h.157a3 3 0 0 1 .472.018c.08.01.193 0 .257.06.077.072.036.194.018.282-.05.246-.066.469-.066.72.328-.051.419-.576.535-.84.131-.298.265-.597.387-.9.06-.148.14-.314.119-.479-.024-.185-.157-.381-.25-.54-.177-.298-.378-.606-.508-.929-.104-.258-.007-.58.286-.672.161-.05.334.049.439.166.22.244.363.609.523.896l1.249 2.248q.159.286.32.57c.043.074.086.188.173.219.077.028.182-.012.26-.027.198-.04.398-.083.598-.12.24-.043.605-.035.778-.222-.253-.08-.545-.075-.808-.057-.158.01-.333.067-.479-.025-.216-.137-.36-.455-.492-.667-.326-.525-.633-1.057-.945-1.59l-.05-.084-.1-.17q-.075-.126-.149-.255c-.037-.066-.092-.153-.039-.227.056-.076.179-.08.29-.081h.021q.066.001.117-.004a10 10 0 0 1 1.347-.107c-.035-.122-.135-.26-.103-.39.071-.292.49-.383.686-.174.131.14.207.334.292.504.113.223.24.44.361.66.211.383.441.757.658 1.138l.055.094.028.047c.093.156.187.314.238.489-.753-.035-1.318-.909-1.646-1.499-.027.095.016.179.05.27q.103.282.262.54c.152.244.326.495.556.673.408.315.945.317 1.436.283.315-.022.708-.165 1.018-.068s.434.438.25.7c-.138.196-.321.27-.55.3.162.346.373.667.527 1.02.064.146.13.37.283.448.102.051.248.003.358 0-.11-.292-.317-.54-.419-.839.31.015.61.176.898.28.567.202 1.128.424 1.687.648l.258.104c.23.092.462.183.689.283.083.037.198.123.29.07.074-.043.123-.146.169-.215a10.3 10.3 0 0 0 1.393-3.208c.75-2.989.106-6.287-1.695-8.783-.692-.96-1.562-1.789-2.522-2.476-2.401-1.718-5.551-2.407-8.44-1.768m4.908 14.904c-.636.166-1.292.317-1.945.401.086.293.296.577.45.84.059.101.122.237.24.281.132.05.292-.03.417-.072-.058-.158-.155-.3-.235-.45-.033-.06-.084-.133-.056-.206.05-.137.263-.13.381-.153.31-.063.617-.142.928-.204.114-.023.274-.085.389-.047.086.03.138.1.187.174l.022.033q.043.07.097.122c.125.113.313.13.472.162-.097-.219-.259-.41-.362-.63-.06-.127-.11-.315-.242-.388-.182-.102-.557.089-.743.137m-4.01-1.457c-.03.38-.147.689-.33 1.019.21.026.423.036.629.087.154.038.296.11.449.153-.082-.224-.233-.423-.35-.63-.12-.208-.226-.462-.398-.63" fill="currentColor"/></svg>',
|
||||
},
|
||||
{
|
||||
value: "vite",
|
||||
title: "Vite",
|
||||
logo: '<svg xmlns="http://www.w3.org/2000/svg" width="410" height="404" fill="none" viewBox="0 0 410 404"><path fill="var(--foreground)" d="m399.641 59.525-183.998 329.02c-3.799 6.793-13.559 6.833-17.415.073L10.582 59.556C6.38 52.19 12.68 43.266 21.028 44.76l184.195 32.923c1.175.21 2.378.208 3.553-.006l180.343-32.87c8.32-1.517 14.649 7.337 10.522 14.719"/><path fill="var(--background)" d="M292.965 1.574 156.801 28.255a5 5 0 0 0-4.03 4.611l-8.376 141.464c-.197 3.332 2.863 5.918 6.115 5.168l37.91-8.749c3.547-.818 6.752 2.306 6.023 5.873l-11.263 55.153c-.758 3.712 2.727 6.886 6.352 5.785l23.415-7.114c3.63-1.102 7.118 2.081 6.35 5.796l-17.899 86.633c-1.12 5.419 6.088 8.374 9.094 3.728l2.008-3.103 110.954-221.428c1.858-3.707-1.346-7.935-5.418-7.15l-39.022 7.532c-3.667.707-6.787-2.708-5.752-6.296l25.469-88.291c1.036-3.594-2.095-7.012-5.766-6.293"/></svg>',
|
||||
},
|
||||
] as const
|
||||
|
||||
export function ToolbarControls() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const [config, setConfig] = useConfig()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
const packageManager = config.packageManager || "pnpm"
|
||||
|
||||
const commands = React.useMemo(() => {
|
||||
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
|
||||
const url = `${origin}/init?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&template=${params.template}`
|
||||
const templateFlag = params.template ? ` --template ${params.template}` : ""
|
||||
return {
|
||||
pnpm: `pnpm dlx shadcn@latest create --preset "${url}"${templateFlag}`,
|
||||
npm: `npx shadcn@latest create --preset "${url}"${templateFlag}`,
|
||||
yarn: `yarn dlx shadcn@latest create --preset "${url}"${templateFlag}`,
|
||||
bun: `bunx --bun shadcn@latest create --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
}, [
|
||||
params.base,
|
||||
params.style,
|
||||
params.baseColor,
|
||||
params.theme,
|
||||
params.iconLibrary,
|
||||
params.font,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
params.radius,
|
||||
params.template,
|
||||
])
|
||||
|
||||
const command = commands[packageManager]
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
const timer = setTimeout(() => setHasCopied(false), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [hasCopied])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
const properties: Record<string, string> = {
|
||||
command,
|
||||
}
|
||||
if (params.template) {
|
||||
properties.template = params.template
|
||||
}
|
||||
copyToClipboardWithMeta(command, {
|
||||
name: "copy_npm_command",
|
||||
properties,
|
||||
})
|
||||
setOpen(false)
|
||||
setHasCopied(true)
|
||||
toast("Command copied to clipboard.", {
|
||||
description:
|
||||
"Paste and run the command in your terminal to create a new shadcn/ui project.",
|
||||
position: "bottom-center",
|
||||
classNames: {
|
||||
content: "rounded-xl",
|
||||
toast: "rounded-xl!",
|
||||
description: "text-sm/leading-normal!",
|
||||
},
|
||||
})
|
||||
}, [command, params.template, setOpen])
|
||||
|
||||
const handleCopyFromTabs = React.useCallback(() => {
|
||||
const properties: Record<string, string> = {
|
||||
command,
|
||||
}
|
||||
if (params.template) {
|
||||
properties.template = params.template
|
||||
}
|
||||
copyToClipboardWithMeta(command, {
|
||||
name: "copy_npm_command",
|
||||
properties,
|
||||
})
|
||||
setHasCopied(true)
|
||||
}, [command, params.template])
|
||||
|
||||
const selectedTemplate = TEMPLATES.find(
|
||||
(template) => template.value === params.template
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" className="hidden h-[31px] rounded-lg pl-2 md:flex">
|
||||
<HugeiconsIcon
|
||||
icon={ComputerTerminal01Icon}
|
||||
className="hidden xl:flex"
|
||||
/>
|
||||
Create Project
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="dialog-ring min-w-0 overflow-hidden rounded-xl sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
<DialogDescription className="text-balance">
|
||||
Select a template and run this command to create a{" "}
|
||||
{selectedTemplate?.title} + shadcn/ui project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="template" className="sr-only">
|
||||
Template
|
||||
</FieldLabel>
|
||||
<RadioGroup
|
||||
id="template"
|
||||
value={params.template}
|
||||
onValueChange={(value) => {
|
||||
setParams({
|
||||
template: value as "next" | "start" | "vite",
|
||||
})
|
||||
}}
|
||||
className="grid grid-cols-3 gap-2"
|
||||
>
|
||||
{TEMPLATES.map((template) => (
|
||||
<FieldLabel
|
||||
key={template.value}
|
||||
htmlFor={template.value}
|
||||
className="rounded-lg!"
|
||||
>
|
||||
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!">
|
||||
<RadioGroupItem
|
||||
value={template.value}
|
||||
id={template.value}
|
||||
className="sr-only"
|
||||
/>
|
||||
{template.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! size-6 [&_svg]:size-6"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: template.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FieldTitle>{template.title}</FieldTitle>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig({
|
||||
...config,
|
||||
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
|
||||
})
|
||||
}}
|
||||
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
|
||||
>
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
|
||||
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
|
||||
<TabsTrigger value="npm">npm</TabsTrigger>
|
||||
<TabsTrigger value="yarn">yarn</TabsTrigger>
|
||||
<TabsTrigger value="bun">bun</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto size-7 rounded-lg"
|
||||
onClick={handleCopyFromTabs}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{hasCopied ? "Copied!" : "Copy command"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="bg-surface border-border/50 text-surface-foreground relative overflow-hidden border-t px-3 py-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<DialogFooter className="bg-muted/50 -mx-6 mt-2 -mb-6 flex flex-col gap-2 border-t p-6 sm:flex-col">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="h-9 w-full rounded-lg"
|
||||
>
|
||||
Copy Command
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
54
apps/v4/app/(create)/components/v0-button.tsx
Normal file
54
apps/v4/app/(create)/components/v0-button.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Skeleton } from "@/registry/new-york-v4/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function V0Button({ className }: { className?: string }) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const isMobile = useIsMobile()
|
||||
const isMounted = useMounted()
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_APP_URL}/create/v0?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&item=${params.item}`
|
||||
|
||||
if (!isMounted) {
|
||||
return <Skeleton className="h-8 w-24 rounded-lg" />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isMobile ? "default" : "outline"}
|
||||
className={cn(
|
||||
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px]",
|
||||
className
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`}
|
||||
target="_blank"
|
||||
>
|
||||
Open in <Icons.v0 className="size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Open current design in v0</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
69
apps/v4/app/(create)/components/welcome-dialog.tsx
Normal file
69
apps/v4/app/(create)/components/welcome-dialog.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Icons } from "@/components/icons"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
|
||||
const STORAGE_KEY = "shadcn-create-welcome-dialog"
|
||||
|
||||
export function WelcomeDialog() {
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const dismissed = localStorage.getItem(STORAGE_KEY)
|
||||
if (!dismissed) {
|
||||
setIsOpen(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setIsOpen(open)
|
||||
if (!open) {
|
||||
localStorage.setItem(STORAGE_KEY, "true")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className="dialog-ring max-w-[23rem] min-w-0 gap-0 overflow-hidden rounded-xl p-0 sm:max-w-sm dark:bg-neutral-900"
|
||||
>
|
||||
<div className="flex aspect-[2/1.2] w-full items-center justify-center rounded-t-xl bg-neutral-950 text-center text-neutral-100 sm:aspect-[2/1]">
|
||||
<div className="font-mono text-2xl font-bold">
|
||||
<Icons.logo className="size-12" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogHeader className="gap-1 p-4">
|
||||
<DialogTitle className="text-left text-base">
|
||||
Build your own shadcn/ui
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-foreground text-left leading-relaxed">
|
||||
Customize everything from the ground up. Pick your component
|
||||
library, font, color scheme, and more.
|
||||
</DialogDescription>
|
||||
<DialogDescription className="text-foreground mt-2 text-left leading-relaxed font-medium">
|
||||
Available for Next.js, Vite, TanStack Start, and v0.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="p-4 pt-0">
|
||||
<DialogClose asChild>
|
||||
<Button className="w-full rounded-lg shadow-none">
|
||||
Get Started
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
9
apps/v4/app/(create)/create/layout.tsx
Normal file
9
apps/v4/app/(create)/create/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { LocksProvider } from "@/app/(create)/hooks/use-locks"
|
||||
|
||||
export default function CreateLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <LocksProvider>{children}</LocksProvider>
|
||||
}
|
||||
142
apps/v4/app/(create)/create/page.tsx
Normal file
142
apps/v4/app/(create)/create/page.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { type Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeftIcon } from "lucide-react"
|
||||
import type { SearchParams } from "nuqs/server"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { ModeSwitcher } from "@/components/mode-switcher"
|
||||
import { SiteConfig } from "@/components/site-config"
|
||||
import { BASES } from "@/registry/config"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { Customizer } from "@/app/(create)/components/customizer"
|
||||
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
|
||||
import { ItemExplorer } from "@/app/(create)/components/item-explorer"
|
||||
import { ItemPicker } from "@/app/(create)/components/item-picker"
|
||||
import { Preview } from "@/app/(create)/components/preview"
|
||||
import { ShareButton } from "@/app/(create)/components/share-button"
|
||||
import { ToolbarControls } from "@/app/(create)/components/toolbar-controls"
|
||||
import { V0Button } from "@/app/(create)/components/v0-button"
|
||||
import { WelcomeDialog } from "@/app/(create)/components/welcome-dialog"
|
||||
import { getItemsForBase } from "@/app/(create)/lib/api"
|
||||
import { loadDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "New Project",
|
||||
description:
|
||||
"Customize everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.",
|
||||
openGraph: {
|
||||
title: "New Project",
|
||||
description:
|
||||
"Customize everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.",
|
||||
type: "website",
|
||||
url: absoluteUrl("/create"),
|
||||
images: [
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "New Project",
|
||||
description:
|
||||
"Customize everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.",
|
||||
images: [siteConfig.ogImage],
|
||||
creator: "@shadcn",
|
||||
},
|
||||
}
|
||||
|
||||
export default async function CreatePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<SearchParams>
|
||||
}) {
|
||||
const params = await loadDesignSystemSearchParams(searchParams)
|
||||
const base = BASES.find((b) => b.name === params.base) ?? BASES[0]
|
||||
|
||||
const items = await getItemsForBase(base.name)
|
||||
|
||||
const filteredItems = items
|
||||
.filter((item) => item !== null)
|
||||
.map((item) => ({
|
||||
name: item.name,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="layout"
|
||||
className="section-soft relative z-10 flex min-h-svh flex-col"
|
||||
>
|
||||
<header className="sticky top-0 z-50 w-full">
|
||||
<div className="container-wrapper 3xl:fixed:px-0 px-6">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
|
||||
<div className="flex items-center xl:w-1/3">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-lg shadow-none"
|
||||
>
|
||||
<Link href="/">
|
||||
<ArrowLeftIcon />
|
||||
Back
|
||||
</Link>
|
||||
</Button>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 hidden sm:mx-4 lg:flex"
|
||||
/>
|
||||
<div className="text-muted-foreground hidden text-sm font-medium lg:flex">
|
||||
New Project
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center gap-2 px-4.5 pb-4 sm:static sm:justify-end sm:p-0 lg:ml-0 xl:justify-center">
|
||||
<ItemPicker items={filteredItems} />
|
||||
<CustomizerControls className="sm:hidden" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-2 hidden sm:flex xl:hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end xl:ml-auto xl:w-1/3">
|
||||
<SiteConfig className="3xl:flex hidden" />
|
||||
<Separator orientation="vertical" className="3xl:flex hidden" />
|
||||
<ModeSwitcher />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-0 -ml-2 sm:ml-0"
|
||||
/>
|
||||
<ShareButton />
|
||||
<V0Button />
|
||||
<ToolbarControls />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex flex-1 flex-col pb-16 sm:pb-0">
|
||||
<SidebarProvider className="flex h-auto min-h-min flex-1 flex-col items-start overflow-hidden px-0">
|
||||
<div
|
||||
data-slot="designer"
|
||||
className="3xl:fixed:container flex w-full flex-1 flex-col gap-2 p-6 pt-1 pb-4 [--sidebar-width:--spacing(40)] sm:gap-2 sm:pt-2 md:flex-row md:pb-6 2xl:gap-6"
|
||||
>
|
||||
<ItemExplorer base={base.name} items={filteredItems} />
|
||||
<Preview />
|
||||
<Customizer />
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
<WelcomeDialog />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
391
apps/v4/app/(create)/create/v0/route.ts
Normal file
391
apps/v4/app/(create)/create/v0/route.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
import { NextResponse, type NextRequest } from "next/server"
|
||||
import { track } from "@vercel/analytics/server"
|
||||
import dedent from "dedent"
|
||||
import {
|
||||
registryItemFileSchema,
|
||||
registryItemSchema,
|
||||
type configSchema,
|
||||
type RegistryItem,
|
||||
} from "shadcn/schema"
|
||||
import { transformIcons, transformMenu, transformRender } from "shadcn/utils"
|
||||
import { Project, ScriptKind } from "ts-morph"
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
buildRegistryBase,
|
||||
designSystemConfigSchema,
|
||||
fonts,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
|
||||
const { Index } = await import("@/registry/bases/__index__")
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
|
||||
const parseResult = designSystemConfigSchema.safeParse({
|
||||
base: searchParams.get("base"),
|
||||
style: searchParams.get("style"),
|
||||
iconLibrary: searchParams.get("iconLibrary"),
|
||||
baseColor: searchParams.get("baseColor"),
|
||||
theme: searchParams.get("theme"),
|
||||
font: searchParams.get("font"),
|
||||
item: searchParams.get("item"),
|
||||
menuAccent: searchParams.get("menuAccent"),
|
||||
menuColor: searchParams.get("menuColor"),
|
||||
radius: searchParams.get("radius"),
|
||||
})
|
||||
|
||||
if (!parseResult.success) {
|
||||
return NextResponse.json(
|
||||
{ error: parseResult.error.issues[0].message },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const designSystemConfig = parseResult.data
|
||||
const registryBase = buildRegistryBase(designSystemConfig)
|
||||
const validateResult = registryItemSchema.safeParse(registryBase)
|
||||
|
||||
if (!validateResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid registry base item",
|
||||
details: validateResult.error.format(),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
track("create_open_in_v0", designSystemConfig)
|
||||
|
||||
const payload = await buildV0Payload(designSystemConfig)
|
||||
|
||||
return NextResponse.json(payload)
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
error instanceof Error ? error.message : "An unknown error occurred",
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
|
||||
const files: z.infer<typeof registryItemFileSchema>[] = []
|
||||
|
||||
// Build globals.css file.
|
||||
files.push(buildGlobalsCss(designSystemConfig))
|
||||
|
||||
// Build layout.tsx file.
|
||||
files.push(buildLayoutFile(designSystemConfig))
|
||||
|
||||
// Build component files.
|
||||
const componentFiles = await buildComponentFiles(designSystemConfig)
|
||||
files.push(...componentFiles)
|
||||
|
||||
return registryItemSchema.parse({
|
||||
name: designSystemConfig.item ?? "Item",
|
||||
type: "registry:item",
|
||||
files,
|
||||
})
|
||||
}
|
||||
|
||||
function buildGlobalsCss(designSystemConfig: DesignSystemConfig) {
|
||||
const registryBase = buildRegistryBase(designSystemConfig)
|
||||
|
||||
const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
const darkVars = Object.entries(registryBase.cssVars?.dark ?? {})
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
const content = dedent`@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
/* @import "shadcn/tailwind.css"; */
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
}
|
||||
|
||||
:root {
|
||||
${lightVars}
|
||||
}
|
||||
|
||||
.dark {
|
||||
${darkVars}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
return registryItemFileSchema.parse({
|
||||
path: "app/globals.css",
|
||||
type: "registry:file",
|
||||
target: "app/globals.css",
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
function buildLayoutFile(designSystemConfig: DesignSystemConfig) {
|
||||
const font = fonts.find(
|
||||
(font) => font.name === `font-${designSystemConfig.font}`
|
||||
)
|
||||
if (!font) {
|
||||
throw new Error(`Font "${designSystemConfig.font}" not found`)
|
||||
}
|
||||
|
||||
const content = dedent`
|
||||
import type { Metadata } from "next";
|
||||
import { ${font.font.import} } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const fontSans = ${font.font.import}({subsets:['latin'],variable:'--font-sans'});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={fontSans.variable}>
|
||||
<body
|
||||
className="antialiased"
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
`
|
||||
|
||||
return registryItemFileSchema.parse({
|
||||
path: "app/layout.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/layout.tsx",
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
async function buildComponentFiles(designSystemConfig: DesignSystemConfig) {
|
||||
const files = []
|
||||
const allItemsForBase = Object.values(Index[designSystemConfig.base])
|
||||
.filter(
|
||||
(item: RegistryItem) =>
|
||||
item.type === "registry:ui" || item.name === "example"
|
||||
)
|
||||
.map((item) => item.name)
|
||||
|
||||
const registryItemFiles = await Promise.all(
|
||||
allItemsForBase.map(async (name) => {
|
||||
const file = await getRegistryItemFile(name, designSystemConfig)
|
||||
return file
|
||||
})
|
||||
)
|
||||
files.push(...registryItemFiles)
|
||||
|
||||
const pageFile = {
|
||||
path: "app/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/page.tsx",
|
||||
content: dedent`
|
||||
import { Button } from "@/components/ui/button";
|
||||
export default function Page() {
|
||||
return <Button>Click me</Button>
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
// Build the actual item component.
|
||||
if (designSystemConfig.item) {
|
||||
const itemComponentFile = await getRegistryItemFile(
|
||||
designSystemConfig.item,
|
||||
designSystemConfig
|
||||
)
|
||||
if (itemComponentFile) {
|
||||
// Find the export default function from the component file.
|
||||
const exportDefault = itemComponentFile.content.match(
|
||||
/export default function (\w+)/
|
||||
)
|
||||
if (exportDefault) {
|
||||
const functionName = exportDefault[1]
|
||||
|
||||
// Replace the export default function with a named export.
|
||||
itemComponentFile.content = itemComponentFile.content.replace(
|
||||
/export default function (\w+)/,
|
||||
`export function ${functionName}`
|
||||
)
|
||||
|
||||
// Import and render the item on the page.
|
||||
pageFile.content = dedent`import { ${functionName} } from "@/components/${designSystemConfig.item}";
|
||||
|
||||
export default function Page() {
|
||||
return <${functionName} />
|
||||
}`
|
||||
}
|
||||
|
||||
files.push({
|
||||
...itemComponentFile,
|
||||
target: `components/${designSystemConfig.item}.tsx`,
|
||||
type: "registry:component",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
files.push(pageFile)
|
||||
|
||||
return z.array(registryItemFileSchema).parse(files)
|
||||
}
|
||||
|
||||
async function getRegistryItemFile(
|
||||
name: string,
|
||||
designSystemConfig: DesignSystemConfig
|
||||
) {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_APP_URL}/r/styles/${designSystemConfig.base}-${designSystemConfig.style}/${name}.json`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch registry item: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
const item = registryItemSchema.parse(json)
|
||||
|
||||
// Build a v0 config i.e components.json
|
||||
const config = {
|
||||
$schema: "https://ui.shadcn.com/schema.json",
|
||||
style: `${designSystemConfig.base}-${designSystemConfig.style}`,
|
||||
rsc: true,
|
||||
tsx: true,
|
||||
tailwind: {
|
||||
config: "",
|
||||
css: "app/globals.css",
|
||||
baseColor: designSystemConfig.baseColor,
|
||||
cssVariables: true,
|
||||
prefix: "",
|
||||
},
|
||||
iconLibrary: designSystemConfig.iconLibrary,
|
||||
aliases: {
|
||||
components: "@/components",
|
||||
utils: "@/lib/utils",
|
||||
ui: "@/components/ui",
|
||||
lib: "@/lib",
|
||||
hooks: "@/hooks",
|
||||
},
|
||||
menuAccent: designSystemConfig.menuAccent,
|
||||
menuColor: designSystemConfig.menuColor,
|
||||
resolvedPaths: {
|
||||
cwd: "/",
|
||||
tailwindConfig: "./tailwind.config.js",
|
||||
tailwindCss: "./globals.css",
|
||||
utils: "./lib/utils",
|
||||
components: "./components",
|
||||
lib: "./lib",
|
||||
hooks: "./hooks",
|
||||
ui: "./components/ui",
|
||||
},
|
||||
} satisfies z.infer<typeof configSchema>
|
||||
|
||||
const file = item.files?.[0]
|
||||
if (!file?.content) {
|
||||
return null
|
||||
}
|
||||
|
||||
const content = await transformFileContent(file.content, config)
|
||||
|
||||
return {
|
||||
...file,
|
||||
target:
|
||||
name === "example"
|
||||
? "components/example.tsx"
|
||||
: `components/ui/${name}.tsx`,
|
||||
type: name === "example" ? "registry:component" : "registry:ui",
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
const transformers = [transformIcons, transformMenu, transformRender]
|
||||
|
||||
async function transformFileContent(
|
||||
content: string,
|
||||
config: z.infer<typeof configSchema>
|
||||
) {
|
||||
const project = new Project({
|
||||
compilerOptions: {},
|
||||
})
|
||||
|
||||
const sourceFile = project.createSourceFile("component.tsx", content, {
|
||||
scriptKind: ScriptKind.TSX,
|
||||
})
|
||||
|
||||
for (const transformer of transformers) {
|
||||
await transformer({
|
||||
filename: "component.tsx",
|
||||
raw: content,
|
||||
sourceFile,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
return sourceFile.getText()
|
||||
}
|
||||
63
apps/v4/app/(create)/hooks/use-iframe-sync.tsx
Normal file
63
apps/v4/app/(create)/hooks/use-iframe-sync.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import type { DesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
type ParentToIframeMessage = {
|
||||
type: "design-system-params"
|
||||
data: DesignSystemSearchParams
|
||||
}
|
||||
|
||||
export const isInIframe = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return false
|
||||
}
|
||||
return window.self !== window.top
|
||||
}
|
||||
|
||||
export function useIframeMessageListener<
|
||||
Message extends ParentToIframeMessage,
|
||||
MessageType extends Message["type"],
|
||||
>(
|
||||
messageType: MessageType,
|
||||
onMessage: (data: Extract<Message, { type: MessageType }>["data"]) => void
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
if (!isInIframe()) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === messageType) {
|
||||
onMessage(event.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage)
|
||||
}
|
||||
}, [messageType, onMessage])
|
||||
}
|
||||
|
||||
export function sendToIframe<
|
||||
Message extends ParentToIframeMessage,
|
||||
MessageType extends Message["type"],
|
||||
>(
|
||||
iframe: HTMLIFrameElement | null,
|
||||
messageType: MessageType,
|
||||
data: Extract<Message, { type: MessageType }>["data"]
|
||||
) {
|
||||
if (!iframe?.contentWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: messageType,
|
||||
data,
|
||||
},
|
||||
"*"
|
||||
)
|
||||
}
|
||||
57
apps/v4/app/(create)/hooks/use-locks.tsx
Normal file
57
apps/v4/app/(create)/hooks/use-locks.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
export type LockableParam =
|
||||
| "style"
|
||||
| "baseColor"
|
||||
| "theme"
|
||||
| "iconLibrary"
|
||||
| "font"
|
||||
| "menuAccent"
|
||||
| "menuColor"
|
||||
| "radius"
|
||||
|
||||
type LocksContextValue = {
|
||||
locks: Set<LockableParam>
|
||||
isLocked: (param: LockableParam) => boolean
|
||||
toggleLock: (param: LockableParam) => void
|
||||
}
|
||||
|
||||
const LocksContext = React.createContext<LocksContextValue | null>(null)
|
||||
|
||||
export function LocksProvider({ children }: { children: React.ReactNode }) {
|
||||
const [locks, setLocks] = React.useState<Set<LockableParam>>(new Set())
|
||||
|
||||
const isLocked = React.useCallback(
|
||||
(param: LockableParam) => locks.has(param),
|
||||
[locks]
|
||||
)
|
||||
|
||||
const toggleLock = React.useCallback((param: LockableParam) => {
|
||||
setLocks((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(param)) {
|
||||
next.delete(param)
|
||||
} else {
|
||||
next.add(param)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({ locks, isLocked, toggleLock }),
|
||||
[locks, isLocked, toggleLock]
|
||||
)
|
||||
|
||||
return <LocksContext value={value}>{children}</LocksContext>
|
||||
}
|
||||
|
||||
export function useLocks() {
|
||||
const context = React.useContext(LocksContext)
|
||||
if (!context) {
|
||||
throw new Error("useLocks must be used within LocksProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
56
apps/v4/app/(create)/init/route.ts
Normal file
56
apps/v4/app/(create)/init/route.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextResponse, type NextRequest } from "next/server"
|
||||
import { track } from "@vercel/analytics/server"
|
||||
import { registryItemSchema } from "shadcn/schema"
|
||||
|
||||
import { buildRegistryBase, designSystemConfigSchema } from "@/registry/config"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
|
||||
const result = designSystemConfigSchema.safeParse({
|
||||
base: searchParams.get("base"),
|
||||
style: searchParams.get("style"),
|
||||
iconLibrary: searchParams.get("iconLibrary"),
|
||||
baseColor: searchParams.get("baseColor"),
|
||||
theme: searchParams.get("theme"),
|
||||
font: searchParams.get("font"),
|
||||
menuAccent: searchParams.get("menuAccent"),
|
||||
menuColor: searchParams.get("menuColor"),
|
||||
radius: searchParams.get("radius"),
|
||||
template: searchParams.get("template"),
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: result.error.issues[0].message },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const registryBase = buildRegistryBase(result.data)
|
||||
const parseResult = registryItemSchema.safeParse(registryBase)
|
||||
|
||||
if (!parseResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid registry base item",
|
||||
details: parseResult.error.format(),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
track("create_app", result.data)
|
||||
|
||||
return NextResponse.json(parseResult.data)
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
error instanceof Error ? error.message : "An unknown error occurred",
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user