mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
Compare commits
408 Commits
shadcn@3.8
...
shadcn/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86479e0a30 | ||
|
|
62abc6be99 | ||
|
|
0072c9801f | ||
|
|
a4c6504c96 | ||
|
|
1bd5f3d7c8 | ||
|
|
3d6ea09c50 | ||
|
|
de497a36bb | ||
|
|
882a9cb145 | ||
|
|
65cb5b49ff | ||
|
|
ae6f2e67aa | ||
|
|
67c99dd33c | ||
|
|
e489552614 | ||
|
|
8386198073 | ||
|
|
9c570f1435 | ||
|
|
ed2d9a6728 | ||
|
|
f336513d18 | ||
|
|
5755d6aa1f | ||
|
|
e363e343b7 | ||
|
|
fe955258c3 | ||
|
|
f5ac4a0d2a | ||
|
|
97ed7eb35c | ||
|
|
6909385aea | ||
|
|
8dabe113fa | ||
|
|
f5556230f1 | ||
|
|
327551f8b6 | ||
|
|
cdb4a4547f | ||
|
|
52f72b9cf7 | ||
|
|
048dac9359 | ||
|
|
f93d44730e | ||
|
|
21c64cb561 | ||
|
|
7e405f1568 | ||
|
|
04248d752e | ||
|
|
15f6a0fe49 | ||
|
|
54d254100d | ||
|
|
6d7f3479d1 | ||
|
|
5e1fca8b4e | ||
|
|
30229bfd14 | ||
|
|
1ce9c2dd6a | ||
|
|
5edf9c95b7 | ||
|
|
35657b4d5f | ||
|
|
b7786c4b42 | ||
|
|
6ca3784b67 | ||
|
|
c1b92c3175 | ||
|
|
b7afa9ba73 | ||
|
|
119d534e85 | ||
|
|
4e416dea5e | ||
|
|
b600dd7091 | ||
|
|
d59e5be214 | ||
|
|
cddbc1f3ff | ||
|
|
7c0d413e3c | ||
|
|
00de8addfe | ||
|
|
869e7bb17f | ||
|
|
8491d4207a | ||
|
|
6f31c22f11 | ||
|
|
a4c806ec26 | ||
|
|
1445fb769d | ||
|
|
a6bdaa6776 | ||
|
|
b44ca370f1 | ||
|
|
d2776903c2 | ||
|
|
936ee754b1 | ||
|
|
3a431547bb | ||
|
|
066e1e9abd | ||
|
|
b19fa88dec | ||
|
|
3aa50ddc9d | ||
|
|
f26db39334 | ||
|
|
3003e9e67a | ||
|
|
ee1303198a | ||
|
|
acb92a8df9 | ||
|
|
78410f9738 | ||
|
|
edf571debd | ||
|
|
257448bead | ||
|
|
e9f4cfb010 | ||
|
|
3c5f594b94 | ||
|
|
cf3f9f134a | ||
|
|
a643dc6ab5 | ||
|
|
8c705f8af9 | ||
|
|
28104c684d | ||
|
|
eccf6a2522 | ||
|
|
8ba3d50d7d | ||
|
|
75031d4461 | ||
|
|
13e64ea341 | ||
|
|
6034ffcd3c | ||
|
|
a749633d51 | ||
|
|
dad8a74ab4 | ||
|
|
3f03d30ce5 | ||
|
|
3365f4ebb2 | ||
|
|
68b8932406 | ||
|
|
a24351838a | ||
|
|
67b1083f3a | ||
|
|
aa4a97730a | ||
|
|
02f34a3b31 | ||
|
|
7cebd74ce5 | ||
|
|
bd1d93bbbc | ||
|
|
37ff1a3d12 | ||
|
|
308ebdbd3b | ||
|
|
cb6e798b90 | ||
|
|
2224411358 | ||
|
|
586f09a0c0 | ||
|
|
475ae744e6 | ||
|
|
553b6454f1 | ||
|
|
5805be2a2a | ||
|
|
c44d89a742 | ||
|
|
ce3fc7625a | ||
|
|
2532aeaa1d | ||
|
|
a4dafd1b32 | ||
|
|
07c87ff431 | ||
|
|
4a4b379f21 | ||
|
|
837e2bcc93 | ||
|
|
33dc7ea273 | ||
|
|
b8da7ce8b8 | ||
|
|
da3c255575 | ||
|
|
5eaad6ea6c | ||
|
|
f68e240293 | ||
|
|
ddc68e480a | ||
|
|
c31ebfaf6b | ||
|
|
e79f6e74bb | ||
|
|
57f9d875be | ||
|
|
a59144d8e1 | ||
|
|
3d8837bddb | ||
|
|
4d89b13e6f | ||
|
|
7d9689ba01 | ||
|
|
81a1dde380 | ||
|
|
8448acdf90 | ||
|
|
51b867e5dc | ||
|
|
c97ab6ee18 | ||
|
|
9584703534 | ||
|
|
f31ed81983 | ||
|
|
e85a698821 | ||
|
|
2bb09a50a1 | ||
|
|
17ed9baedb | ||
|
|
b40685050d | ||
|
|
0dab4f92ac | ||
|
|
0ddc3503a5 | ||
|
|
29ea3a7d67 | ||
|
|
823a1a42b4 | ||
|
|
0b66b1c473 | ||
|
|
934afbcf15 | ||
|
|
e0c924d2f4 | ||
|
|
a92b56491e | ||
|
|
6dcd9f4fef | ||
|
|
f5c36e520e | ||
|
|
fb2a3433e2 | ||
|
|
87ddddf41e | ||
|
|
45c8c1b873 | ||
|
|
68c9ada079 | ||
|
|
16a0473b10 | ||
|
|
4210d1ab05 | ||
|
|
bb7cf2c425 | ||
|
|
1a67379f57 | ||
|
|
9954e2b014 | ||
|
|
7d28dfdb15 | ||
|
|
fd9c64f416 | ||
|
|
7e766f4714 | ||
|
|
9dc307f7cc | ||
|
|
47c0330610 | ||
|
|
ded8a4086f | ||
|
|
f6dc35c9a1 | ||
|
|
408d15f73f | ||
|
|
a50f6795cc | ||
|
|
da10396f2b | ||
|
|
c2f28e3ef5 | ||
|
|
40ab22fded | ||
|
|
db0482ed1f | ||
|
|
9f8a877e8f | ||
|
|
331fe02c2a | ||
|
|
34ee2a17c2 | ||
|
|
8dbb61cdd4 | ||
|
|
cc86750dfb | ||
|
|
646f884e8f | ||
|
|
fbdf6c02c1 | ||
|
|
8ab757be8d | ||
|
|
b557df5840 | ||
|
|
8271bb7f40 | ||
|
|
0008c487e9 | ||
|
|
ae68204542 | ||
|
|
e68e081d7f | ||
|
|
006dc8f9d0 | ||
|
|
b9b30a23e6 | ||
|
|
8af3cfd031 | ||
|
|
fae5e78292 | ||
|
|
a13adf8f3a | ||
|
|
dc89adf190 | ||
|
|
3fc793287b | ||
|
|
7d4dd65acd | ||
|
|
d4a2a5fe80 | ||
|
|
d9a01999e8 | ||
|
|
6bb4060686 | ||
|
|
605246f93b | ||
|
|
5ef76dece1 | ||
|
|
d24d2e6fd0 | ||
|
|
9546f3ad1e | ||
|
|
6d2c00376e | ||
|
|
117136ada3 | ||
|
|
f130d4d8c7 | ||
|
|
a46eea77a6 | ||
|
|
0b42927d38 | ||
|
|
b979ca6e79 | ||
|
|
91ce4cc854 | ||
|
|
b58195e154 | ||
|
|
0d3f6a0812 | ||
|
|
22ce4605d8 | ||
|
|
474d461b1c | ||
|
|
339de90b8a | ||
|
|
048313aefa | ||
|
|
805f73582f | ||
|
|
a6ab998e5c | ||
|
|
92075c8426 | ||
|
|
751c520865 | ||
|
|
4fa2ef66ed | ||
|
|
aa735ef562 | ||
|
|
a927f9c458 | ||
|
|
82f03d0f1d | ||
|
|
40aca13fb0 | ||
|
|
e2832bac7c | ||
|
|
5f96916701 | ||
|
|
4a96d95bde | ||
|
|
dc3eb9081a | ||
|
|
2ddd920e4d | ||
|
|
e1e9940a04 | ||
|
|
f2817b7c49 | ||
|
|
fc79e82108 | ||
|
|
58052634fa | ||
|
|
c1374c5592 | ||
|
|
3a5d636345 | ||
|
|
642d802eee | ||
|
|
76ba624dce | ||
|
|
01d5f034b9 | ||
|
|
b7ced9f289 | ||
|
|
9c39e1ddc9 | ||
|
|
bbac1cb663 | ||
|
|
3bc23a60c7 | ||
|
|
c171ae4761 | ||
|
|
b530f4928e | ||
|
|
9fc6afd181 | ||
|
|
eb3d88afbf | ||
|
|
8ded0658d4 | ||
|
|
d032f81fd6 | ||
|
|
75becccf78 | ||
|
|
bfb84e2960 | ||
|
|
2f64c5a407 | ||
|
|
9e6765f4e2 | ||
|
|
d77c84b7c9 | ||
|
|
7172f787ac | ||
|
|
77f66d5357 | ||
|
|
4307815c0f | ||
|
|
b484f36a22 | ||
|
|
360a649d2a | ||
|
|
4bdd23291c | ||
|
|
1ee480122b | ||
|
|
382a5220e0 | ||
|
|
627155b13c | ||
|
|
0ca4dd1b32 | ||
|
|
383bcc4fc1 | ||
|
|
8028a0d75d | ||
|
|
31c1c5eb56 | ||
|
|
ca9295016a | ||
|
|
7b90fe9833 | ||
|
|
330786352c | ||
|
|
da309ae929 | ||
|
|
3877ae5328 | ||
|
|
9c99070d54 | ||
|
|
5751250a7f | ||
|
|
f97ff8124c | ||
|
|
7f37ed96d1 | ||
|
|
7ff7049018 | ||
|
|
ae895787c1 | ||
|
|
305f5c7d47 | ||
|
|
f0d3984376 | ||
|
|
b8f355ac4f | ||
|
|
29195a17a7 | ||
|
|
e90efd4fa9 | ||
|
|
70c158990d | ||
|
|
6e2efb4b55 | ||
|
|
18db1a78ab | ||
|
|
bd87d729fd | ||
|
|
a6f3ef591f | ||
|
|
aaed0a186c | ||
|
|
2b74bbca5c | ||
|
|
36758f61b4 | ||
|
|
f9de81f032 | ||
|
|
444aa53803 | ||
|
|
4e9f3e6e05 | ||
|
|
3fc4482d7c | ||
|
|
ad851375dd | ||
|
|
dd3e942057 | ||
|
|
dd4439c34a | ||
|
|
e81d850438 | ||
|
|
779453be26 | ||
|
|
867d341182 | ||
|
|
78b51f9a11 | ||
|
|
417772dd9c | ||
|
|
b86885512f | ||
|
|
65381cd614 | ||
|
|
e3f11d8fe1 | ||
|
|
093eb419a8 | ||
|
|
ad25490cf9 | ||
|
|
e94d3d80fa | ||
|
|
0e6b6d90bc | ||
|
|
ce1f9259bf | ||
|
|
8cec12b98b | ||
|
|
028b1b2d93 | ||
|
|
d8e5d0d4f1 | ||
|
|
0da9826821 | ||
|
|
b416e09e8b | ||
|
|
2ef58bd75d | ||
|
|
cac794208e | ||
|
|
a22aec8694 | ||
|
|
6f11e820b5 | ||
|
|
6a75b60b4f | ||
|
|
c494adbd87 | ||
|
|
3aa0f13869 | ||
|
|
e9af9efaf3 | ||
|
|
1ecc8066db | ||
|
|
525775fb36 | ||
|
|
3e4c608aca | ||
|
|
bd5028e331 | ||
|
|
4207614600 | ||
|
|
e1af950724 | ||
|
|
e91388a010 | ||
|
|
8648ddb528 | ||
|
|
feff5b6a57 | ||
|
|
32198910ce | ||
|
|
07f7147ff3 | ||
|
|
0e8a006adc | ||
|
|
d2f91d6f1e | ||
|
|
9ed5093474 | ||
|
|
a12dd019d3 | ||
|
|
e53bc92f41 | ||
|
|
597a8db2d9 | ||
|
|
0b0f639cd0 | ||
|
|
6b4ba6bca1 | ||
|
|
3cdd67b5b4 | ||
|
|
2b03bc7a53 | ||
|
|
f6447b8936 | ||
|
|
4069c33671 | ||
|
|
4dbc5581a5 | ||
|
|
3fc5c1c995 | ||
|
|
f123057ae5 | ||
|
|
5025ec818f | ||
|
|
65c6c8146d | ||
|
|
b93745f24a | ||
|
|
bbb59c9fe1 | ||
|
|
fb56f6571a | ||
|
|
082af1f82c | ||
|
|
f5b3a0cbad | ||
|
|
d602ccc224 | ||
|
|
ab54e7b7bd | ||
|
|
0137b07f66 | ||
|
|
ae95fbd1be | ||
|
|
625bd97d8b | ||
|
|
603fce7cd3 | ||
|
|
c759f460d5 | ||
|
|
e1c00667f7 | ||
|
|
46631fc4d4 | ||
|
|
f235a5d951 | ||
|
|
b0b711f181 | ||
|
|
f1b7102583 | ||
|
|
f076420e68 | ||
|
|
4ce0a7eaa1 | ||
|
|
270b730c21 | ||
|
|
14a6cc5999 | ||
|
|
0067873f60 | ||
|
|
fc16e1461f | ||
|
|
8f01916bb2 | ||
|
|
87d522f249 | ||
|
|
ead138b4cd | ||
|
|
ef39979548 | ||
|
|
ab6c8caf2f | ||
|
|
ba9206bded | ||
|
|
c5838cf955 | ||
|
|
0c41fc30e4 | ||
|
|
8270cfa39e | ||
|
|
06e356cab9 | ||
|
|
f24631dc48 | ||
|
|
ec936bcd06 | ||
|
|
6c7975e400 | ||
|
|
8acef7ab66 | ||
|
|
4ddfd39b0d | ||
|
|
3ba37cc24c | ||
|
|
da080118b0 | ||
|
|
e8897ea80a | ||
|
|
9d26f582fa | ||
|
|
0a2ad2176c | ||
|
|
7c36439836 | ||
|
|
a1e3afed06 | ||
|
|
be5b1bbae3 | ||
|
|
52de23bf95 | ||
|
|
1d16fe46cd | ||
|
|
cbecda13f9 | ||
|
|
24649ec103 | ||
|
|
b9f62a8399 | ||
|
|
689d45e095 | ||
|
|
33f7b3f2bb | ||
|
|
2cce072393 | ||
|
|
d64bdec2f9 | ||
|
|
5adacdecad | ||
|
|
f2552d3f3b | ||
|
|
b435e01199 | ||
|
|
cd576df6e4 | ||
|
|
08e54510ed | ||
|
|
a95606cee9 | ||
|
|
c990476d99 | ||
|
|
c719d24f3a | ||
|
|
f746368369 | ||
|
|
164b6ff6c1 | ||
|
|
3dbe9e6a3e | ||
|
|
31f8af8409 | ||
|
|
9317a93152 |
40
.github/dependabot.yml
vendored
40
.github/dependabot.yml
vendored
@@ -4,3 +4,43 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
3
.github/workflows/code-check.yml
vendored
3
.github/workflows/code-check.yml
vendored
@@ -77,6 +77,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm --filter=shadcn build
|
||||
|
||||
- run: pnpm format:check
|
||||
|
||||
tsc:
|
||||
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -42,7 +42,4 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build --filter=shadcn
|
||||
|
||||
- run: pnpm test
|
||||
|
||||
43
.github/workflows/validate-registries.yml
vendored
43
.github/workflows/validate-registries.yml
vendored
@@ -4,13 +4,53 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
|
||||
jobs:
|
||||
check-registry-sync:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
name: Check registry sync
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check changed files
|
||||
id: changed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only)
|
||||
|
||||
DIRECTORY_CHANGED=false
|
||||
REGISTRIES_CHANGED=false
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q "^apps/v4/registry/directory.json$"; then
|
||||
DIRECTORY_CHANGED=true
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q "^apps/v4/public/r/registries.json$"; then
|
||||
REGISTRIES_CHANGED=true
|
||||
fi
|
||||
|
||||
echo "directory_changed=$DIRECTORY_CHANGED" >> $GITHUB_OUTPUT
|
||||
echo "registries_changed=$REGISTRIES_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Flag missing registries.json update
|
||||
if: steps.changed.outputs.directory_changed == 'true' && steps.changed.outputs.registries_changed == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "registries: invalid"
|
||||
gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "can you run \`pnpm registry:build\` and commit the json files please?"
|
||||
exit 1
|
||||
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm validate:registries
|
||||
@@ -47,8 +87,5 @@ jobs:
|
||||
- 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
@@ -41,3 +41,5 @@ tsconfig.tsbuildinfo
|
||||
.vscode
|
||||
|
||||
.notes
|
||||
.playwright-mcp
|
||||
shadcn-workspace
|
||||
|
||||
@@ -6,7 +6,7 @@ A set of beautifully designed components that you can customize, extend, and bui
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit http://ui.shadcn.com/docs to view the documentation.
|
||||
Visit https://ui.shadcn.com/docs to view the documentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -14,4 +14,4 @@ Please read the [contributing guide](/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md).
|
||||
Licensed under the [MIT license](./LICENSE.md).
|
||||
|
||||
@@ -93,7 +93,7 @@ export function AppearanceSettings() {
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 !w-14 font-mono"
|
||||
className="h-7 w-14! font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -21,7 +21,7 @@ export function ButtonGroupPopover() {
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
|
||||
<PopoverContent align="end" className="gap-0 rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-md rounded-lg border p-6">
|
||||
<div className="w-full max-w-md rounded-xl border p-6">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
|
||||
@@ -46,7 +46,7 @@ export function FieldHear() {
|
||||
<FieldLabel
|
||||
htmlFor={option.value}
|
||||
key={option.value}
|
||||
className="!w-fit"
|
||||
className="w-fit!"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
|
||||
@@ -19,7 +19,7 @@ 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="mx-auto grid gap-8 py-1 theme-container md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<FieldDemo />
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function InputGroupButtonExample() {
|
||||
Input Secure
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-19" className="!pl-0.5" />
|
||||
<InputGroupInput id="input-secure-19" className="pl-0.5!" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
@@ -46,7 +46,7 @@ export function InputGroupButtonExample() {
|
||||
<p>You should not enter any sensitive information on this site.</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="text-muted-foreground !pl-1">
|
||||
<InputGroupAddon className="pl-1! text-muted-foreground">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
|
||||
@@ -32,7 +32,7 @@ export function InputGroupDemo() {
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="example.com" className="!pl-1" />
|
||||
<InputGroupInput placeholder="example.com" className="pl-1!" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -73,7 +73,7 @@ export function InputGroupDemo() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">52% used</InputGroupText>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
@@ -87,7 +87,7 @@ export function InputGroupDemo() {
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="@shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function ItemAvatar() {
|
||||
</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">
|
||||
<div className="flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background *:data-[slot=avatar]:grayscale">
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
|
||||
@@ -190,12 +190,12 @@ export function NotionPromptForm() {
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroup className="rounded-xl">
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<InputGroupAddon align="block-start" className="pt-3">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
@@ -209,7 +209,7 @@ export function NotionPromptForm() {
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
className="transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
@@ -235,6 +235,7 @@ export function NotionPromptForm() {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
className="rounded-lg"
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
@@ -246,7 +247,7 @@ export function NotionPromptForm() {
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
<div className="-m-1.5 no-scrollbar flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
@@ -261,7 +262,7 @@ export function NotionPromptForm() {
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full !pl-2"
|
||||
className="rounded-full pl-2!"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
@@ -301,9 +302,13 @@ export function NotionPromptForm() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="min-w-48"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
@@ -421,7 +426,7 @@ export function NotionPromptForm() {
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function IndexPage() {
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm" className="h-[31px] rounded-lg">
|
||||
<Link href="/docs/installation">Get Started</Link>
|
||||
<Link href="/create">New Project</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost" className="rounded-lg">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
@@ -64,12 +64,12 @@ export default function IndexPage() {
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageNav className="hidden md:flex">
|
||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper section-soft flex-1 pb-6">
|
||||
<div className="container-wrapper flex-1 section-soft pb-6">
|
||||
<div className="container overflow-hidden">
|
||||
<section className="border-border/50 -mx-4 w-[160vw] overflow-hidden rounded-lg border md:hidden md:w-[150vw]">
|
||||
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
|
||||
<Image
|
||||
src="/r/styles/new-york-v4/dashboard-01-light.png"
|
||||
width={1400}
|
||||
@@ -87,7 +87,7 @@ export default function IndexPage() {
|
||||
priority
|
||||
/>
|
||||
</section>
|
||||
<section className="theme-container hidden md:block">
|
||||
<section className="hidden theme-container md:block">
|
||||
<RootComponents />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function BlocksLayout({
|
||||
<Link href="/blocks/sidebar">Browse all blocks</Link>
|
||||
</Button>
|
||||
</PageNav>
|
||||
<div className="container-wrapper section-soft flex-1 md:py-12">
|
||||
<div className="container-wrapper flex-1 section-soft md:py-12">
|
||||
<div className="container">{children}</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function ColorsLayout({
|
||||
<div className="hidden">
|
||||
<div className="container-wrapper">
|
||||
<div className="container flex items-center justify-between gap-8 py-4">
|
||||
<ColorsNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ColorsNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,7 +134,7 @@ export default async function Page(props: {
|
||||
</div>
|
||||
</div>
|
||||
{doc.description && (
|
||||
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
<p className="text-[1.05rem] text-muted-foreground sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
{doc.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function ChangelogPage() {
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
<p className="text-[1.05rem] text-muted-foreground sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
Latest updates and announcements.
|
||||
</p>
|
||||
</div>
|
||||
@@ -91,9 +91,9 @@ export default function ChangelogPage() {
|
||||
<Link
|
||||
key={page.url}
|
||||
href={page.url}
|
||||
className="bg-surface text-surface-foreground hover:bg-surface/80 flex w-full flex-col rounded-xl px-4 py-3 transition-colors"
|
||||
className="flex w-full flex-col rounded-xl bg-surface px-4 py-3 text-surface-foreground transition-colors hover:bg-surface/80"
|
||||
>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{date}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
@@ -110,7 +110,7 @@ export default function ChangelogPage() {
|
||||
<div className="h-(--top-spacing) shrink-0"></div>
|
||||
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
|
||||
<div className="flex flex-col gap-2 p-4 pt-0 text-sm">
|
||||
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
|
||||
<p className="sticky top-0 h-6 bg-background text-xs font-medium text-muted-foreground">
|
||||
On This Page
|
||||
</p>
|
||||
{latestPages.map((page) => {
|
||||
@@ -119,7 +119,7 @@ export default function ChangelogPage() {
|
||||
<Link
|
||||
key={page.url}
|
||||
href={page.url}
|
||||
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
|
||||
className="text-[0.8rem] text-muted-foreground no-underline transition-colors hover:text-foreground"
|
||||
>
|
||||
{data.title}
|
||||
</Link>
|
||||
@@ -128,7 +128,7 @@ export default function ChangelogPage() {
|
||||
{olderPages.length > 0 && (
|
||||
<a
|
||||
href="#more-updates"
|
||||
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
|
||||
className="text-[0.8rem] text-muted-foreground no-underline transition-colors hover:text-foreground"
|
||||
>
|
||||
More Updates
|
||||
</a>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function DocsLayout({
|
||||
return (
|
||||
<div className="container-wrapper flex flex-1 flex-col px-2">
|
||||
<SidebarProvider
|
||||
className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--top-spacing:calc(var(--spacing)*4)]"
|
||||
className="min-h-min flex-1 items-start px-0 [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--top-spacing:calc(var(--spacing)*4)] 3xl:fixed:container 3xl:fixed:px-3"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
|
||||
@@ -43,8 +43,8 @@ export default function AuthenticationPage() {
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<div className="text-primary relative hidden h-full flex-col p-10 lg:flex dark:border-r">
|
||||
<div className="bg-primary/5 absolute inset-0" />
|
||||
<div className="relative hidden h-full flex-col p-10 text-primary lg:flex dark:border-r">
|
||||
<div className="absolute inset-0 bg-primary/5" />
|
||||
<div className="relative z-20 flex items-center text-lg font-medium">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -74,7 +74,7 @@ export default function AuthenticationPage() {
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Create an account
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enter your email below to create your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -159,10 +159,10 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||
className="data-[slot=sidebar-menu-button]:p-1.5!"
|
||||
>
|
||||
<Link href="#">
|
||||
<IconInnerShadowTop className="!size-5" />
|
||||
<IconInnerShadowTop className="size-5!" />
|
||||
<span className="text-base font-semibold">Acme Inc.</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
@@ -174,7 +174,7 @@ export function ChartAreaInteractive() {
|
||||
value={timeRange}
|
||||
onValueChange={setTimeRange}
|
||||
variant="outline"
|
||||
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
|
||||
className="hidden *:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex"
|
||||
>
|
||||
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
|
||||
|
||||
@@ -128,9 +128,9 @@ function DragHandle({ id }: { id: number }) {
|
||||
{...listeners}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground size-7 hover:bg-transparent"
|
||||
className="size-7 text-muted-foreground hover:bg-transparent"
|
||||
>
|
||||
<IconGripVertical className="text-muted-foreground size-3" />
|
||||
<IconGripVertical className="size-3 text-muted-foreground" />
|
||||
<span className="sr-only">Drag to reorder</span>
|
||||
</Button>
|
||||
)
|
||||
@@ -181,7 +181,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
header: "Section Type",
|
||||
cell: ({ row }) => (
|
||||
<div className="w-32">
|
||||
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
{row.original.type}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -191,7 +191,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
{row.original.status === "Done" ? (
|
||||
<IconCircleCheckFilled className="fill-green-500 dark:fill-green-400" />
|
||||
) : (
|
||||
@@ -219,7 +219,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
Target
|
||||
</Label>
|
||||
<Input
|
||||
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
defaultValue={row.original.target}
|
||||
id={`${row.original.id}-target`}
|
||||
/>
|
||||
@@ -244,7 +244,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
Limit
|
||||
</Label>
|
||||
<Input
|
||||
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
defaultValue={row.original.limit}
|
||||
id={`${row.original.id}-limit`}
|
||||
/>
|
||||
@@ -292,7 +292,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="data-[state=open]:bg-muted text-muted-foreground flex size-8"
|
||||
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
|
||||
size="icon"
|
||||
>
|
||||
<IconDotsVertical />
|
||||
@@ -425,7 +425,7 @@ export function DataTable({
|
||||
<SelectItem value="focus-documents">Focus Documents</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||
<TabsList className="hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||
<TabsTrigger value="outline">Outline</TabsTrigger>
|
||||
<TabsTrigger value="past-performance">
|
||||
Past Performance <Badge variant="secondary">3</Badge>
|
||||
@@ -488,7 +488,7 @@ export function DataTable({
|
||||
id={sortableId}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="bg-muted sticky top-0 z-10">
|
||||
<TableHeader className="sticky top-0 z-10 bg-muted">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
@@ -531,7 +531,7 @@ export function DataTable({
|
||||
</DndContext>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
||||
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
@@ -653,7 +653,7 @@ function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
||||
return (
|
||||
<Drawer direction={isMobile ? "bottom" : "right"}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="link" className="text-foreground w-fit px-0 text-left">
|
||||
<Button variant="link" className="w-fit px-0 text-left text-foreground">
|
||||
{item.header}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
|
||||
@@ -52,7 +52,7 @@ export function NavDocuments({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="data-[state=open]:bg-accent rounded-sm"
|
||||
className="rounded-sm data-[state=open]:bg-accent"
|
||||
>
|
||||
<IconDots />
|
||||
<span className="sr-only">More</span>
|
||||
|
||||
@@ -55,7 +55,7 @@ export function NavUser({
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
@@ -76,7 +76,7 @@ export function NavUser({
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="bg-background/90 sticky top-0 z-10 flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<header className="sticky top-0 z-10 flex h-(--header-height) shrink-0 items-center gap-2 border-b bg-background/90 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<h1 className="text-base font-medium">Documents</h1>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
|
||||
@@ -65,12 +65,12 @@ export default function ExamplesLayout({
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageNav id="examples" className="hidden md:flex">
|
||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
<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">
|
||||
<div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding has-[[data-slot=rtl-components]]:overflow-visible has-[[data-slot=rtl-components]]:border-0 has-[[data-slot=rtl-components]]:bg-transparent md:flex-1 xl:rounded-xl">
|
||||
<div className="container-wrapper flex flex-1 flex-col section-soft pb-6">
|
||||
<div className="container flex flex-1 scroll-mt-20 flex-col theme-container">
|
||||
<div className="flex flex-col overflow-hidden rounded-lg border bg-background bg-clip-padding has-[[data-slot=rtl-components]]:overflow-visible has-[[data-slot=rtl-components]]:border-0 has-[[data-slot=rtl-components]]:bg-transparent md:flex-1 xl:rounded-xl">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function CodeViewer() {
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your API Key can be found here. You should use environment
|
||||
variables or a secret management tool to expose your key to your
|
||||
applications.
|
||||
|
||||
@@ -27,7 +27,7 @@ export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) {
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="maxlength">Maximum Length</Label>
|
||||
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<h4 className="leading-none font-medium">{peekedModel.name}</h4>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{peekedModel.description}
|
||||
</div>
|
||||
{peekedModel.strengths ? (
|
||||
@@ -86,7 +86,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
|
||||
<h5 className="text-sm leading-none font-medium">
|
||||
Strengths
|
||||
</h5>
|
||||
<ul className="text-muted-foreground text-sm">
|
||||
<ul className="text-sm text-muted-foreground">
|
||||
{peekedModel.strengths}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@ export function PresetActions() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-6">
|
||||
<h4 className="text-muted-foreground text-sm">
|
||||
<h4 className="text-sm text-muted-foreground">
|
||||
Playground Warnings
|
||||
</h4>
|
||||
<div className="flex items-start justify-between gap-4 pt-3">
|
||||
@@ -79,7 +79,7 @@ export function PresetActions() {
|
||||
<span className="font-semibold">
|
||||
Show a warning when content is flagged
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
A warning will be shown when sexual, hateful, violent or
|
||||
self-harm content is detected.
|
||||
</span>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function PresetShare() {
|
||||
<PopoverContent align="end" className="flex w-[520px] flex-col gap-4">
|
||||
<div className="flex flex-col gap-1 text-center sm:text-left">
|
||||
<h3 className="text-lg font-semibold">Share preset</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Anyone who has this link and an OpenAI account will be able to view
|
||||
this.
|
||||
</p>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function TemperatureSelector({
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="temperature">Temperature</Label>
|
||||
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function TopPSelector({ defaultValue }: TopPSelectorProps) {
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="top-p">Top P</Label>
|
||||
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -278,7 +278,7 @@ export default function PlaygroundPage() {
|
||||
placeholder="We're writing to [inset]. Congrats from OpenAI!"
|
||||
className="h-full min-h-[300px] p-4 lg:min-h-[700px] xl:min-h-[700px]"
|
||||
/>
|
||||
<div className="bg-muted rounded-md border"></div>
|
||||
<div className="rounded-md border bg-muted"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button>Submit</Button>
|
||||
@@ -312,7 +312,7 @@ export default function PlaygroundPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-muted min-h-[400px] rounded-md border lg:min-h-[700px]" />
|
||||
<div className="min-h-[400px] rounded-md border bg-muted lg:min-h-[700px]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button>Submit</Button>
|
||||
|
||||
@@ -128,7 +128,7 @@ export function AppearanceSettings() {
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 !w-14 font-mono"
|
||||
className="h-7 w-14! font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -63,7 +63,7 @@ export function FieldHear() {
|
||||
<FieldLabel
|
||||
htmlFor={`rtl-${option.value}`}
|
||||
key={option.value}
|
||||
className="!w-fit"
|
||||
className="w-fit!"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
|
||||
@@ -50,7 +50,7 @@ export function InputGroupButtonExample() {
|
||||
{t.inputLabel}
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-rtl" className="!pr-0.5" />
|
||||
<InputGroupInput id="input-secure-rtl" className="pr-0.5!" />
|
||||
<InputGroupAddon>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
|
||||
@@ -116,7 +116,7 @@ export function InputGroupDemo() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ms-auto">{t.used}</InputGroupText>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
@@ -130,7 +130,7 @@ export function InputGroupDemo() {
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
|
||||
@@ -288,7 +288,7 @@ export function NotionPromptForm() {
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
|
||||
<div className="-m-1.5 no-scrollbar flex gap-1 overflow-y-auto p-1.5">
|
||||
{mentions.map((mention) => {
|
||||
const item = SAMPLE_DATA.mentionable.find(
|
||||
(item) => item.title === mention
|
||||
@@ -303,7 +303,7 @@ export function NotionPromptForm() {
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full !pr-2"
|
||||
className="rounded-full pr-2!"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
@@ -352,7 +352,7 @@ export function NotionPromptForm() {
|
||||
dir={t.dir}
|
||||
>
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
{t.selectAgentMode}
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
@@ -493,7 +493,7 @@ export function NotionPromptForm() {
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> {t.connectApps}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
{t.searchSourcesNote}
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const columns: ColumnDef<Task>[] = [
|
||||
return (
|
||||
<div className="flex w-[100px] items-center gap-2">
|
||||
{status.icon && (
|
||||
<status.icon className="text-muted-foreground size-4" />
|
||||
<status.icon className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
<span>{status.label}</span>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ export const columns: ColumnDef<Task>[] = [
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{priority.icon && (
|
||||
<priority.icon className="text-muted-foreground size-4" />
|
||||
<priority.icon className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
<span>{priority.label}</span>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="data-[state=open]:bg-accent -ml-3 h-8"
|
||||
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
>
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === "desc" ? (
|
||||
|
||||
@@ -107,18 +107,18 @@ export function DataTableFacetedFilter<TData, TValue>({
|
||||
className={cn(
|
||||
"flex size-4 items-center justify-center rounded-[4px] border",
|
||||
isSelected
|
||||
? "bg-primary border-primary text-primary-foreground"
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-input [&_svg]:invisible"
|
||||
)}
|
||||
>
|
||||
<Check className="text-primary-foreground size-3.5" />
|
||||
<Check className="size-3.5 text-primary-foreground" />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="text-muted-foreground size-4" />
|
||||
<option.icon className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
{facets?.get(option.value) && (
|
||||
<span className="text-muted-foreground ml-auto flex size-4 items-center justify-center font-mono text-xs">
|
||||
<span className="ml-auto flex size-4 items-center justify-center font-mono text-xs text-muted-foreground">
|
||||
{facets.get(option.value)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function DataTablePagination<TData>({
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="text-muted-foreground flex-1 text-sm">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function DataTableRowActions<TData>({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="data-[state=open]:bg-muted size-8"
|
||||
className="size-8 data-[state=open]:bg-muted"
|
||||
>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">Open menu</span>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function UserNav() {
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm leading-none font-medium">shadcn</p>
|
||||
<p className="text-muted-foreground text-xs leading-none">
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
m@example.com
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="layout"
|
||||
className="bg-background relative z-10 flex min-h-svh flex-col"
|
||||
className="relative z-10 flex min-h-svh flex-col bg-background"
|
||||
>
|
||||
<SiteHeader />
|
||||
<main className="flex flex-1 flex-col">{children}</main>
|
||||
|
||||
@@ -12,8 +12,8 @@ export default function ThemesPage() {
|
||||
<ThemeCustomizer />
|
||||
</div>
|
||||
</div>
|
||||
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
|
||||
<div className="theme-container container flex flex-1 flex-col">
|
||||
<div className="container-wrapper flex flex-1 flex-col section-soft pb-6">
|
||||
<div className="container flex flex-1 flex-col theme-container">
|
||||
<CardsDemo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,23 +26,23 @@ export function MenuAccentPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<div className="group/picker relative pr-3 md:pr-0">
|
||||
<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">
|
||||
<div className="text-xs text-muted-foreground">Menu Accent</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{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">
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="text-foreground"
|
||||
className="size-4 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"
|
||||
@@ -51,7 +51,7 @@ export function MenuAccentPicker({
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-accent={currentAccent?.value}
|
||||
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
|
||||
className="fill-muted-foreground/30 data-[accent=bold]:fill-foreground"
|
||||
></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"
|
||||
@@ -60,7 +60,7 @@ export function MenuAccentPicker({
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-accent={currentAccent?.value}
|
||||
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
|
||||
className="fill-muted-foreground/30 data-[accent=bold]:fill-foreground"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -78,7 +78,11 @@ export function MenuAccentPicker({
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_ACCENTS.map((accent) => (
|
||||
<PickerRadioItem key={accent.value} value={accent.value}>
|
||||
<PickerRadioItem
|
||||
key={accent.value}
|
||||
value={accent.value}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{accent.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
@@ -88,7 +92,7 @@ export function MenuAccentPicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuAccent"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
88
apps/v4/app/(create)/components/action-menu.tsx
Normal file
88
apps/v4/app/(create)/components/action-menu.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
|
||||
import Script from "next/script"
|
||||
import {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/examples/base/ui/command"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { useActionMenu } from "@/app/(create)/hooks/use-action-menu"
|
||||
|
||||
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
||||
|
||||
export function ActionMenu({
|
||||
itemsByBase,
|
||||
}: {
|
||||
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
|
||||
}) {
|
||||
const {
|
||||
activeRegistryName,
|
||||
getCommandValue,
|
||||
groups,
|
||||
handleSelect,
|
||||
open,
|
||||
setOpen,
|
||||
} = useActionMenu(itemsByBase)
|
||||
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen} className="animate-none!">
|
||||
<Command loop>
|
||||
<CommandInput placeholder="Search" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No items found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{groups.map((group) =>
|
||||
group.items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
value={getCommandValue(item)}
|
||||
data-checked={activeRegistryName === item.registryName}
|
||||
className="px-2"
|
||||
onSelect={() => {
|
||||
handleSelect(item.registryName)
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</CommandItem>
|
||||
))
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function ActionMenuScript() {
|
||||
return (
|
||||
<Script
|
||||
id="design-system-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
// Forward Cmd/Ctrl + K (and P) to parent
|
||||
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
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
"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"
|
||||
@@ -10,10 +9,8 @@ import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerItem,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
@@ -25,7 +22,6 @@ export function BaseColorPicker({
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
@@ -39,22 +35,20 @@ export function BaseColorPicker({
|
||||
<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">
|
||||
<div className="text-xs text-muted-foreground">Base Color</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{currentBaseColor?.title}
|
||||
</div>
|
||||
</div>
|
||||
{mounted && resolvedTheme && (
|
||||
{mounted && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
currentBaseColor?.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["muted-foreground"],
|
||||
currentBaseColor?.cssVars?.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"
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none md:right-2.5"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
@@ -66,59 +60,26 @@ export function BaseColorPicker({
|
||||
<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
|
||||
key={baseColor.name}
|
||||
value={baseColor.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{baseColor.title}
|
||||
</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"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -40,49 +40,47 @@ export function BasePicker({
|
||||
)
|
||||
|
||||
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 className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Base</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{currentBase?.title}
|
||||
</div>
|
||||
</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}
|
||||
{currentBase?.meta?.logo && (
|
||||
<div
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 text-foreground select-none md:right-2.5 *:[svg]:size-4 *:[svg]:text-foreground!"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentBase.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<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>
|
||||
<PickerRadioGroup
|
||||
value={currentBase?.name}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASES.map((base) => (
|
||||
<PickerRadioItem
|
||||
key={base.name}
|
||||
value={base.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{base.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
43
apps/v4/app/(create)/components/copy-preset.tsx
Normal file
43
apps/v4/app/(create)/components/copy-preset.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||
|
||||
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
||||
const presetCode = usePresetCode()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
const timer = setTimeout(() => setHasCopied(false), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [hasCopied])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
copyToClipboardWithMeta(`--preset ${presetCode}`, {
|
||||
name: "copy_preset_command",
|
||||
properties: {
|
||||
preset: presetCode,
|
||||
},
|
||||
})
|
||||
setHasCopied(true)
|
||||
}, [presetCode])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span>{hasCopied ? "Copied" : `--preset ${presetCode}`}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Settings05Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/examples/base/ui/card"
|
||||
import { FieldGroup } from "@/examples/base/ui/field"
|
||||
import { Separator } from "@/examples/base/ui/separator"
|
||||
import { CardTitle } from "@/examples/radix/ui/card"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config"
|
||||
import { FieldGroup } from "@/registry/new-york-v4/ui/field"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { getThemesForBaseColor, STYLES } from "@/registry/config"
|
||||
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
|
||||
import { ActionMenu } from "@/app/(create)/components/action-menu"
|
||||
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
|
||||
import { BasePicker } from "@/app/(create)/components/base-picker"
|
||||
import { CopyPreset } from "@/app/(create)/components/copy-preset"
|
||||
import { FontPicker } from "@/app/(create)/components/font-picker"
|
||||
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
|
||||
import { MainMenu } from "@/app/(create)/components/main-menu"
|
||||
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
|
||||
import { PresetPicker } from "@/app/(create)/components/preset-picker"
|
||||
import { ProjectForm } from "@/app/(create)/components/project-form"
|
||||
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
|
||||
import { RandomButton } from "@/app/(create)/components/random-button"
|
||||
import { ResetButton } from "@/app/(create)/components/reset-button"
|
||||
import { ResetDialog } from "@/app/(create)/components/reset-button"
|
||||
import { StylePicker } from "@/app/(create)/components/style-picker"
|
||||
import { ThemePicker } from "@/app/(create)/components/theme-picker"
|
||||
import { V0Button } from "@/app/(create)/components/v0-button"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function Customizer() {
|
||||
export function Customizer({
|
||||
itemsByBase,
|
||||
}: {
|
||||
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
|
||||
}) {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isMobile = useIsMobile()
|
||||
const anchorRef = React.useRef<HTMLDivElement | null>(null)
|
||||
@@ -34,32 +48,16 @@ export function Customizer() {
|
||||
)
|
||||
|
||||
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"
|
||||
<Card
|
||||
className="dark top-24 right-12 isolate z-10 max-h-full min-h-0 w-full self-start rounded-2xl bg-card/90 shadow-xl backdrop-blur-xl md:w-(--customizer-width)"
|
||||
ref={anchorRef}
|
||||
size="sm"
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
<CardHeader className="hidden items-center justify-between gap-2 border-b group-data-reversed/layout:flex-row-reverse md:flex">
|
||||
<MainMenu />
|
||||
</CardHeader>
|
||||
<CardContent className="no-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden md:overflow-y-auto">
|
||||
<FieldGroup className="flex-row gap-2.5 py-px md:flex-col md:gap-3.25">
|
||||
<BasePicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<StylePicker
|
||||
styles={STYLES}
|
||||
@@ -77,12 +75,14 @@ export function Customizer() {
|
||||
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<div className="mt-auto hidden w-full flex-col items-center gap-0 md:flex">
|
||||
<RandomButton />
|
||||
<ResetButton />
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex min-w-0 gap-2 md:flex-col md:**:[button,a]:w-full">
|
||||
<CopyPreset className="flex-1 md:flex-none" />
|
||||
<RandomButton className="flex-1 md:flex-none" />
|
||||
<ActionMenu itemsByBase={itemsByBase} />
|
||||
<ResetDialog />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,25 +6,53 @@ import {
|
||||
buildRegistryTheme,
|
||||
DEFAULT_CONFIG,
|
||||
type DesignSystemConfig,
|
||||
type RadiusValue,
|
||||
} 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"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function DesignSystemProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [
|
||||
{ style, theme, font, baseColor, menuAccent, menuColor, radius },
|
||||
setSearchParams,
|
||||
] = useDesignSystemSearchParams({
|
||||
const [searchParams, 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 [liveSearchParams, setLiveSearchParams] = React.useState(searchParams)
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
const { style, theme, font, baseColor, menuAccent, menuColor, radius } =
|
||||
liveSearchParams
|
||||
const effectiveRadius = style === "lyra" ? "none" : radius
|
||||
|
||||
React.useEffect(() => {
|
||||
setLiveSearchParams(searchParams)
|
||||
}, [searchParams])
|
||||
|
||||
const handleDesignSystemMessage = React.useCallback(
|
||||
(nextParams: DesignSystemSearchParams) => {
|
||||
setLiveSearchParams(nextParams)
|
||||
setSearchParams(nextParams)
|
||||
},
|
||||
[setSearchParams]
|
||||
)
|
||||
|
||||
useIframeMessageListener("design-system-params", handleDesignSystemMessage)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (style === "lyra" && radius !== "none") {
|
||||
setLiveSearchParams((prev) => ({
|
||||
...prev,
|
||||
radius: "none",
|
||||
}))
|
||||
setSearchParams({ radius: "none" as RadiusValue })
|
||||
}
|
||||
}, [style, radius, setSearchParams])
|
||||
|
||||
// Use useLayoutEffect for synchronous style updates to prevent flash.
|
||||
React.useLayoutEffect(() => {
|
||||
@@ -34,23 +62,20 @@ export function DesignSystemProvider({
|
||||
|
||||
const body = document.body
|
||||
|
||||
// Update style class in place (remove old, add new).
|
||||
body.classList.forEach((className) => {
|
||||
if (className.startsWith("style-")) {
|
||||
// Iterate over a snapshot so removals do not affect traversal.
|
||||
Array.from(body.classList).forEach((className) => {
|
||||
if (
|
||||
className.startsWith("style-") ||
|
||||
className.startsWith("base-color-")
|
||||
) {
|
||||
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}`)
|
||||
body.classList.add(`style-${style}`, `base-color-${baseColor}`)
|
||||
|
||||
// Update font.
|
||||
// Always set --font-sans for the preview so the selected font is visible.
|
||||
// The font type (sans/serif/mono) is metadata for the CLI updater.
|
||||
const selectedFont = FONTS.find((f) => f.value === font)
|
||||
if (selectedFont) {
|
||||
const fontFamily = selectedFont.font.style.fontFamily
|
||||
@@ -61,7 +86,7 @@ export function DesignSystemProvider({
|
||||
}, [style, theme, font, baseColor])
|
||||
|
||||
const registryTheme = React.useMemo(() => {
|
||||
if (!baseColor || !theme || !menuAccent || !radius) {
|
||||
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -70,11 +95,11 @@ export function DesignSystemProvider({
|
||||
baseColor,
|
||||
theme,
|
||||
menuAccent,
|
||||
radius,
|
||||
radius: effectiveRadius,
|
||||
}
|
||||
|
||||
return buildRegistryTheme(config)
|
||||
}, [baseColor, theme, menuAccent, radius])
|
||||
}, [baseColor, theme, menuAccent, effectiveRadius])
|
||||
|
||||
// Use useLayoutEffect for synchronous CSS var updates.
|
||||
React.useLayoutEffect(() => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerLabel,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
@@ -37,19 +38,38 @@ export function FontPicker({
|
||||
() => fonts.find((font) => font.value === params.font),
|
||||
[fonts, params.font]
|
||||
)
|
||||
const groupedFonts = React.useMemo(() => {
|
||||
const groups = new Map<Font["type"], Font[]>()
|
||||
|
||||
for (const font of fonts) {
|
||||
const existing = groups.get(font.type)
|
||||
if (existing) {
|
||||
existing.push(font)
|
||||
continue
|
||||
}
|
||||
|
||||
groups.set(font.type, [font])
|
||||
}
|
||||
|
||||
return Array.from(groups.entries()).map(([type, items]) => ({
|
||||
type,
|
||||
label: `${type.charAt(0).toUpperCase()}${type.slice(1)}`,
|
||||
items,
|
||||
}))
|
||||
}, [fonts])
|
||||
|
||||
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">
|
||||
<div className="text-xs text-muted-foreground">Font</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{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"
|
||||
className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5"
|
||||
style={{ fontFamily: currentFont?.font.style.fontFamily }}
|
||||
>
|
||||
Aa
|
||||
@@ -59,7 +79,7 @@ export function FontPicker({
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-96 md:w-72"
|
||||
className="max-h-96"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentFont?.value}
|
||||
@@ -67,36 +87,26 @@ export function FontPicker({
|
||||
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>
|
||||
{groupedFonts.map((group) => (
|
||||
<PickerGroup key={group.type}>
|
||||
<PickerLabel>{group.label}</PickerLabel>
|
||||
{group.items.map((font) => (
|
||||
<PickerRadioItem
|
||||
key={font.value}
|
||||
value={font.value}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{font.name}
|
||||
</PickerRadioItem>
|
||||
{index < fonts.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
))}
|
||||
</PickerGroup>
|
||||
))}
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="font"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
78
apps/v4/app/(create)/components/history-buttons.tsx
Normal file
78
apps/v4/app/(create)/components/history-buttons.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import Script from "next/script"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { useHistory } from "@/app/(create)/hooks/use-history"
|
||||
|
||||
export const UNDO_FORWARD_TYPE = "undo-forward"
|
||||
export const REDO_FORWARD_TYPE = "redo-forward"
|
||||
|
||||
export function HistoryButtons() {
|
||||
const { canGoBack, canGoForward, goBack, goForward } = useHistory()
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Undo"
|
||||
disabled={!canGoBack}
|
||||
onClick={goBack}
|
||||
>
|
||||
<HugeiconsIcon icon={Undo02Icon} />
|
||||
<span className="sr-only">Undo</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Redo"
|
||||
disabled={!canGoForward}
|
||||
onClick={goForward}
|
||||
>
|
||||
<HugeiconsIcon icon={Redo02Icon} />
|
||||
<span className="sr-only">Redo</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function HistoryScript() {
|
||||
return (
|
||||
<Script
|
||||
id="history-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (!e.metaKey && !e.ctrlKey) return;
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
var key = e.key.toLowerCase();
|
||||
if ((key === 'z' && e.shiftKey) || (key === 'y' && e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type: '${REDO_FORWARD_TYPE}' }, '*');
|
||||
}
|
||||
} else if (key === 'z') {
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type: '${UNDO_FORWARD_TYPE}' }, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
"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 { iconLibraries, type IconLibraryName } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
@@ -16,124 +10,10 @@ import {
|
||||
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 IconRemixicon = lazy(() =>
|
||||
import("@/registry/icons/icon-remixicon").then((mod) => ({
|
||||
default: mod.IconRemixicon,
|
||||
}))
|
||||
)
|
||||
|
||||
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",
|
||||
],
|
||||
remixicon: [
|
||||
"RiFileCopyLine",
|
||||
"RiErrorWarningLine",
|
||||
"RiDeleteBinLine",
|
||||
"RiShareLine",
|
||||
"RiShoppingBagLine",
|
||||
"RiMoreLine",
|
||||
"RiLoaderLine",
|
||||
"RiAddLine",
|
||||
"RiSubtractLine",
|
||||
"RiArrowLeftLine",
|
||||
"RiArrowRightLine",
|
||||
"RiCheckLine",
|
||||
"RiArrowDownSLine",
|
||||
"RiArrowRightSLine",
|
||||
],
|
||||
}
|
||||
|
||||
const logos = {
|
||||
lucide: (
|
||||
<svg
|
||||
@@ -248,12 +128,12 @@ export function IconLibraryPicker({
|
||||
<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">
|
||||
<div className="text-xs text-muted-foreground">Icon Library</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{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">
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5 *:[svg]:text-foreground!">
|
||||
{logos[currentIconLibrary?.name as keyof typeof logos]}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
@@ -269,16 +149,14 @@ export function IconLibraryPicker({
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{Object.values(iconLibraries).map((iconLibrary) => (
|
||||
<PickerRadioItem
|
||||
key={iconLibrary.name}
|
||||
value={iconLibrary.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{iconLibrary.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
@@ -286,81 +164,8 @@ export function IconLibraryPicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="iconLibrary"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-8 -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
|
||||
: iconLibrary === "phosphor"
|
||||
? IconPhosphor
|
||||
: IconRemixicon
|
||||
|
||||
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>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -36,6 +36,15 @@ const IconRemixicon = lazy(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
// Preload all icon renderer modules so switching libraries is instant.
|
||||
// These warm the browser module cache; React.lazy resolves immediately
|
||||
// for modules that are already loaded.
|
||||
void import("@/registry/icons/icon-lucide")
|
||||
void import("@/registry/icons/icon-tabler")
|
||||
void import("@/registry/icons/icon-hugeicons")
|
||||
void import("@/registry/icons/icon-phosphor")
|
||||
void import("@/registry/icons/icon-remixicon")
|
||||
|
||||
export function IconPlaceholder({
|
||||
...props
|
||||
}: {
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
|
||||
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"
|
||||
} from "@/examples/base/ui/collapsible"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -20,7 +15,12 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
} from "@/examples/base/ui/sidebar"
|
||||
import { ChevronRightIcon } from "lucide-react"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type Base } from "@/registry/bases"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
||||
|
||||
@@ -48,10 +48,10 @@ export function ItemExplorer({
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
className="sticky z-30 hidden h-[calc(100svh-var(--header-height)-2rem)] overscroll-none bg-transparent xl:flex"
|
||||
className="sticky z-30 hidden h-full overscroll-none bg-transparent xl:flex"
|
||||
collapsible="none"
|
||||
>
|
||||
<SidebarContent className="no-scrollbar -mx-1 overflow-x-hidden">
|
||||
<SidebarContent className="-mx-1 no-scrollbar overflow-x-hidden">
|
||||
{groupedItems.map((group) => (
|
||||
<Collapsible
|
||||
key={group.type}
|
||||
@@ -60,26 +60,26 @@ export function ItemExplorer({
|
||||
>
|
||||
<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" />
|
||||
<ChevronRightIcon className="size-3.5 text-muted-foreground transition-transform" />
|
||||
<span>{group.title}</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="border-border/50 relative ml-1.5 border-l pl-2">
|
||||
<SidebarMenu className="relative ml-1.5 border-l border-border/50 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",
|
||||
"absolute top-1/2 -left-2 h-px w-2 border-t border-border/50",
|
||||
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" />
|
||||
<div className="absolute top-1/2 -bottom-1 -left-2.5 w-1 bg-sidebar" />
|
||||
)}
|
||||
<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"
|
||||
className="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=true]:border-accent data-[active=true]:bg-accent 3xl:fixed:w-full 3xl:fixed:max-w-48"
|
||||
data-active={item.name === currentItem?.name}
|
||||
isActive={item.name === currentItem?.name}
|
||||
>
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
"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-64"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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 xl:w-96"
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<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
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -7,11 +7,6 @@ import {
|
||||
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({
|
||||
@@ -25,26 +20,22 @@ export function LockButton({
|
||||
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>
|
||||
<button
|
||||
type="button"
|
||||
title={locked ? "Unlock" : "Lock"}
|
||||
aria-label={locked ? "Unlock" : "Lock"}
|
||||
onClick={() => toggleLock(param)}
|
||||
data-locked={locked}
|
||||
className={cn(
|
||||
"flex size-4 cursor-pointer items-center justify-center rounded opacity-0 ring-foreground/60 transition-opacity outline-none group-focus-within/picker:opacity-100 group-hover/picker:opacity-100 focus:opacity-100 focus-visible:ring-1 data-[locked=true]:opacity-100 pointer-coarse:hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={locked ? SquareLock01Icon : SquareUnlock01Icon}
|
||||
strokeWidth={2}
|
||||
className="size-5 text-foreground"
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
83
apps/v4/app/(create)/components/main-menu.tsx
Normal file
83
apps/v4/app/(create)/components/main-menu.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type Button } from "@/examples/base/ui/button"
|
||||
import { Menu09Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerItem,
|
||||
PickerSeparator,
|
||||
PickerShortcut,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useActionMenuTrigger } from "@/app/(create)/hooks/use-action-menu"
|
||||
import { useHistory } from "@/app/(create)/hooks/use-history"
|
||||
import { useRandom } from "@/app/(create)/hooks/use-random"
|
||||
import { useReset } from "@/app/(create)/hooks/use-reset"
|
||||
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
|
||||
|
||||
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
|
||||
|
||||
export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
|
||||
const [isMac, setIsMac] = React.useState(false)
|
||||
const { canGoBack, canGoForward, goBack, goForward } = useHistory()
|
||||
const { openActionMenu } = useActionMenuTrigger()
|
||||
const { randomize } = useRandom()
|
||||
const { toggleTheme } = useThemeToggle()
|
||||
const { setShowResetDialog } = useReset()
|
||||
|
||||
React.useEffect(() => {
|
||||
const platform = navigator.platform
|
||||
const userAgent = navigator.userAgent
|
||||
setIsMac(APPLE_PLATFORM_REGEX.test(platform || userAgent))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Picker>
|
||||
<PickerTrigger
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 rounded-lg px-1.75 ring-1 ring-foreground/10 focus-visible:ring-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="font-medium">Menu</span>
|
||||
<HugeiconsIcon icon={Menu09Icon} strokeWidth={2} className="size-5" />
|
||||
</PickerTrigger>
|
||||
<PickerContent side="right" align="start" alignOffset={-8}>
|
||||
<PickerGroup>
|
||||
<PickerItem onClick={openActionMenu}>
|
||||
Navigate...
|
||||
<PickerShortcut>{isMac ? "⌘P" : "Ctrl+P"}</PickerShortcut>
|
||||
</PickerItem>
|
||||
<PickerItem onClick={randomize}>
|
||||
Shuffle <PickerShortcut>R</PickerShortcut>
|
||||
</PickerItem>
|
||||
<PickerItem onClick={toggleTheme}>
|
||||
Light/Dark <PickerShortcut>D</PickerShortcut>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerItem onClick={goBack} disabled={!canGoBack}>
|
||||
Undo <PickerShortcut>{isMac ? "⌘Z" : "Ctrl+Z"}</PickerShortcut>
|
||||
</PickerItem>
|
||||
<PickerItem onClick={goForward} disabled={!canGoForward}>
|
||||
Redo{" "}
|
||||
<PickerShortcut>{isMac ? "⇧⌘Z" : "Ctrl+Shift+Z"}</PickerShortcut>
|
||||
</PickerItem>
|
||||
<PickerSeparator />
|
||||
<PickerItem onClick={() => setShowResetDialog(true)}>
|
||||
Reset
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const MENU_OPTIONS = [
|
||||
fill="none"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
className="size-4 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"
|
||||
@@ -71,7 +71,7 @@ const MENU_OPTIONS = [
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
className="fill-foreground text-foreground"
|
||||
className="size-4 fill-background"
|
||||
>
|
||||
<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"
|
||||
@@ -80,21 +80,21 @@ const MENU_OPTIONS = [
|
||||
/>
|
||||
<path
|
||||
d="M8.5 11.5L14.5001 11.5"
|
||||
stroke="var(--background)"
|
||||
stroke="var(--foreground)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.5 15H13.5"
|
||||
stroke="var(--background)"
|
||||
stroke="var(--foreground)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 8H15.5"
|
||||
stroke="var(--background)"
|
||||
stroke="var(--foreground)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -123,12 +123,12 @@ export function MenuColorPicker({
|
||||
<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">
|
||||
<div className="text-xs text-muted-foreground">Menu Color</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{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">
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
|
||||
{currentMenu?.icon}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
@@ -145,8 +145,11 @@ export function MenuColorPicker({
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_OPTIONS.map((menu) => (
|
||||
<PickerRadioItem key={menu.value} value={menu.value}>
|
||||
{menu.icon}
|
||||
<PickerRadioItem
|
||||
key={menu.value}
|
||||
value={menu.value}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{menu.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
@@ -156,7 +159,7 @@ export function MenuColorPicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
87
apps/v4/app/(create)/components/mode-switcher.tsx
Normal file
87
apps/v4/app/(create)/components/mode-switcher.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Script from "next/script"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
|
||||
|
||||
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
|
||||
|
||||
export function ModeSwitcher({
|
||||
variant = "ghost",
|
||||
className,
|
||||
}: {
|
||||
variant?: React.ComponentProps<typeof Button>["variant"]
|
||||
className?: React.ComponentProps<typeof Button>["className"]
|
||||
}) {
|
||||
const { toggleTheme } = useThemeToggle()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={variant}
|
||||
size="icon"
|
||||
className={cn("group/toggle extend-touch-target", className)}
|
||||
onClick={toggleTheme}
|
||||
id="mode-switcher-button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="size-4.5"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M12 3l0 18" />
|
||||
<path d="M12 9l4.65 -4.65" />
|
||||
<path d="M12 14.3l7.37 -7.37" />
|
||||
<path d="M12 19.6l8.85 -8.85" />
|
||||
</svg>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function DarkModeScript() {
|
||||
return (
|
||||
<Script
|
||||
id="dark-mode-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
// Forward D key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'd' || e.key === 'D') && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
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: '${DARK_MODE_FORWARD_TYPE}',
|
||||
key: e.key
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
|
||||
<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",
|
||||
"relative w-40 shrink-0 touch-manipulation rounded-xl p-3 ring-1 ring-foreground/10 select-none hover:bg-muted focus-visible:ring-foreground/50 focus-visible:outline-none disabled:opacity-50 data-popup-open:bg-muted md:w-full md:rounded-lg md:px-2.5 md:py-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -31,7 +31,7 @@ function PickerContent({
|
||||
align = "start",
|
||||
alignOffset = 0,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
sideOffset = 20,
|
||||
anchor,
|
||||
className,
|
||||
...props
|
||||
@@ -53,13 +53,13 @@ function PickerContent({
|
||||
<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",
|
||||
"cn-menu-target z-50 no-scrollbar max-h-(--available-height) w-[calc(var(--available-width)-(--spacing(6)))] min-w-32 origin-(--transform-origin) translate-y-2 overflow-x-hidden overflow-y-auto rounded-xl border-0 bg-neutral-950/80 p-1.5 text-neutral-100 ring-1 ring-neutral-950/80 backdrop-blur-xl outline-none md:w-52 dark:bg-neutral-800/90 dark:ring-neutral-700/50 data-closed:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
<div className="absolute inset-0 z-40 bg-transparent" />
|
||||
<div className="absolute inset-y-0 right-0 left-62 z-40 bg-transparent" />
|
||||
</MenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ function PickerLabel({
|
||||
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",
|
||||
"px-2 py-1.5 text-xs font-medium text-neutral-400 data-inset:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -103,7 +103,7 @@ function PickerItem({
|
||||
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",
|
||||
"group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium outline-hidden select-none **:text-neutral-100 focus:bg-neutral-600 focus:text-neutral-100 focus:**:text-neutral-100 data-inset:pl-8 dark:focus:bg-neutral-700/80 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -128,7 +128,7 @@ function PickerSubTrigger({
|
||||
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",
|
||||
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent/95 focus:text-accent-foreground focus:ring-1 focus:ring-foreground/20 not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-8 data-open:bg-accent/95 data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -158,7 +158,7 @@ function PickerSubContent({
|
||||
<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",
|
||||
"w-auto min-w-[96px] rounded-md bg-popover/90 p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 backdrop-blur-xs duration-100 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
align={align}
|
||||
@@ -180,7 +180,7 @@ function PickerCheckboxItem({
|
||||
<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",
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent/95 focus:text-accent-foreground focus:ring-1 focus:ring-foreground/20 focus:**:text-accent-foreground 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}
|
||||
@@ -220,7 +220,7 @@ function PickerRadioItem({
|
||||
<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",
|
||||
"relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-sm font-medium outline-hidden select-none **:text-neutral-100 focus:bg-neutral-600 focus:text-neutral-100 focus:**:text-neutral-100 data-inset:pl-8 dark:focus:bg-neutral-700/80 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -252,7 +252,10 @@ function PickerSeparator({
|
||||
return (
|
||||
<MenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
className={cn(
|
||||
"-mx-1.5 my-1.5 h-px bg-neutral-600 dark:bg-neutral-700",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -263,7 +266,7 @@ function PickerShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
<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",
|
||||
"ml-auto text-xs tracking-widest text-neutral-400! group-focus/dropdown-menu-item:text-neutral-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
38
apps/v4/app/(create)/components/preset-handler.tsx
Normal file
38
apps/v4/app/(create)/components/preset-handler.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { generateRandomPreset, isPresetCode } from "shadcn/preset"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function PresetHandler() {
|
||||
const router = useRouter()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const hasConverted = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (params.preset === "random") {
|
||||
router.replace(`/create?preset=${generateRandomPreset()}`)
|
||||
}
|
||||
}, [params.preset, router])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasConverted.current) {
|
||||
return
|
||||
}
|
||||
hasConverted.current = true
|
||||
|
||||
if (!params.preset || params.preset === "random") {
|
||||
return
|
||||
}
|
||||
|
||||
if (isPresetCode(params.preset)) {
|
||||
return
|
||||
}
|
||||
|
||||
setParams({ base: params.base })
|
||||
}, [params.preset, params.base, setParams])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -80,8 +80,8 @@ export function PresetPicker({
|
||||
<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">
|
||||
<div className="text-xs text-muted-foreground">Preset</div>
|
||||
<div className="line-clamp-1 text-sm font-medium text-foreground">
|
||||
{currentPreset?.description ?? "Custom"}
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,7 +100,11 @@ export function PresetPicker({
|
||||
{currentBasePresets.map((preset) => {
|
||||
const style = STYLES.find((s) => s.name === preset.style)
|
||||
return (
|
||||
<PickerRadioItem key={preset.title} value={preset.title}>
|
||||
<PickerRadioItem
|
||||
key={preset.title}
|
||||
value={preset.title}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{style?.icon && (
|
||||
<div className="flex size-4 shrink-0 items-center justify-center">
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type PanelImperativeHandle } from "react-resizable-panels"
|
||||
|
||||
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker"
|
||||
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/action-menu"
|
||||
import {
|
||||
REDO_FORWARD_TYPE,
|
||||
UNDO_FORWARD_TYPE,
|
||||
} from "@/app/(create)/components/history-buttons"
|
||||
import { DARK_MODE_FORWARD_TYPE } from "@/app/(create)/components/mode-switcher"
|
||||
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
|
||||
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import {
|
||||
@@ -13,17 +15,75 @@ import {
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
|
||||
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
|
||||
|
||||
// Hoisted — only uses module-level constants, no component state. (rendering-hoist-jsx)
|
||||
function handleMessage(event: MessageEvent) {
|
||||
if (
|
||||
typeof window === "undefined" ||
|
||||
event.origin !== window.location.origin
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const type = event.data.type
|
||||
if (type === CMD_K_FORWARD_TYPE) {
|
||||
const isMac = MAC_REGEX.test(navigator.userAgent)
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: event.data.key || "k",
|
||||
metaKey: isMac,
|
||||
ctrlKey: !isMac,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === RANDOMIZE_FORWARD_TYPE) {
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: event.data.key || "r",
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === UNDO_FORWARD_TYPE) {
|
||||
const isMac = MAC_REGEX.test(navigator.userAgent)
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: "z",
|
||||
metaKey: isMac,
|
||||
ctrlKey: !isMac,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === REDO_FORWARD_TYPE) {
|
||||
const isMac = MAC_REGEX.test(navigator.userAgent)
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: "z",
|
||||
shiftKey: true,
|
||||
metaKey: isMac,
|
||||
ctrlKey: !isMac,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === DARK_MODE_FORWARD_TYPE) {
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: event.data.key || "d",
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function Preview() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
const resizablePanelRef = React.useRef<PanelImperativeHandle>(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
|
||||
@@ -45,44 +105,6 @@ export function Preview() {
|
||||
}
|
||||
}, [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 () => {
|
||||
@@ -104,9 +126,9 @@ export function Preview() {
|
||||
}, [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" />
|
||||
<div className="relative flex flex-1 flex-col justify-center overflow-hidden rounded-2xl ring ring-foreground/10 md:ring-muted dark:ring-foreground/10">
|
||||
<div className="relative z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden">
|
||||
<div className="absolute inset-0 bg-muted dark:bg-muted/30" />
|
||||
<iframe
|
||||
key={params.base + params.item}
|
||||
ref={iframeRef}
|
||||
@@ -114,12 +136,6 @@ export function Preview() {
|
||||
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>
|
||||
)
|
||||
|
||||
312
apps/v4/app/(create)/components/project-form.tsx
Normal file
312
apps/v4/app/(create)/components/project-form.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/examples/base/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/base/ui/field"
|
||||
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui/radio-group"
|
||||
import { Switch } from "@/examples/base/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/examples/base/ui/tabs"
|
||||
import { Copy01Icon, Globe02Icon, Tick02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
import {
|
||||
getFramework,
|
||||
getTemplateValue,
|
||||
NO_MONOREPO_FRAMEWORKS,
|
||||
TEMPLATES,
|
||||
} from "@/app/(create)/lib/templates"
|
||||
|
||||
const TURBOREPO_LOGO =
|
||||
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Turborepo</title><path d="M11.9906 4.1957c-4.2998 0-7.7981 3.501-7.7981 7.8043s3.4983 7.8043 7.7981 7.8043c4.2999 0 7.7982-3.501 7.7982-7.8043s-3.4983-7.8043-7.7982-7.8043m0 11.843c-2.229 0-4.0356-1.8079-4.0356-4.0387s1.8065-4.0387 4.0356-4.0387S16.0262 9.7692 16.0262 12s-1.8065 4.0388-4.0356 4.0388m.6534-13.1249V0C18.9726.3386 24 5.5822 24 12s-5.0274 11.66-11.356 12v-2.9139c4.7167-.3372 8.4516-4.2814 8.4516-9.0861s-3.735-8.749-8.4516-9.0861M5.113 17.9586c-1.2502-1.4446-2.0562-3.2845-2.2-5.3046H0c.151 2.8266 1.2808 5.3917 3.051 7.3668l2.0606-2.0622zM11.3372 24v-2.9139c-2.02-.1439-3.8584-.949-5.3019-2.2018l-2.0606 2.0623c1.975 1.773 4.538 2.9022 7.361 3.0534z"/></svg>'
|
||||
const ORIGIN = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000"
|
||||
const IS_LOCAL_DEV = ORIGIN.includes("localhost")
|
||||
const SHADCN_VERSION = process.env.NEXT_PUBLIC_RC ? "@rc" : "@latest"
|
||||
const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"] as const
|
||||
type PackageManager = (typeof PACKAGE_MANAGERS)[number]
|
||||
|
||||
export function ProjectForm({
|
||||
className,
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const presetCode = usePresetCode()
|
||||
const [config, setConfig] = useConfig()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
const packageManager = (config.packageManager || "pnpm") as PackageManager
|
||||
const framework = React.useMemo(
|
||||
() => getFramework(params.template ?? "next"),
|
||||
[params.template]
|
||||
)
|
||||
const isMonorepo = React.useMemo(
|
||||
() => params.template?.endsWith("-monorepo") ?? false,
|
||||
[params.template]
|
||||
)
|
||||
|
||||
const hasMonorepo = !NO_MONOREPO_FRAMEWORKS.includes(
|
||||
framework as (typeof NO_MONOREPO_FRAMEWORKS)[number]
|
||||
)
|
||||
|
||||
const commands = React.useMemo(() => {
|
||||
const presetFlag = ` --preset ${presetCode}`
|
||||
const baseFlag = params.base !== "radix" ? ` --base ${params.base}` : ""
|
||||
const templateFlag = ` --template ${framework}`
|
||||
const monorepoFlag = isMonorepo ? " --monorepo" : ""
|
||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
|
||||
|
||||
return IS_LOCAL_DEV && !process.env.NEXT_PUBLIC_RC
|
||||
? {
|
||||
pnpm: `shadcn init${flags}`,
|
||||
npm: `shadcn init${flags}`,
|
||||
yarn: `shadcn init${flags}`,
|
||||
bun: `shadcn init${flags}`,
|
||||
}
|
||||
: {
|
||||
pnpm: `pnpm dlx shadcn${SHADCN_VERSION} init${flags}`,
|
||||
npm: `npx shadcn${SHADCN_VERSION} init${flags}`,
|
||||
yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`,
|
||||
bun: `bunx --bun shadcn${SHADCN_VERSION} init${flags}`,
|
||||
}
|
||||
}, [framework, isMonorepo, params.base, params.rtl, presetCode])
|
||||
|
||||
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,
|
||||
})
|
||||
setHasCopied(true)
|
||||
}, [command, params.template])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger render={<Button className={cn(className)} />}>
|
||||
Create Project
|
||||
</DialogTrigger>
|
||||
<DialogContent className="min-w-0 sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pick a template and configure your project. Available for all major
|
||||
React frameworks.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel className="sr-only">Template</FieldLabel>
|
||||
<TemplateGrid template={params.template} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label" className="sr-only">
|
||||
Options
|
||||
</FieldLegend>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-disabled={hasMonorepo ? undefined : "true"}
|
||||
>
|
||||
<FieldLabel htmlFor="monorepo">
|
||||
<span
|
||||
className="size-4 text-foreground [&_svg]:size-4 [&_svg]:fill-current"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: TURBOREPO_LOGO,
|
||||
}}
|
||||
/>
|
||||
Create a monorepo
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="monorepo"
|
||||
checked={params.template?.endsWith("-monorepo") ?? false}
|
||||
disabled={!hasMonorepo}
|
||||
onCheckedChange={(checked) => {
|
||||
const framework = getFramework(params.template ?? "next")
|
||||
setParams({
|
||||
template: getTemplateValue(
|
||||
framework,
|
||||
checked === true
|
||||
) as typeof params.template,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="rtl">
|
||||
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
|
||||
Enable RTL support
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="rtl"
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ rtl: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
<DialogFooter className="min-w-0">
|
||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
packageManager: value as PackageManager,
|
||||
}))
|
||||
}}
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-lg border bg-surface"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-1 py-1">
|
||||
<TabsList className="font-mono">
|
||||
{PACKAGE_MANAGERS.map((manager) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={manager}
|
||||
value={manager}
|
||||
className="data-[state=active]:shadow-none"
|
||||
>
|
||||
{manager}
|
||||
</TabsTrigger>
|
||||
)
|
||||
})}
|
||||
</TabsList>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="relative overflow-hidden border-t border-border/50 bg-surface px-3 py-3 text-surface-foreground">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<Button onClick={handleCopy} className="h-9 w-full">
|
||||
{hasCopied ? "Copied" : "Copy Command"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const TemplateGrid = React.memo(function TemplateGrid({
|
||||
template,
|
||||
setParams,
|
||||
}: {
|
||||
template: DesignSystemSearchParams["template"]
|
||||
setParams: ReturnType<typeof useDesignSystemSearchParams>[1]
|
||||
}) {
|
||||
const isMonorepo = template?.endsWith("-monorepo") ?? false
|
||||
const framework = getFramework(template ?? "next")
|
||||
|
||||
const handleTemplateChange = React.useCallback(
|
||||
(value: string) => {
|
||||
setParams({
|
||||
template: getTemplateValue(
|
||||
value,
|
||||
isMonorepo
|
||||
) as DesignSystemSearchParams["template"],
|
||||
})
|
||||
},
|
||||
[isMonorepo, setParams]
|
||||
)
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
value={framework}
|
||||
onValueChange={handleTemplateChange}
|
||||
className="grid grid-cols-3 gap-2"
|
||||
>
|
||||
{TEMPLATES.map((item) => (
|
||||
<FieldLabel
|
||||
key={item.value}
|
||||
htmlFor={`template-${item.value}`}
|
||||
className="py-1"
|
||||
>
|
||||
<Field className="gap-0" orientation="horizontal">
|
||||
<FieldContent className="flex flex-col items-center justify-center gap-2">
|
||||
<div
|
||||
className="size-6 text-foreground [&_svg]:size-6 *:[svg]:text-foreground!"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.logo,
|
||||
}}
|
||||
></div>
|
||||
<FieldTitle className="text-xs">{item.title}</FieldTitle>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={item.value}
|
||||
id={`template-${item.value}`}
|
||||
className="sr-only absolute"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { RADII, type RadiusValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
@@ -21,22 +23,26 @@ export function RadiusPicker({
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const isRadiusLocked = params.style === "lyra"
|
||||
const selectedRadiusName = isRadiusLocked ? "none" : params.radius
|
||||
|
||||
const currentRadius = RADII.find((radius) => radius.name === params.radius)
|
||||
const currentRadius = RADII.find(
|
||||
(radius) => radius.name === selectedRadiusName
|
||||
)
|
||||
const defaultRadius = RADII.find((radius) => radius.name === "default")
|
||||
const otherRadii = RADII.filter((radius) => radius.name !== "default")
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<PickerTrigger disabled={isRadiusLocked}>
|
||||
<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">
|
||||
<div className="text-xs text-muted-foreground">Radius</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{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">
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base text-foreground select-none md:right-2.5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
@@ -63,6 +69,9 @@ export function RadiusPicker({
|
||||
<PickerRadioGroup
|
||||
value={currentRadius?.name}
|
||||
onValueChange={(value) => {
|
||||
if (isRadiusLocked) {
|
||||
return
|
||||
}
|
||||
setParams({ radius: value as RadiusValue })
|
||||
}}
|
||||
>
|
||||
@@ -71,20 +80,20 @@ export function RadiusPicker({
|
||||
<PickerRadioItem
|
||||
key={defaultRadius.name}
|
||||
value={defaultRadius.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
<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>
|
||||
{defaultRadius.label}
|
||||
</PickerRadioItem>
|
||||
)}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{otherRadii.map((radius) => (
|
||||
<PickerRadioItem key={radius.name} value={radius.name}>
|
||||
<PickerRadioItem
|
||||
key={radius.name}
|
||||
value={radius.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{radius.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
@@ -94,7 +103,7 @@ export function RadiusPicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="radius"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,146 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Script from "next/script"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
import { DiceFaces05Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import {
|
||||
BASE_COLORS,
|
||||
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 {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
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"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useRandom } from "@/app/(create)/hooks/use-random"
|
||||
|
||||
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
|
||||
|
||||
function randomItem<T>(array: readonly T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
export function RandomButton() {
|
||||
const { locks } = useLocks()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
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])
|
||||
export function RandomButton({
|
||||
variant = "outline",
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { randomize } = useRandom()
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
Use browser back/forward to navigate history
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant={variant}
|
||||
onClick={randomize}
|
||||
className={cn(
|
||||
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="w-full text-center font-medium">Shuffle</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Undo02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { DEFAULT_CONFIG } from "@/registry/config"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -14,60 +9,25 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/alert-dialog"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
} from "@/examples/base/ui/alert-dialog"
|
||||
|
||||
export function ResetButton() {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
import { useReset } from "@/app/(create)/hooks/use-reset"
|
||||
|
||||
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])
|
||||
export function ResetDialog() {
|
||||
const { showResetDialog, setShowResetDialog, confirmReset } = useReset()
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent className="dialog-ring p-4 sm:max-w-sm">
|
||||
<AlertDialog open={showResetDialog} onOpenChange={setShowResetDialog}>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Reset to defaults?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will reset all customization options to their default values.
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="rounded-lg">Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction className="rounded-lg" onClick={handleReset}>
|
||||
Reset
|
||||
</AlertDialogAction>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmReset}>Reset</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -1,37 +1,23 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
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 { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function ShareButton() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const presetCode = usePresetCode()
|
||||
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,
|
||||
])
|
||||
return `${origin}/create?preset=${presetCode}&item=${params.item}`
|
||||
}, [presetCode, params.item])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
@@ -51,23 +37,21 @@ export function ShareButton() {
|
||||
}, [shareUrl])
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="rounded-lg shadow-none lg:w-8 xl:w-fit"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
|
||||
)}
|
||||
<span className="lg:hidden xl:block">Share</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Copy Link</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button variant="outline" className="hidden md:flex" onClick={handleCopy}>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon
|
||||
icon={Tick02Icon}
|
||||
strokeWidth={2}
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
) : (
|
||||
<HugeiconsIcon
|
||||
icon={Share03Icon}
|
||||
strokeWidth={2}
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
)}
|
||||
Share
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
@@ -33,13 +32,13 @@ export function StylePicker({
|
||||
<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">
|
||||
<div className="text-xs text-muted-foreground">Style</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{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">
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center select-none md:right-2.5">
|
||||
{React.cloneElement(currentStyle.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
@@ -50,7 +49,6 @@ export function StylePicker({
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="md:w-64"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentStyle?.name}
|
||||
@@ -59,29 +57,14 @@ export function StylePicker({
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{styles.map((style) => (
|
||||
<PickerRadioItem
|
||||
value={style.name}
|
||||
key={style.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{style.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
@@ -89,7 +72,7 @@ export function StylePicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="style"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"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"
|
||||
@@ -26,7 +25,6 @@ export function ThemePicker({
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
@@ -51,24 +49,22 @@ export function ThemePicker({
|
||||
<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">
|
||||
<div className="text-xs text-muted-foreground">Theme</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{currentTheme?.title}
|
||||
</div>
|
||||
</div>
|
||||
{mounted && resolvedTheme && (
|
||||
{mounted && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
currentTheme?.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.[
|
||||
currentTheme?.cssVars?.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"
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none md:right-2.5"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
@@ -76,7 +72,7 @@ export function ThemePicker({
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-[23rem]"
|
||||
className="max-h-92"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentTheme?.name}
|
||||
@@ -90,34 +86,13 @@ export function ThemePicker({
|
||||
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
|
||||
key={theme.name}
|
||||
value={theme.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{theme.title}
|
||||
</PickerRadioItem>
|
||||
)
|
||||
})}
|
||||
@@ -133,23 +108,12 @@ export function ThemePicker({
|
||||
)
|
||||
.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
|
||||
key={theme.name}
|
||||
value={theme.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{theme.title}
|
||||
</PickerRadioItem>
|
||||
)
|
||||
})}
|
||||
@@ -159,7 +123,7 @@ export function ThemePicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="theme"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
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,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
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:4000"
|
||||
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}&rtl=${params.rtl}`
|
||||
const templateFlag = params.template ? ` --template ${params.template}` : ""
|
||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||
return {
|
||||
pnpm: `pnpm dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `npx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `yarn dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `bunx --bun shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
}, [
|
||||
params.base,
|
||||
params.style,
|
||||
params.baseColor,
|
||||
params.theme,
|
||||
params.iconLibrary,
|
||||
params.font,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
params.radius,
|
||||
params.template,
|
||||
params.rtl,
|
||||
])
|
||||
|
||||
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 className="gap-3">
|
||||
<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="has-data-[state=checked]:border-primary/10 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>
|
||||
<FieldLabel className="has-data-[state=checked]:border-primary/10 rounded-lg!">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent className="gap-1">
|
||||
<FieldTitle>Enable RTL</FieldTitle>
|
||||
<FieldDescription>
|
||||
<a
|
||||
href={`/docs/rtl/${params.template}`}
|
||||
className="text-foreground underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View the RTL setup guide for {selectedTemplate?.title}.
|
||||
</a>
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(rtl) => setParams({ rtl })}
|
||||
className="shadow-none"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<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 group-data-[orientation=horizontal]/tabs:h-8 *:data-[slot=tabs-trigger]:h-7 *: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>
|
||||
</FieldGroup>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
import { Skeleton } from "@/examples/base/ui/skeleton"
|
||||
|
||||
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 }) {
|
||||
@@ -18,38 +15,43 @@ export function V0Button({ className }: { className?: string }) {
|
||||
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}`
|
||||
const url = React.useMemo(() => {
|
||||
const searchParams = new URLSearchParams()
|
||||
|
||||
if (params.preset) {
|
||||
searchParams.set("preset", params.preset)
|
||||
}
|
||||
|
||||
searchParams.set("base", params.base)
|
||||
|
||||
return `${process.env.NEXT_PUBLIC_APP_URL}/init/v0?${searchParams.toString()}`
|
||||
}, [params.preset, params.base])
|
||||
|
||||
const title = React.useMemo(() => {
|
||||
return params.base && params.style
|
||||
? `New ${params.base}-${params.style} project`
|
||||
: "New Project"
|
||||
}, [params.base, params.style])
|
||||
|
||||
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] lg:w-8 xl:w-24",
|
||||
className
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`}
|
||||
target="_blank"
|
||||
>
|
||||
<span className="lg:hidden xl:block">Open in</span>
|
||||
<Icons.v0 className="size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Open current design in v0</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
role="link"
|
||||
variant={isMobile ? "default" : "outline"}
|
||||
className={cn("h-[31px] gap-1 rounded-lg", className)}
|
||||
render={
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${url}&title=${title}`}
|
||||
target="_blank"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span>Open in</span>
|
||||
<Icons.v0 className="size-5" data-icon="inline-end" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Icons } from "@/components/icons"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -12,7 +10,9 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
} from "@/examples/base/ui/dialog"
|
||||
|
||||
import { Icons } from "@/components/icons"
|
||||
|
||||
const STORAGE_KEY = "shadcn-create-welcome-dialog"
|
||||
|
||||
@@ -26,20 +26,21 @@ export function WelcomeDialog() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
// Stable callback — avoids re-creation on every render. (rerender-functional-setstate)
|
||||
const handleOpenChange = React.useCallback((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"
|
||||
className="dialog-ring max-w-92 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="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>
|
||||
@@ -48,19 +49,17 @@ export function WelcomeDialog() {
|
||||
<DialogTitle className="text-left text-base">
|
||||
Build your own shadcn/ui
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-foreground text-left leading-relaxed">
|
||||
<DialogDescription className="text-left leading-relaxed text-foreground">
|
||||
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 className="mt-2 text-left leading-relaxed font-medium text-foreground">
|
||||
Available for all major React frameworks.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="p-4 pt-0">
|
||||
<DialogClose asChild>
|
||||
<Button className="w-full rounded-lg shadow-none">
|
||||
Get Started
|
||||
</Button>
|
||||
<DialogFooter className="m-0">
|
||||
<DialogClose render={<Button className="w-full" />}>
|
||||
Get Started
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { HistoryProvider } from "@/app/(create)/hooks/use-history"
|
||||
import { LocksProvider } from "@/app/(create)/hooks/use-locks"
|
||||
|
||||
export default function CreateLayout({
|
||||
@@ -5,5 +8,11 @@ export default function CreateLayout({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <LocksProvider>{children}</LocksProvider>
|
||||
return (
|
||||
<LocksProvider>
|
||||
<Suspense>
|
||||
<HistoryProvider>{children}</HistoryProvider>
|
||||
</Suspense>
|
||||
</LocksProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,35 +1,13 @@
|
||||
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 { source } from "@/lib/source"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { MainNav } from "@/components/main-nav"
|
||||
import { MobileNav } from "@/components/mobile-nav"
|
||||
import { ModeSwitcher } from "@/components/mode-switcher"
|
||||
import { SiteConfig } from "@/components/site-config"
|
||||
import { 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 { SiteHeader } from "@/components/site-header"
|
||||
import { Customizer } from "@/app/(create)/components/customizer"
|
||||
import { ItemExplorer } from "@/app/(create)/components/item-explorer"
|
||||
import { ItemPicker } from "@/app/(create)/components/item-picker"
|
||||
import { PresetHandler } from "@/app/(create)/components/preset-handler"
|
||||
import { Preview } from "@/app/(create)/components/preview"
|
||||
import { RandomButton } from "@/app/(create)/components/random-button"
|
||||
import { ResetButton } from "@/app/(create)/components/reset-button"
|
||||
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"
|
||||
import { getAllItems } from "@/app/(create)/lib/api"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "New Project",
|
||||
@@ -60,86 +38,22 @@ export const metadata: Metadata = {
|
||||
},
|
||||
}
|
||||
|
||||
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 pageTree = source.pageTree
|
||||
const items = await getItemsForBase(base.name)
|
||||
|
||||
const filteredItems = items
|
||||
.filter((item) => item !== null)
|
||||
.map((item) => ({
|
||||
name: item.name,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
}))
|
||||
export default async function CreatePage() {
|
||||
const itemsByBase = await getAllItems()
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="layout"
|
||||
className="section-soft relative z-10 flex min-h-svh flex-col"
|
||||
className="group/layout relative z-10 flex h-svh flex-col overflow-hidden section-soft [--customizer-width:--spacing(56)] [--gap:--spacing(4)] md:[--gap:--spacing(6)]"
|
||||
>
|
||||
<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="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
|
||||
<MobileNav
|
||||
tree={pageTree}
|
||||
items={siteConfig.navItems}
|
||||
className="flex lg:hidden"
|
||||
/>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hidden size-8 lg:flex"
|
||||
>
|
||||
<Link href="/">
|
||||
<Icons.logo className="size-5" />
|
||||
<span className="sr-only">{siteConfig.name}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
|
||||
</div>
|
||||
<div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center justify-end gap-2 px-4.5 pb-4 sm:static sm:p-0 lg:ml-0">
|
||||
<ItemPicker items={filteredItems} />
|
||||
<div className="items-center gap-0 sm:hidden">
|
||||
<RandomButton />
|
||||
<ResetButton />
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mr-2 flex" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end">
|
||||
<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>
|
||||
<SiteHeader />
|
||||
<main
|
||||
data-slot="designer"
|
||||
className="flex min-h-0 flex-1 flex-col gap-(--gap) p-(--gap) pt-[calc(var(--gap)*0.25)] md:flex-row-reverse"
|
||||
>
|
||||
<Preview />
|
||||
<Customizer itemsByBase={itemsByBase} />
|
||||
<PresetHandler />
|
||||
<WelcomeDialog />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
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
|
||||
|
||||
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 registryBase = buildRegistryBase(designSystemConfig)
|
||||
|
||||
// Build all files in parallel.
|
||||
const [globalsCss, layoutFile, componentFiles] = await Promise.all([
|
||||
buildGlobalsCss(registryBase),
|
||||
buildLayoutFile(designSystemConfig),
|
||||
buildComponentFiles(designSystemConfig),
|
||||
])
|
||||
|
||||
return registryItemSchema.parse({
|
||||
name: designSystemConfig.item ?? "Item",
|
||||
type: "registry:item",
|
||||
files: [globalsCss, layoutFile, ...componentFiles],
|
||||
})
|
||||
}
|
||||
|
||||
function buildGlobalsCss(registryBase: RegistryItem) {
|
||||
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()
|
||||
}
|
||||
142
apps/v4/app/(create)/hooks/use-action-menu.ts
Normal file
142
apps/v4/app/(create)/hooks/use-action-menu.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
import useSWR from "swr"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
||||
|
||||
const ACTION_MENU_OPEN_KEY = "create:action-menu-open"
|
||||
|
||||
type ActionMenuItem = {
|
||||
id: string
|
||||
type: string
|
||||
label: string
|
||||
registryName: string
|
||||
}
|
||||
|
||||
type ActionMenuGroup = {
|
||||
type: string
|
||||
title: string
|
||||
items: ActionMenuItem[]
|
||||
}
|
||||
|
||||
type ActionMenuSourceItem = Pick<RegistryItem, "name" | "title" | "type">
|
||||
|
||||
const SEARCH_KEYWORDS: Record<string, string> = {
|
||||
"registry:block": "block blocks component components",
|
||||
"registry:item": "item items component components",
|
||||
}
|
||||
|
||||
function sortRegistryGroups(groups: ReturnType<typeof groupItemsByType>) {
|
||||
return [...groups].sort((a, b) => {
|
||||
if (a.type === b.type) {
|
||||
return a.title.localeCompare(b.title)
|
||||
}
|
||||
if (a.type === "registry:block") {
|
||||
return -1
|
||||
}
|
||||
if (b.type === "registry:block") {
|
||||
return 1
|
||||
}
|
||||
return a.title.localeCompare(b.title)
|
||||
})
|
||||
}
|
||||
|
||||
export function useActionMenu(
|
||||
itemsByBase: Record<string, ActionMenuSourceItem[]>
|
||||
) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const { data: open = false, mutate: setOpenData } = useSWR<boolean>(
|
||||
ACTION_MENU_OPEN_KEY,
|
||||
{
|
||||
fallbackData: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
}
|
||||
)
|
||||
|
||||
const groups = React.useMemo<ActionMenuGroup[]>(() => {
|
||||
const currentBaseItems = itemsByBase?.[params.base] ?? []
|
||||
const sortedRegistryGroups = sortRegistryGroups(
|
||||
groupItemsByType(currentBaseItems)
|
||||
)
|
||||
|
||||
return sortedRegistryGroups.map((group) => ({
|
||||
type: group.type,
|
||||
title: group.title,
|
||||
items: group.items.map((item) => ({
|
||||
id: `${group.type}:${item.name}`,
|
||||
type: group.type,
|
||||
label: item.title ?? item.name,
|
||||
registryName: item.name,
|
||||
})),
|
||||
}))
|
||||
}, [itemsByBase, params.base])
|
||||
|
||||
const activeRegistryName = params.item
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(registryName: string) => {
|
||||
setParams({ item: registryName })
|
||||
void setOpenData(false, { revalidate: false })
|
||||
},
|
||||
[setOpenData, setParams]
|
||||
)
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
void setOpenData(nextOpen, { revalidate: false })
|
||||
},
|
||||
[setOpenData]
|
||||
)
|
||||
|
||||
const getCommandValue = React.useCallback((item: ActionMenuItem) => {
|
||||
const keywords = SEARCH_KEYWORDS[item.type] ?? item.type.replace(":", " ")
|
||||
return `${item.label ?? ""} ${keywords}`.trim()
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "p" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
void setOpenData((currentOpen = false) => !currentOpen, {
|
||||
revalidate: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [setOpenData])
|
||||
|
||||
return {
|
||||
activeRegistryName,
|
||||
getCommandValue,
|
||||
groups,
|
||||
handleSelect,
|
||||
open,
|
||||
setOpen: handleOpenChange,
|
||||
}
|
||||
}
|
||||
|
||||
export function useActionMenuTrigger() {
|
||||
const { mutate: setOpenData } = useSWR<boolean>(ACTION_MENU_OPEN_KEY, {
|
||||
fallbackData: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
})
|
||||
|
||||
const openActionMenu = React.useCallback(() => {
|
||||
void setOpenData(true, { revalidate: false })
|
||||
}, [setOpenData])
|
||||
|
||||
return {
|
||||
openActionMenu,
|
||||
}
|
||||
}
|
||||
40
apps/v4/app/(create)/hooks/use-design-system.ts
Normal file
40
apps/v4/app/(create)/hooks/use-design-system.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { encodePreset, isPresetCode } from "shadcn/preset"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
// Returns the current preset code derived from search params.
|
||||
export function usePresetCode() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
|
||||
return React.useMemo(() => {
|
||||
// If preset is already in the URL, return it.
|
||||
if (params.preset && isPresetCode(params.preset)) {
|
||||
return params.preset
|
||||
}
|
||||
|
||||
// Otherwise encode current params (e.g. on initial load before first interaction).
|
||||
return encodePreset({
|
||||
style: params.style ?? undefined,
|
||||
baseColor: params.baseColor ?? undefined,
|
||||
theme: params.theme ?? undefined,
|
||||
iconLibrary: params.iconLibrary ?? undefined,
|
||||
font: params.font ?? undefined,
|
||||
radius: params.radius ?? undefined,
|
||||
menuAccent: params.menuAccent ?? undefined,
|
||||
menuColor: params.menuColor ?? undefined,
|
||||
} as Parameters<typeof encodePreset>[0])
|
||||
}, [
|
||||
params.preset,
|
||||
params.style,
|
||||
params.baseColor,
|
||||
params.theme,
|
||||
params.iconLibrary,
|
||||
params.font,
|
||||
params.radius,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
])
|
||||
}
|
||||
145
apps/v4/app/(create)/hooks/use-history.tsx
Normal file
145
apps/v4/app/(create)/hooks/use-history.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
|
||||
type HistoryContextValue = {
|
||||
canGoBack: boolean
|
||||
canGoForward: boolean
|
||||
goBack: () => void
|
||||
goForward: () => void
|
||||
}
|
||||
|
||||
const HistoryContext = React.createContext<HistoryContextValue | null>(null)
|
||||
|
||||
export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const preset = searchParams.get("preset") ?? ""
|
||||
|
||||
const entriesRef = React.useRef<string[]>([preset])
|
||||
const indexRef = React.useRef(0)
|
||||
const maxIndexRef = React.useRef(0)
|
||||
const isNavigatingRef = React.useRef(false)
|
||||
|
||||
const [index, setIndex] = React.useState(0)
|
||||
const [maxIndex, setMaxIndex] = React.useState(0)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isNavigatingRef.current) {
|
||||
isNavigatingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
if (preset === entriesRef.current[indexRef.current]) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextEntries = entriesRef.current.slice(0, indexRef.current + 1)
|
||||
nextEntries.push(preset)
|
||||
entriesRef.current = nextEntries
|
||||
|
||||
const nextIndex = nextEntries.length - 1
|
||||
indexRef.current = nextIndex
|
||||
maxIndexRef.current = nextIndex
|
||||
setIndex(nextIndex)
|
||||
setMaxIndex(nextIndex)
|
||||
}, [preset])
|
||||
|
||||
const canGoBack = index > 0
|
||||
const canGoForward = index < maxIndex
|
||||
|
||||
const goBack = React.useCallback(() => {
|
||||
if (indexRef.current <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
isNavigatingRef.current = true
|
||||
const nextIndex = indexRef.current - 1
|
||||
indexRef.current = nextIndex
|
||||
setIndex(nextIndex)
|
||||
|
||||
const targetPreset = entriesRef.current[nextIndex]
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (targetPreset) {
|
||||
params.set("preset", targetPreset)
|
||||
} else {
|
||||
params.delete("preset")
|
||||
}
|
||||
const query = params.toString()
|
||||
router.replace(query ? `${pathname}?${query}` : pathname)
|
||||
}, [pathname, router])
|
||||
|
||||
const goForward = React.useCallback(() => {
|
||||
if (indexRef.current >= maxIndexRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
isNavigatingRef.current = true
|
||||
const nextIndex = indexRef.current + 1
|
||||
indexRef.current = nextIndex
|
||||
setIndex(nextIndex)
|
||||
|
||||
const targetPreset = entriesRef.current[nextIndex]
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (targetPreset) {
|
||||
params.set("preset", targetPreset)
|
||||
} else {
|
||||
params.delete("preset")
|
||||
}
|
||||
const query = params.toString()
|
||||
router.replace(query ? `${pathname}?${query}` : pathname)
|
||||
}, [pathname, router])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (!e.metaKey && !e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = e.key.toLowerCase()
|
||||
|
||||
if ((key === "z" && e.shiftKey) || (key === "y" && e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
goForward()
|
||||
return
|
||||
}
|
||||
|
||||
if (key === "z") {
|
||||
e.preventDefault()
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [goBack, goForward])
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({ canGoBack, canGoForward, goBack, goForward }),
|
||||
[canGoBack, canGoForward, goBack, goForward]
|
||||
)
|
||||
|
||||
return <HistoryContext value={value}>{children}</HistoryContext>
|
||||
}
|
||||
|
||||
export function useHistory() {
|
||||
const context = React.useContext(HistoryContext)
|
||||
if (!context) {
|
||||
throw new Error("useHistory must be used within HistoryProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -23,6 +23,12 @@ export function useIframeMessageListener<
|
||||
messageType: MessageType,
|
||||
onMessage: (data: Extract<Message, { type: MessageType }>["data"]) => void
|
||||
) {
|
||||
const onMessageRef = React.useRef(onMessage)
|
||||
|
||||
React.useEffect(() => {
|
||||
onMessageRef.current = onMessage
|
||||
}, [onMessage])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isInIframe()) {
|
||||
return
|
||||
@@ -30,7 +36,7 @@ export function useIframeMessageListener<
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === messageType) {
|
||||
onMessage(event.data.data)
|
||||
onMessageRef.current(event.data.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +44,7 @@ export function useIframeMessageListener<
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage)
|
||||
}
|
||||
}, [messageType, onMessage])
|
||||
}, [messageType])
|
||||
}
|
||||
|
||||
export function sendToIframe<
|
||||
|
||||
132
apps/v4/app/(create)/hooks/use-random.tsx
Normal file
132
apps/v4/app/(create)/hooks/use-random.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
BASE_COLORS,
|
||||
getThemesForBaseColor,
|
||||
iconLibraries,
|
||||
MENU_ACCENTS,
|
||||
MENU_COLORS,
|
||||
RADII,
|
||||
STYLES,
|
||||
} from "@/registry/config"
|
||||
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"
|
||||
|
||||
function randomItem<T>(array: readonly T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
export function useRandom() {
|
||||
const { locks } = useLocks()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const paramsRef = React.useRef(params)
|
||||
React.useEffect(() => {
|
||||
paramsRef.current = params
|
||||
}, [params])
|
||||
|
||||
const randomize = React.useCallback(() => {
|
||||
const selectedStyle = locks.has("style")
|
||||
? paramsRef.current.style
|
||||
: randomItem(STYLES).name
|
||||
|
||||
const context: RandomizeContext = {
|
||||
style: selectedStyle,
|
||||
}
|
||||
|
||||
const availableBaseColors = applyBias(
|
||||
BASE_COLORS,
|
||||
context,
|
||||
RANDOMIZE_BIASES.baseColors
|
||||
)
|
||||
const baseColor = locks.has("baseColor")
|
||||
? paramsRef.current.baseColor
|
||||
: randomItem(availableBaseColors).name
|
||||
context.baseColor = 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")
|
||||
? paramsRef.current.theme
|
||||
: randomItem(availableThemes).name
|
||||
const selectedFont = locks.has("font")
|
||||
? paramsRef.current.font
|
||||
: randomItem(availableFonts).value
|
||||
const selectedRadius = locks.has("radius")
|
||||
? paramsRef.current.radius
|
||||
: randomItem(availableRadii).name
|
||||
const selectedIconLibrary = locks.has("iconLibrary")
|
||||
? paramsRef.current.iconLibrary
|
||||
: randomItem(Object.values(iconLibraries)).name
|
||||
const selectedMenuAccent = locks.has("menuAccent")
|
||||
? paramsRef.current.menuAccent
|
||||
: randomItem(MENU_ACCENTS).value
|
||||
const selectedMenuColor = locks.has("menuColor")
|
||||
? paramsRef.current.menuColor
|
||||
: randomItem(MENU_COLORS).value
|
||||
|
||||
context.theme = selectedTheme
|
||||
context.font = selectedFont
|
||||
context.radius = selectedRadius
|
||||
|
||||
const nextParams = {
|
||||
style: selectedStyle,
|
||||
baseColor,
|
||||
theme: selectedTheme,
|
||||
iconLibrary: selectedIconLibrary,
|
||||
font: selectedFont,
|
||||
menuAccent: selectedMenuAccent,
|
||||
menuColor: selectedMenuColor,
|
||||
radius: selectedRadius,
|
||||
}
|
||||
|
||||
// Keep the ref in sync so rapid repeats use the latest randomized state
|
||||
// even before the URL state finishes committing.
|
||||
paramsRef.current = {
|
||||
...paramsRef.current,
|
||||
...nextParams,
|
||||
}
|
||||
|
||||
setParams(nextParams)
|
||||
}, [setParams, locks])
|
||||
|
||||
const randomizeRef = React.useRef(randomize)
|
||||
React.useEffect(() => {
|
||||
randomizeRef.current = randomize
|
||||
}, [randomize])
|
||||
|
||||
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()
|
||||
randomizeRef.current()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { randomize }
|
||||
}
|
||||
55
apps/v4/app/(create)/hooks/use-reset.tsx
Normal file
55
apps/v4/app/(create)/hooks/use-reset.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import useSWR from "swr"
|
||||
|
||||
import { DEFAULT_CONFIG } from "@/registry/config"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const RESET_DIALOG_KEY = "create:reset-dialog-open"
|
||||
|
||||
export function useReset() {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const { data: showResetDialog = false, mutate: setShowResetDialogData } =
|
||||
useSWR<boolean>(RESET_DIALOG_KEY, {
|
||||
fallbackData: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
})
|
||||
|
||||
const reset = React.useCallback(() => {
|
||||
setParams({
|
||||
base: params.base,
|
||||
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 handleShowResetDialogChange = React.useCallback(
|
||||
(open: boolean) => {
|
||||
void setShowResetDialogData(open, { revalidate: false })
|
||||
},
|
||||
[setShowResetDialogData]
|
||||
)
|
||||
|
||||
const confirmReset = React.useCallback(() => {
|
||||
reset()
|
||||
void setShowResetDialogData(false, { revalidate: false })
|
||||
}, [reset, setShowResetDialogData])
|
||||
|
||||
return {
|
||||
reset,
|
||||
showResetDialog,
|
||||
setShowResetDialog: handleShowResetDialogChange,
|
||||
confirmReset,
|
||||
}
|
||||
}
|
||||
48
apps/v4/app/(create)/hooks/use-theme-toggle.tsx
Normal file
48
apps/v4/app/(create)/hooks/use-theme-toggle.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { useMetaColor } from "@/hooks/use-meta-color"
|
||||
|
||||
export function useThemeToggle() {
|
||||
const { setTheme, resolvedTheme } = useTheme()
|
||||
const { setMetaColor, metaColor } = useMetaColor()
|
||||
|
||||
React.useEffect(() => {
|
||||
setMetaColor(metaColor)
|
||||
}, [metaColor, setMetaColor])
|
||||
|
||||
const toggleTheme = React.useCallback(() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}, [resolvedTheme, setTheme])
|
||||
|
||||
// Listen for the D key to toggle theme.
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (
|
||||
(e.key === "d" || e.key === "D") &&
|
||||
!e.metaKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey
|
||||
) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
toggleTheme()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [toggleTheme])
|
||||
|
||||
return { toggleTheme }
|
||||
}
|
||||
244
apps/v4/app/(create)/init/md/build-instructions.ts
Normal file
244
apps/v4/app/(create)/init/md/build-instructions.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import dedent from "dedent"
|
||||
|
||||
import { UI_COMPONENTS } from "@/lib/components"
|
||||
import {
|
||||
buildRegistryBase,
|
||||
fonts,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
|
||||
// Builds step-by-step markdown instructions for manually setting up a project.
|
||||
export function buildInstructions(config: DesignSystemConfig) {
|
||||
const registryBase = buildRegistryBase(config)
|
||||
|
||||
const dependencies = [
|
||||
...(registryBase.dependencies ?? []),
|
||||
"clsx",
|
||||
"tailwind-merge",
|
||||
]
|
||||
|
||||
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 font = fonts.find((f) => f.name === `font-${config.font}`)
|
||||
|
||||
const sections = [
|
||||
buildDependenciesSection(dependencies),
|
||||
buildUtilsSection(),
|
||||
buildCssSection(lightVars, darkVars),
|
||||
buildFontSection(font),
|
||||
buildComponentsJsonSection(config),
|
||||
buildAvailableComponentsSection(config),
|
||||
config.rtl ? buildRtlSection(config) : null,
|
||||
]
|
||||
|
||||
return sections.filter(Boolean).join("\n\n---\n\n")
|
||||
}
|
||||
|
||||
function buildDependenciesSection(dependencies: string[]) {
|
||||
const list = dependencies.map((dep) => `- ${dep}`).join("\n")
|
||||
|
||||
return dedent`
|
||||
## Step 1: Dependencies
|
||||
|
||||
The following dependencies are required:
|
||||
|
||||
${list}
|
||||
`
|
||||
}
|
||||
|
||||
function buildUtilsSection() {
|
||||
return dedent`
|
||||
## Step 2: Create \`lib/utils.ts\`
|
||||
|
||||
\`\`\`ts
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
function buildCssSection(lightVars: string, darkVars: string) {
|
||||
return dedent`
|
||||
## Step 3: Set up CSS
|
||||
|
||||
Add the following to your global CSS file (e.g. \`app/globals.css\`):
|
||||
|
||||
\`\`\`css
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@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) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
${lightVars}
|
||||
}
|
||||
|
||||
.dark {
|
||||
${darkVars}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
function buildFontSection(font: (typeof fonts)[number] | undefined) {
|
||||
if (!font) {
|
||||
return null
|
||||
}
|
||||
|
||||
const googleFontsUrl = `https://fonts.google.com/specimen/${font.font.import.replace(/_/g, "+")}`
|
||||
|
||||
return dedent`
|
||||
## Step 4: Set up the font
|
||||
|
||||
This config uses **${font.title}** (\`${font.font.variable}\`).
|
||||
|
||||
### Next.js
|
||||
|
||||
\`\`\`tsx
|
||||
import { ${font.font.import} } from "next/font/google"
|
||||
|
||||
const fontSans = ${font.font.import}({
|
||||
subsets: ["latin"],
|
||||
variable: "${font.font.variable}",
|
||||
})
|
||||
|
||||
// Add fontSans.variable to your <html> className.
|
||||
// <html className={fontSans.variable}>
|
||||
\`\`\`
|
||||
|
||||
### Other frameworks
|
||||
|
||||
Add the font from [Google Fonts](${googleFontsUrl}) and set the \`${font.font.variable}\` CSS variable to the font family:
|
||||
|
||||
\`\`\`css
|
||||
:root {
|
||||
${font.font.variable}: ${font.font.family};
|
||||
}
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
function buildComponentsJsonSection(config: DesignSystemConfig) {
|
||||
const componentsJson = {
|
||||
$schema: "https://ui.shadcn.com/schema.json",
|
||||
style: `${config.base}-${config.style}`,
|
||||
tailwind: {
|
||||
css: "app/globals.css",
|
||||
baseColor: config.baseColor,
|
||||
},
|
||||
iconLibrary: config.iconLibrary,
|
||||
aliases: {
|
||||
components: "@/components",
|
||||
utils: "@/lib/utils",
|
||||
ui: "@/components/ui",
|
||||
lib: "@/lib",
|
||||
hooks: "@/hooks",
|
||||
},
|
||||
}
|
||||
|
||||
return dedent`
|
||||
## Step 5: Create \`components.json\`
|
||||
|
||||
Add a \`components.json\` file to the root of your project:
|
||||
|
||||
\`\`\`json
|
||||
${JSON.stringify(componentsJson, null, 2)}
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
function buildAvailableComponentsSection(config: DesignSystemConfig) {
|
||||
const list = UI_COMPONENTS.join(", ")
|
||||
const style = `${config.base}-${config.style}`
|
||||
|
||||
return dedent`
|
||||
## Available Components
|
||||
|
||||
${list}
|
||||
|
||||
To fetch the source for a component, use:
|
||||
\`https://ui.shadcn.com/r/styles/${style}/<component>.json\`
|
||||
|
||||
For documentation and examples, visit:
|
||||
\`https://ui.shadcn.com/docs/components/${config.base}/<component>\`
|
||||
`
|
||||
}
|
||||
|
||||
function buildRtlSection(config: DesignSystemConfig) {
|
||||
const template =
|
||||
config.template === "next-monorepo" ? "next" : (config.template ?? "next")
|
||||
|
||||
return dedent`
|
||||
## RTL Support
|
||||
|
||||
Add \`dir="rtl"\` to your root \`<html>\` element:
|
||||
|
||||
\`\`\`html
|
||||
<html dir="rtl">
|
||||
\`\`\`
|
||||
|
||||
For full RTL setup including the \`DirectionProvider\`, see the [RTL documentation](https://ui.shadcn.com/docs/rtl/${template}).
|
||||
`
|
||||
}
|
||||
30
apps/v4/app/(create)/init/md/route.ts
Normal file
30
apps/v4/app/(create)/init/md/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { type NextRequest } from "next/server"
|
||||
import { track } from "@vercel/analytics/server"
|
||||
|
||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||
|
||||
import { buildInstructions } from "./build-instructions"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const result = parseDesignSystemConfig(searchParams)
|
||||
|
||||
if (!result.success) {
|
||||
return new Response(result.error, { status: 400 })
|
||||
}
|
||||
|
||||
track("create_app_manual", result.data)
|
||||
|
||||
const markdown = buildInstructions(result.data)
|
||||
|
||||
return new Response(markdown, {
|
||||
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
error instanceof Error ? error.message : "An unknown error occurred",
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
47
apps/v4/app/(create)/init/parse-config.ts
Normal file
47
apps/v4/app/(create)/init/parse-config.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { decodePreset, isPresetCode } from "shadcn/preset"
|
||||
|
||||
import {
|
||||
designSystemConfigSchema,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
|
||||
// Parses design system config from URL search params.
|
||||
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||
let configInput: Record<string, unknown>
|
||||
const presetParam = searchParams.get("preset")
|
||||
|
||||
if (presetParam && isPresetCode(presetParam)) {
|
||||
const decoded = decodePreset(presetParam)
|
||||
if (!decoded) {
|
||||
return { success: false as const, error: "Invalid preset code" }
|
||||
}
|
||||
configInput = {
|
||||
...decoded,
|
||||
base: searchParams.get("base") ?? "radix",
|
||||
template: searchParams.get("template") ?? undefined,
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
}
|
||||
} else {
|
||||
configInput = {
|
||||
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") ?? undefined,
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
}
|
||||
}
|
||||
|
||||
const result = designSystemConfigSchema.safeParse(configInput)
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false as const, error: result.error.issues[0].message }
|
||||
}
|
||||
|
||||
return { success: true as const, data: result.data as DesignSystemConfig }
|
||||
}
|
||||
@@ -2,31 +2,16 @@ import { NextResponse, type NextRequest } from "next/server"
|
||||
import { track } from "@vercel/analytics/server"
|
||||
import { registryItemSchema } from "shadcn/schema"
|
||||
|
||||
import { buildRegistryBase, designSystemConfigSchema } from "@/registry/config"
|
||||
import { buildRegistryBase } from "@/registry/config"
|
||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-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"),
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
})
|
||||
const result = parseDesignSystemConfig(searchParams)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: result.error.issues[0].message },
|
||||
{ status: 400 }
|
||||
)
|
||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const registryBase = buildRegistryBase(result.data)
|
||||
@@ -42,7 +27,10 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
track("create_app", result.data)
|
||||
track("create_app", {
|
||||
...result.data,
|
||||
preset: searchParams.get("preset") ?? "",
|
||||
})
|
||||
|
||||
return NextResponse.json(parseResult.data)
|
||||
} catch (error) {
|
||||
|
||||
36
apps/v4/app/(create)/init/v0/route.ts
Normal file
36
apps/v4/app/(create)/init/v0/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { after, NextResponse, type NextRequest } from "next/server"
|
||||
import { track } from "@vercel/analytics/server"
|
||||
|
||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||
import { buildV0Payload } from "@/app/(create)/lib/v0"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const result = parseDesignSystemConfig(searchParams)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
}
|
||||
|
||||
// Defer analytics to after response is sent.
|
||||
after(() => {
|
||||
track("create_open_in_v0", {
|
||||
...result.data,
|
||||
preset: searchParams.get("preset") ?? "",
|
||||
})
|
||||
})
|
||||
|
||||
const payload = await buildV0Payload(result.data)
|
||||
|
||||
return NextResponse.json(payload)
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
error instanceof Error ? error.message : "An unknown error occurred",
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@ import "server-only"
|
||||
|
||||
import { registryItemSchema } from "shadcn/schema"
|
||||
|
||||
import { getThemesForBaseColor, type BaseName } from "@/registry/config"
|
||||
import { ALLOWED_ITEM_TYPES } from "@/app/(create)/lib/constants"
|
||||
import { BASES, getThemesForBaseColor, type BaseName } from "@/registry/config"
|
||||
import {
|
||||
ALLOWED_ITEM_TYPES,
|
||||
EXCLUDED_ITEMS,
|
||||
} from "@/app/(create)/lib/constants"
|
||||
|
||||
export async function getItemsForBase(base: BaseName) {
|
||||
const { Index } = await import("@/registry/bases/__index__")
|
||||
@@ -13,8 +16,10 @@ export async function getItemsForBase(base: BaseName) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.values(index).filter((item) =>
|
||||
ALLOWED_ITEM_TYPES.includes(item.type)
|
||||
return Object.values(index).filter(
|
||||
(item) =>
|
||||
ALLOWED_ITEM_TYPES.includes(item.type) &&
|
||||
!EXCLUDED_ITEMS.includes(item.name)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,7 +27,7 @@ export async function getBaseItem(name: string, base: BaseName) {
|
||||
const { Index } = await import("@/registry/bases/__index__")
|
||||
const index = Index[base]
|
||||
|
||||
if (!index?.[name]) {
|
||||
if (!index?.[name] || EXCLUDED_ITEMS.includes(name)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -33,12 +38,35 @@ export async function getBaseComponent(name: string, base: BaseName) {
|
||||
const { Index } = await import("@/registry/bases/__index__")
|
||||
const index = Index[base]
|
||||
|
||||
if (!index?.[name]) {
|
||||
if (!index?.[name] || EXCLUDED_ITEMS.includes(name)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return index[name].component
|
||||
}
|
||||
|
||||
export async function getAllItems() {
|
||||
const entries = await Promise.all(
|
||||
BASES.map(async (base) => {
|
||||
const items = await getItemsForBase(base.name as BaseName)
|
||||
const filtered: Pick<
|
||||
NonNullable<(typeof items)[number]>,
|
||||
"name" | "title" | "type"
|
||||
>[] = []
|
||||
for (const item of items) {
|
||||
if (item !== null && !/\d+$/.test(item.name)) {
|
||||
filtered.push({
|
||||
name: item.name,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
return [base.name, filtered] as const
|
||||
})
|
||||
)
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
// Re-export for server-side use.
|
||||
export { getThemesForBaseColor }
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export const ALLOWED_ITEM_TYPES = ["registry:block", "registry:example"]
|
||||
|
||||
export const EXCLUDED_ITEMS = ["component-example"]
|
||||
|
||||
@@ -5,12 +5,17 @@ import {
|
||||
Geist_Mono,
|
||||
Inter,
|
||||
JetBrains_Mono,
|
||||
Lora,
|
||||
Merriweather,
|
||||
Noto_Sans,
|
||||
Noto_Serif,
|
||||
Nunito_Sans,
|
||||
Outfit,
|
||||
Playfair_Display,
|
||||
Public_Sans,
|
||||
Raleway,
|
||||
Roboto,
|
||||
Roboto_Slab,
|
||||
} from "next/font/google"
|
||||
|
||||
const inter = Inter({
|
||||
@@ -73,6 +78,31 @@ const outfit = Outfit({
|
||||
variable: "--font-outfit",
|
||||
})
|
||||
|
||||
const notoSerif = Noto_Serif({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-noto-serif",
|
||||
})
|
||||
|
||||
const robotoSlab = Roboto_Slab({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-roboto-slab",
|
||||
})
|
||||
|
||||
const merriweather = Merriweather({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-merriweather",
|
||||
})
|
||||
|
||||
const lora = Lora({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-lora",
|
||||
})
|
||||
|
||||
const playfairDisplay = Playfair_Display({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-playfair-display",
|
||||
})
|
||||
|
||||
export const FONTS = [
|
||||
{
|
||||
name: "Geist",
|
||||
@@ -146,6 +176,36 @@ export const FONTS = [
|
||||
font: jetbrainsMono,
|
||||
type: "mono",
|
||||
},
|
||||
{
|
||||
name: "Noto Serif",
|
||||
value: "noto-serif",
|
||||
font: notoSerif,
|
||||
type: "serif",
|
||||
},
|
||||
{
|
||||
name: "Roboto Slab",
|
||||
value: "roboto-slab",
|
||||
font: robotoSlab,
|
||||
type: "serif",
|
||||
},
|
||||
{
|
||||
name: "Merriweather",
|
||||
value: "merriweather",
|
||||
font: merriweather,
|
||||
type: "serif",
|
||||
},
|
||||
{
|
||||
name: "Lora",
|
||||
value: "lora",
|
||||
font: lora,
|
||||
type: "serif",
|
||||
},
|
||||
{
|
||||
name: "Playfair Display",
|
||||
value: "playfair-display",
|
||||
font: playfairDisplay,
|
||||
type: "serif",
|
||||
},
|
||||
] as const
|
||||
|
||||
export type Font = (typeof FONTS)[number]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user