mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-08 22:45:47 +00:00
Compare commits
103 Commits
shadcn@4.8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
093a538453 | ||
|
|
5e800ca2bd | ||
|
|
f6a2647854 | ||
|
|
68e1f171aa | ||
|
|
dd6ce77cf1 | ||
|
|
628e427231 | ||
|
|
d8ace420ba | ||
|
|
4163c1dbea | ||
|
|
468674b1f0 | ||
|
|
b13050763e | ||
|
|
c4b22943fe | ||
|
|
34f0dabe48 | ||
|
|
5b5db1f8ea | ||
|
|
d0fae52822 | ||
|
|
2ef6388ca1 | ||
|
|
f3e7de1175 | ||
|
|
a409271270 | ||
|
|
70afde8358 | ||
|
|
574e85e22d | ||
|
|
3310c45dfb | ||
|
|
34223a842d | ||
|
|
dbf9c5ebc4 | ||
|
|
897e9add14 | ||
|
|
8d6553a7f5 | ||
|
|
683073f102 | ||
|
|
02e398ab73 | ||
|
|
af79276f7e | ||
|
|
5a3ad36a5e | ||
|
|
a63e8359ec | ||
|
|
a72491cb9b | ||
|
|
67aec7dcc5 | ||
|
|
40c7064532 | ||
|
|
5b3369f6ee | ||
|
|
b31e6d63b0 | ||
|
|
cf5b227565 | ||
|
|
8055a12f46 | ||
|
|
18fcf0f766 | ||
|
|
c520191cd4 | ||
|
|
35983528c2 | ||
|
|
8692cd4cc1 | ||
|
|
d3727d8c45 | ||
|
|
e3f98d49e4 | ||
|
|
549852bffc | ||
|
|
4139cff3a0 | ||
|
|
95471a0fb9 | ||
|
|
b59f68ecc5 | ||
|
|
6956a099b3 | ||
|
|
2417242b12 | ||
|
|
70a7a0c2a8 | ||
|
|
5602b81d83 | ||
|
|
3ffd3e1c7c | ||
|
|
e03df56fcf | ||
|
|
38fb1b6f41 | ||
|
|
13eb6a81d1 | ||
|
|
5cc8a2af42 | ||
|
|
365d53b590 | ||
|
|
c2ddedf5d2 | ||
|
|
c879483c96 | ||
|
|
4885a9a7ad | ||
|
|
2f5929269a | ||
|
|
2fdf1bb0d4 | ||
|
|
951750bdbe | ||
|
|
6c5d5d6374 | ||
|
|
6ec117715f | ||
|
|
e583c773fe | ||
|
|
0f154171a7 | ||
|
|
9197676b3d | ||
|
|
82dce7e945 | ||
|
|
35bc9934bf | ||
|
|
1fd75c9d7e | ||
|
|
2ea31d8070 | ||
|
|
ea9d371a2d | ||
|
|
d15f17d717 | ||
|
|
46ca8c5d4b | ||
|
|
a5eb279650 | ||
|
|
1994caba0b | ||
|
|
1450bea8d6 | ||
|
|
ced2a5beb5 | ||
|
|
10f1717a3e | ||
|
|
deba036d50 | ||
|
|
5bd81beebf | ||
|
|
3f2ff18157 | ||
|
|
05eb2b968b | ||
|
|
a721cc08e5 | ||
|
|
8da4592308 | ||
|
|
f47d48f316 | ||
|
|
e6d9d6023b | ||
|
|
7dfd933102 | ||
|
|
9c6a5ee1b1 | ||
|
|
c87897b2a5 | ||
|
|
c61197f627 | ||
|
|
a1fb619cef | ||
|
|
d84c4a8ca5 | ||
|
|
cd54e0927f | ||
|
|
adac7cae1f | ||
|
|
7c63c46736 | ||
|
|
916c012132 | ||
|
|
460ad60d84 | ||
|
|
8e2d2d1439 | ||
|
|
67cef8fcb9 | ||
|
|
4ff43ba694 | ||
|
|
efdec3ca45 | ||
|
|
5c849297d0 |
35
.github/collect-prerelease-info.js
vendored
Normal file
35
.github/collect-prerelease-info.js
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
// Collect the packages that a snapshot prerelease just published, so the
|
||||
// prerelease comment workflow can render an install line per package.
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
const [, , prNumber, channel] = process.argv
|
||||
|
||||
const packagesDir = join(process.cwd(), "packages")
|
||||
const published = []
|
||||
|
||||
for (const dir of readdirSync(packagesDir)) {
|
||||
const pkgPath = join(packagesDir, dir, "package.json")
|
||||
if (!existsSync(pkgPath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
|
||||
|
||||
// Snapshot versions are stamped `0.0.0-<channel>-<timestamp>`, so the channel
|
||||
// marker is how we tell which packages this run actually versioned.
|
||||
if (
|
||||
!pkg.private &&
|
||||
typeof pkg.version === "string" &&
|
||||
pkg.version.includes(`-${channel}`)
|
||||
) {
|
||||
published.push({ name: pkg.name, version: pkg.version })
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
"prerelease-info.json",
|
||||
JSON.stringify({ pr: prNumber, channel, packages: published }, null, 2)
|
||||
)
|
||||
|
||||
console.log(`Collected ${published.length} prerelease package(s).`)
|
||||
21
.github/version-script-beta.js
vendored
21
.github/version-script-beta.js
vendored
@@ -1,21 +0,0 @@
|
||||
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
|
||||
// https://github.com/cloudflare/wrangler2/blob/main/.github/version-script.js
|
||||
|
||||
import { exec } from "child_process"
|
||||
import fs from "fs"
|
||||
|
||||
const pkgJsonPath = "packages/shadcn/package.json"
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
|
||||
exec("git rev-parse --short HEAD", (err, stdout) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
process.exit(1)
|
||||
}
|
||||
pkg.version = "0.0.0-beta." + stdout.trim()
|
||||
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, "\t") + "\n")
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
56
.github/workflows/browser-tests.yml
vendored
Normal file
56
.github/workflows/browser-tests.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Browser tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
paths:
|
||||
- "packages/react/**"
|
||||
- ".github/workflows/browser-tests.yml"
|
||||
|
||||
jobs:
|
||||
browser:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm test:browser
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: pnpm --filter=@shadcn/react exec playwright install --with-deps chromium
|
||||
|
||||
- run: pnpm --filter=@shadcn/react test:browser
|
||||
4
.github/workflows/code-check.yml
vendored
4
.github/workflows/code-check.yml
vendored
@@ -78,7 +78,7 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm --filter=shadcn build
|
||||
run: pnpm build:packages
|
||||
|
||||
- run: pnpm format:check
|
||||
|
||||
@@ -117,6 +117,6 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm --filter=shadcn build
|
||||
run: pnpm build:packages
|
||||
|
||||
- run: pnpm typecheck
|
||||
|
||||
100
.github/workflows/prerelease-comment.yml
vendored
100
.github/workflows/prerelease-comment.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# Adapted from create-t3-app.
|
||||
name: Write Beta Release comment
|
||||
name: Write Prerelease comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
@@ -16,51 +16,79 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Write comment to the PR
|
||||
steps:
|
||||
- name: "Comment on PR"
|
||||
# Stable pushes and no-changeset runs upload no artifact, so a missing
|
||||
# download is expected — gate the rest of the job on it succeeding.
|
||||
- name: Download prerelease info
|
||||
id: download
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: prerelease-info
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build comment
|
||||
id: info
|
||||
if: steps.download.outcome == 'success'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
const fs = require("fs");
|
||||
const info = JSON.parse(fs.readFileSync("prerelease-info.json", "utf8"));
|
||||
|
||||
for (const artifact of allArtifacts.data.artifacts) {
|
||||
// Extract the PR number and package version from the artifact name
|
||||
const match = /^npm-package-shadcn@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||
|
||||
if (match) {
|
||||
require("fs").appendFileSync(
|
||||
process.env.GITHUB_ENV,
|
||||
`\nBETA_PACKAGE_VERSION=${match[1]}` +
|
||||
`\nWORKFLOW_RUN_PR=${match[2]}` +
|
||||
`\nWORKFLOW_RUN_ID=${context.payload.workflow_run.id}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (!info.packages || info.packages.length === 0) {
|
||||
core.info("No prerelease packages to comment.");
|
||||
return;
|
||||
}
|
||||
|
||||
- name: "Comment on PR with Link"
|
||||
const installs = info.packages
|
||||
.map((p) => `pnpm dlx ${p.name}@${p.version}`)
|
||||
.join("\n");
|
||||
const links = info.packages
|
||||
.map(
|
||||
(p) =>
|
||||
`- [${p.name}@${p.version}](https://www.npmjs.com/package/${p.name}/v/${p.version})`
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const body = [
|
||||
`A new ${info.channel} prerelease is available for testing:`,
|
||||
"",
|
||||
"```sh",
|
||||
installs,
|
||||
"```",
|
||||
"",
|
||||
links,
|
||||
].join("\n");
|
||||
|
||||
core.setOutput("pr", info.pr);
|
||||
core.setOutput("channel", info.channel);
|
||||
core.setOutput("body", body);
|
||||
|
||||
- name: Comment on PR
|
||||
if: steps.info.outputs.body
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
number: ${{ env.WORKFLOW_RUN_PR }}
|
||||
message: |
|
||||
A new prerelease is available for testing:
|
||||
number: ${{ steps.info.outputs.pr }}
|
||||
message: ${{ steps.info.outputs.body }}
|
||||
|
||||
```sh
|
||||
pnpm dlx shadcn@${{ env.BETA_PACKAGE_VERSION }}
|
||||
```
|
||||
|
||||
- name: "Remove the autorelease label once published"
|
||||
- name: Remove the prerelease label once published
|
||||
if: steps.info.outputs.pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: '${{ env.WORKFLOW_RUN_PR }}',
|
||||
name: '🚀 autorelease',
|
||||
});
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: Number("${{ steps.info.outputs.pr }}"),
|
||||
name: `release: ${{ steps.info.outputs.channel }}`,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
core.info("The prerelease label was already removed.");
|
||||
}
|
||||
|
||||
91
.github/workflows/release.yml
vendored
91
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
name: Release
|
||||
|
||||
run-name: ${{ github.event_name == 'pull_request' && format('Release Beta - PR {0}', github.event.number) || 'Release Stable' }}
|
||||
run-name: ${{ github.event_name == 'pull_request' && format('Release Prerelease - PR {0}', github.event.number) || 'Release Stable' }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -15,8 +15,8 @@ on:
|
||||
|
||||
jobs:
|
||||
prerelease:
|
||||
if: ${{ github.event_name == 'pull_request' && github.repository_owner == 'shadcn-ui' && contains(github.event.pull_request.labels.*.name, '🚀 autorelease') }}
|
||||
name: Publish Beta to NPM
|
||||
if: "${{ github.event_name == 'pull_request' && github.repository_owner == 'shadcn-ui' && (contains(github.event.pull_request.labels.*.name, 'release: beta') || contains(github.event.pull_request.labels.*.name, 'release: rc')) }}"
|
||||
name: Publish Prerelease to NPM
|
||||
runs-on: ubuntu-latest
|
||||
environment: Preview
|
||||
permissions:
|
||||
@@ -24,10 +24,36 @@ jobs:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Select prerelease channel
|
||||
id: prerelease
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prereleaseLabels = [
|
||||
{ name: "release: beta", channel: "beta" },
|
||||
{ name: "release: rc", channel: "rc" },
|
||||
];
|
||||
const labels = context.payload.pull_request.labels.map((label) => label.name);
|
||||
const selectedLabels = prereleaseLabels.filter((label) =>
|
||||
labels.includes(label.name)
|
||||
);
|
||||
|
||||
if (selectedLabels.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected exactly one prerelease label, found: ${
|
||||
selectedLabels.map((label) => label.name).join(", ") || "none"
|
||||
}.`
|
||||
);
|
||||
}
|
||||
|
||||
core.setOutput("channel", selectedLabels[0].channel);
|
||||
core.setOutput("label", selectedLabels[0].name);
|
||||
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Use PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -47,23 +73,49 @@ jobs:
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Modify package.json version
|
||||
run: node .github/version-script-beta.js
|
||||
# A snapshot prerelease needs changesets to compute versions. The
|
||||
# Changesets version PR consumes them, so a label on that PR is a no-op.
|
||||
- name: Check for changesets
|
||||
id: changesets
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
present=false
|
||||
for file in .changeset/*.md; do
|
||||
if [ "$(basename "$file")" != "README.md" ]; then
|
||||
present=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo "present=$present" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish Beta to NPM
|
||||
run: pnpm pub:beta
|
||||
- name: No changesets to prerelease
|
||||
if: steps.changesets.outputs.present == 'false'
|
||||
run: echo "::notice::No changesets found on this branch; nothing to prerelease."
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
path: packages/shadcn
|
||||
# Snapshot versions are stamped per run (timestamped), so each publish is
|
||||
# unique and can never collide with a real release on the latest tag.
|
||||
- name: Version snapshot
|
||||
if: steps.changesets.outputs.present == 'true'
|
||||
run: pnpm exec changeset version --snapshot ${{ steps.prerelease.outputs.channel }}
|
||||
|
||||
- name: Upload packaged artifact
|
||||
- name: Build packages
|
||||
if: steps.changesets.outputs.present == 'true'
|
||||
run: pnpm build:packages
|
||||
|
||||
- name: Publish snapshot to NPM
|
||||
if: steps.changesets.outputs.present == 'true'
|
||||
run: pnpm exec changeset publish --tag ${{ steps.prerelease.outputs.channel }} --no-git-tag
|
||||
|
||||
- name: Collect prerelease info
|
||||
if: steps.changesets.outputs.present == 'true'
|
||||
run: node .github/collect-prerelease-info.js "${{ github.event.number }}" "${{ steps.prerelease.outputs.channel }}"
|
||||
|
||||
- name: Upload prerelease info
|
||||
if: steps.changesets.outputs.present == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-package-shadcn@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/shadcn/dist/index.js
|
||||
name: prerelease-info
|
||||
path: prerelease-info.json
|
||||
|
||||
release:
|
||||
if: ${{ github.event_name == 'push' && github.repository_owner == 'shadcn-ui' }}
|
||||
@@ -98,11 +150,10 @@ jobs:
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
# - name: Check for errors
|
||||
# run: pnpm check
|
||||
|
||||
- name: Build the package
|
||||
run: pnpm shadcn:build
|
||||
# Builds every publishable package under packages/* (shadcn, @shadcn/react),
|
||||
# never apps/v4, so each dist is fresh before changeset publish.
|
||||
- name: Build the packages
|
||||
run: pnpm build:packages
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
|
||||
36
.github/workflows/test.yml
vendored
36
.github/workflows/test.yml
vendored
@@ -46,3 +46,39 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- run: pnpm test
|
||||
|
||||
react:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm test (@shadcn/react)
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 10.33.4
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- run: pnpm --filter=@shadcn/react test
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -45,4 +45,11 @@ tsconfig.tsbuildinfo
|
||||
.playwright-mcp
|
||||
.playwright-cli
|
||||
shadcn-workspace
|
||||
|
||||
# vitest browser mode writes these only on test failure.
|
||||
__screenshots__
|
||||
.codex-artifacts
|
||||
.tmp*
|
||||
|
||||
CONTEXT.md
|
||||
docs/adr
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -15,6 +15,7 @@
|
||||
"search.exclude": {
|
||||
"apps/v4/registry/radix-*": true,
|
||||
"apps/v4/public/r/*": true,
|
||||
"packages/shadcn/test/fixtures/*": true
|
||||
"packages/shadcn/test/fixtures/*": true,
|
||||
"apps/v4/styles/*": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,11 @@ When adding or modifying components, please ensure that:
|
||||
2. You update the documentation.
|
||||
3. You run `pnpm registry:build` to update the registry.
|
||||
|
||||
See [`apps/v4/registry/README.md`](apps/v4/registry/README.md) for how the
|
||||
registry pipeline is structured and for the faster targeted build modes
|
||||
(`--style`, `--registry`, `--examples`, `--indexes`) you can use while
|
||||
iterating locally. Always run the full `pnpm registry:build` before committing.
|
||||
|
||||
## Commit Convention
|
||||
|
||||
Before you create a Pull Request, please check whether your commits comply with
|
||||
|
||||
59
RELEASING.md
Normal file
59
RELEASING.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Releasing
|
||||
|
||||
This monorepo publishes two packages independently with [Changesets](https://github.com/changesets/changesets):
|
||||
|
||||
- **`shadcn`** — the CLI and tooling.
|
||||
- **`@shadcn/react`** — headless React primitives.
|
||||
|
||||
They version on their own lines. A change to one never bumps the other unless a changeset says so.
|
||||
|
||||
## 1. Add a changeset
|
||||
|
||||
Every change that should publish needs a changeset. Run:
|
||||
|
||||
```sh
|
||||
pnpm changeset
|
||||
```
|
||||
|
||||
Select the affected package(s) and bump level. One PR can carry separate changesets for `shadcn` and `@shadcn/react` at different levels. A PR with no changeset publishes nothing.
|
||||
|
||||
## 2. Stable release
|
||||
|
||||
Stable releases are automated by `.github/workflows/release.yml` (the `release` job, on push to `main`):
|
||||
|
||||
1. Merged changesets accumulate on `main`.
|
||||
2. The Changesets action opens/updates a **"Version Packages"** PR that bumps versions and writes changelogs.
|
||||
3. Merging that PR triggers `changeset publish`, which builds all packages (`pnpm build:packages`) and publishes any whose version is ahead of npm — each to the `latest` tag.
|
||||
|
||||
`pnpm build:packages` (`turbo run build --filter=./packages/*`) builds `shadcn` and `@shadcn/react` but never `apps/v4`.
|
||||
|
||||
## 3. Prereleases (per-PR snapshots)
|
||||
|
||||
Add the **`release: beta`** or **`release: rc`** label to a PR. The `prerelease` job in `release.yml`:
|
||||
|
||||
1. Verifies the branch has changesets (a label on the version PR is a no-op, since it consumed them).
|
||||
2. Runs `changeset version --snapshot <channel>` — stamps a unique `0.0.0-<channel>-<timestamp>` on each changeset'd package.
|
||||
3. Builds and runs `changeset publish --tag <channel> --no-git-tag`.
|
||||
4. Uploads the published package list; `prerelease-comment.yml` posts a `pnpm dlx` install line per package and removes the label.
|
||||
|
||||
The label selects the **dist-tag/channel**; the **changesets on the branch** select which packages publish. Snapshots are timestamped, so they never touch `latest` and never collide.
|
||||
|
||||
```sh
|
||||
# Install a snapshot from the PR comment, e.g.:
|
||||
pnpm dlx @shadcn/react@0.0.0-beta-20260624120000
|
||||
```
|
||||
|
||||
## 4. Prerelease trains (sustained `-beta.N` / `-rc.N`)
|
||||
|
||||
For a baking release line (e.g. `1.0.0-rc.0`, `-rc.1`, …) rather than throwaway snapshots, use Changesets pre mode:
|
||||
|
||||
```sh
|
||||
pnpm changeset pre enter rc # writes .changeset/pre.json
|
||||
# ...normal changeset + Version PR cycle now produces -rc.N versions on the rc tag...
|
||||
pnpm changeset pre exit # back to stable; next Version PR ships X.Y.Z on latest
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `pnpm-workspace.yaml` sets `minimumReleaseAge: 2880` (48h), so freshly published stable/beta versions take time to resolve in normal installs. Use `pnpm dlx <pkg>@<exact-snapshot-version>` to test immediately.
|
||||
- Publishing uses npm OIDC/provenance (`id-token: write` + `npm@latest`); no `NPM_TOKEN` secret is needed.
|
||||
1
apps/v4/.gitignore
vendored
1
apps/v4/.gitignore
vendored
@@ -46,3 +46,4 @@ next-env.d.ts
|
||||
.contentlayer
|
||||
.content-collections
|
||||
.source
|
||||
.devtools
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function MenuAccentPicker({
|
||||
isMobile,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/styles/base-nova/ui/command"
|
||||
import { useActionMenu } from "@/app/(app)/create/hooks/use-action-menu"
|
||||
import { useActionMenu } from "@/app/(app)/(create)/hooks/use-action-menu"
|
||||
|
||||
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function BaseColorPicker({
|
||||
isMobile,
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function BasePicker({
|
||||
isMobile,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
getThemesForBaseColor,
|
||||
type ChartColorName,
|
||||
} from "@/registry/config"
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function ChartColorPicker({
|
||||
isMobile,
|
||||
@@ -46,12 +46,6 @@ export function ChartColorPicker({
|
||||
[params.chartColor]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentChartColor && availableChartColors.length > 0) {
|
||||
setParams({ chartColor: availableChartColors[0].name })
|
||||
}
|
||||
}, [currentChartColor, availableChartColors, setParams])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
@@ -5,7 +5,7 @@ import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
||||
import { usePresetCode } from "@/app/(app)/(create)/hooks/use-design-system"
|
||||
|
||||
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
||||
const presetCode = usePresetCode()
|
||||
72
apps/v4/app/(app)/(create)/components/create-devtools.tsx
Normal file
72
apps/v4/app/(app)/(create)/components/create-devtools.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { BASES } from "@/registry/config"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { getDocsPathForItem } from "@/app/(app)/(create)/lib/devtools"
|
||||
import {
|
||||
serializeDesignSystemSearchParams,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function CreateDevtools() {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const previewUrl = React.useMemo(
|
||||
() =>
|
||||
serializeDesignSystemSearchParams(
|
||||
`/preview/${params.base}/${params.item}`,
|
||||
params
|
||||
),
|
||||
[params]
|
||||
)
|
||||
|
||||
const docsUrl = React.useMemo(
|
||||
() => getDocsPathForItem(params.base, params.item),
|
||||
[params.base, params.item]
|
||||
)
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dark absolute bottom-3 left-1/2 z-20 flex -translate-x-1/2 items-center gap-0.5 rounded-xl bg-card/90 p-1 shadow-xl backdrop-blur-xl">
|
||||
{BASES.map((base) => (
|
||||
<Button
|
||||
key={base.name}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-active={params.base === base.name}
|
||||
className="h-7 min-w-8 cursor-pointer rounded-lg px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
|
||||
onClick={() => setParams({ base: base.name })}
|
||||
>
|
||||
{base.name === "radix" ? "Radix" : "Base"}
|
||||
</Button>
|
||||
))}
|
||||
<div className="mx-0.5 h-4 w-px bg-border/80" />
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 rounded-lg px-2.5 text-xs font-medium text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<a href={previewUrl} target="_blank" rel="noreferrer">
|
||||
Open in New Tab
|
||||
</a>
|
||||
</Button>
|
||||
<div className="mx-0.5 h-4 w-px bg-border/80" />
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 rounded-lg px-2.5 text-xs font-medium text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<a href={docsUrl} target="_blank" rel="noreferrer">
|
||||
Open Docs
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
apps/v4/app/(app)/(create)/components/create-skeleton.tsx
Normal file
15
apps/v4/app/(app)/(create)/components/create-skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
|
||||
|
||||
export function CreateSkeleton() {
|
||||
return (
|
||||
<div className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden section-soft [--customizer-width:--spacing(48)] [--gap:--spacing(4)] md:[--gap:--spacing(6)] 2xl:[--customizer-width:--spacing(56)]">
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<Skeleton className="flex-1 rounded-2xl" />
|
||||
<Skeleton className="min-h-[151px] w-full self-start rounded-2xl md:h-full md:max-h-full md:min-h-0 md:w-(--customizer-width)" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { getThemesForBaseColor, STYLES } from "@/registry/config"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -13,30 +14,41 @@ import {
|
||||
CardHeader,
|
||||
} from "@/styles/base-nova/ui/card"
|
||||
import { FieldGroup, FieldSeparator } from "@/styles/base-nova/ui/field"
|
||||
import { MenuAccentPicker } from "@/app/(app)/create/components/accent-picker"
|
||||
import { ActionMenu } from "@/app/(app)/create/components/action-menu"
|
||||
import { BaseColorPicker } from "@/app/(app)/create/components/base-color-picker"
|
||||
import { BasePicker } from "@/app/(app)/create/components/base-picker"
|
||||
import { ChartColorPicker } from "@/app/(app)/create/components/chart-color-picker"
|
||||
import { CopyPreset } from "@/app/(app)/create/components/copy-preset"
|
||||
import { FontPicker } from "@/app/(app)/create/components/font-picker"
|
||||
import { IconLibraryPicker } from "@/app/(app)/create/components/icon-library-picker"
|
||||
import { MainMenu } from "@/app/(app)/create/components/main-menu"
|
||||
import { MenuColorPicker } from "@/app/(app)/create/components/menu-picker"
|
||||
import { OpenPreset } from "@/app/(app)/create/components/open-preset"
|
||||
import { RadiusPicker } from "@/app/(app)/create/components/radius-picker"
|
||||
import { RandomButton } from "@/app/(app)/create/components/random-button"
|
||||
import { ResetDialog } from "@/app/(app)/create/components/reset-button"
|
||||
import { StylePicker } from "@/app/(app)/create/components/style-picker"
|
||||
import { ThemePicker } from "@/app/(app)/create/components/theme-picker"
|
||||
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/create/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { MenuAccentPicker } from "@/app/(app)/(create)/components/accent-picker"
|
||||
import { ActionMenu } from "@/app/(app)/(create)/components/action-menu"
|
||||
import { BaseColorPicker } from "@/app/(app)/(create)/components/base-color-picker"
|
||||
import { BasePicker } from "@/app/(app)/(create)/components/base-picker"
|
||||
import { ChartColorPicker } from "@/app/(app)/(create)/components/chart-color-picker"
|
||||
import { CopyPreset } from "@/app/(app)/(create)/components/copy-preset"
|
||||
import { FontPicker } from "@/app/(app)/(create)/components/font-picker"
|
||||
import { IconLibraryPicker } from "@/app/(app)/(create)/components/icon-library-picker"
|
||||
import { MainMenu } from "@/app/(app)/(create)/components/main-menu"
|
||||
import { MenuColorPicker } from "@/app/(app)/(create)/components/menu-picker"
|
||||
import { OpenPreset } from "@/app/(app)/(create)/components/open-preset"
|
||||
import { RadiusPicker } from "@/app/(app)/(create)/components/radius-picker"
|
||||
import { RandomButton } from "@/app/(app)/(create)/components/random-button"
|
||||
import { ResetDialog } from "@/app/(app)/(create)/components/reset-button"
|
||||
import { StylePicker } from "@/app/(app)/(create)/components/style-picker"
|
||||
import { ThemePicker } from "@/app/(app)/(create)/components/theme-picker"
|
||||
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
// Only visible when user clicks "Create Project".
|
||||
const ProjectForm = dynamic(() =>
|
||||
import("@/app/(app)/create/components/project-form").then(
|
||||
(m) => m.ProjectForm
|
||||
)
|
||||
// Only visible when user clicks "Create Project". Rendered client-only to
|
||||
// avoid a useId hydration mismatch on the Base UI dialog trigger. The loading
|
||||
// placeholder mirrors the trigger button exactly so there is no layout shift.
|
||||
const ProjectForm = dynamic(
|
||||
() =>
|
||||
import("@/app/(app)/(create)/components/project-form").then(
|
||||
(m) => m.ProjectForm
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<Button disabled aria-hidden>
|
||||
Get Code
|
||||
</Button>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
export function Customizer({
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
POINTER_CURSOR_SELECTOR,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
import { useIframeMessageListener } from "@/app/(app)/create/hooks/use-iframe-sync"
|
||||
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
||||
import { useIframeMessageListener } from "@/app/(app)/(create)/hooks/use-iframe-sync"
|
||||
import { FONTS } from "@/app/(app)/(create)/lib/fonts"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
|
||||
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { FONTS } from "@/app/(app)/(create)/lib/fonts"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
type FontPickerOption = {
|
||||
name: string
|
||||
@@ -5,7 +5,7 @@ import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { useHistory } from "@/app/(app)/create/hooks/use-history"
|
||||
import { useHistory } from "@/app/(app)/(create)/hooks/use-history"
|
||||
|
||||
export const UNDO_FORWARD_TYPE = "undo-forward"
|
||||
export const REDO_FORWARD_TYPE = "redo-forward"
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { iconLibraries, type IconLibraryName } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
const logos = {
|
||||
lucide: (
|
||||
@@ -4,7 +4,7 @@ import { lazy, Suspense } from "react"
|
||||
import { SquareIcon } from "lucide-react"
|
||||
import type { IconLibraryName } from "shadcn/icons"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
const IconLucide = lazy(() =>
|
||||
import("@/registry/icons/icon-lucide").then((mod) => ({
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/styles/base-nova/ui/sidebar"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(app)/(create)/lib/utils"
|
||||
|
||||
const cachedGroupedItems = React.cache(
|
||||
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
|
||||
@@ -10,7 +10,7 @@ import { cn } from "@/lib/utils"
|
||||
import {
|
||||
useLocks,
|
||||
type LockableParam,
|
||||
} from "@/app/(app)/create/hooks/use-locks"
|
||||
} from "@/app/(app)/(create)/hooks/use-locks"
|
||||
|
||||
export function LockButton({
|
||||
param,
|
||||
@@ -14,13 +14,13 @@ import {
|
||||
PickerSeparator,
|
||||
PickerShortcut,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useActionMenuTrigger } from "@/app/(app)/create/hooks/use-action-menu"
|
||||
import { useHistory } from "@/app/(app)/create/hooks/use-history"
|
||||
import { useOpenPresetTrigger } from "@/app/(app)/create/hooks/use-open-preset"
|
||||
import { useRandom } from "@/app/(app)/create/hooks/use-random"
|
||||
import { useReset } from "@/app/(app)/create/hooks/use-reset"
|
||||
import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useActionMenuTrigger } from "@/app/(app)/(create)/hooks/use-action-menu"
|
||||
import { useHistory } from "@/app/(app)/(create)/hooks/use-history"
|
||||
import { useOpenPresetTrigger } from "@/app/(app)/(create)/hooks/use-open-preset"
|
||||
import { useRandom } from "@/app/(app)/(create)/hooks/use-random"
|
||||
import { useReset } from "@/app/(app)/(create)/hooks/use-reset"
|
||||
import { useThemeToggle } from "@/app/(app)/(create)/hooks/use-theme-toggle"
|
||||
|
||||
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTheme } from "next-themes"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import { type MenuColorValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import {
|
||||
isTranslucentMenuColor,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
type ColorChoice = "default" | "inverted"
|
||||
type SurfaceChoice = "solid" | "translucent"
|
||||
@@ -5,7 +5,7 @@ import Script from "next/script"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle"
|
||||
import { useThemeToggle } from "@/app/(app)/(create)/hooks/use-theme-toggle"
|
||||
|
||||
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
|
||||
|
||||
@@ -31,9 +31,9 @@ import { Input } from "@/styles/base-nova/ui/input"
|
||||
import {
|
||||
OPEN_PRESET_FORWARD_TYPE,
|
||||
useOpenPreset,
|
||||
} from "@/app/(app)/create/hooks/use-open-preset"
|
||||
import { parsePresetInput } from "@/app/(app)/create/lib/parse-preset-input"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/hooks/use-open-preset"
|
||||
import { parsePresetInput } from "@/app/(app)/(create)/lib/parse-preset-input"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
const PRESET_EXAMPLE = "b2D0wqNxT"
|
||||
const PRESET_TITLE = "Open Preset"
|
||||
@@ -111,11 +111,12 @@ export function OpenPreset({
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={handleOpenChange}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="outline" className={triggerClassName}>
|
||||
{label}
|
||||
</Button>
|
||||
<DrawerTrigger
|
||||
render={<Button variant="outline" className={triggerClassName} />}
|
||||
>
|
||||
{label}
|
||||
</DrawerTrigger>
|
||||
|
||||
<DrawerContent className="dark rounded-t-2xl!">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle className="text-xl">{PRESET_TITLE}</DrawerTitle>
|
||||
@@ -127,10 +128,12 @@ export function OpenPreset({
|
||||
<Button type="submit" className="h-10" disabled={!nextPreset}>
|
||||
Open
|
||||
</Button>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline" type="button" className="h-10">
|
||||
Cancel
|
||||
</Button>
|
||||
<DrawerClose
|
||||
render={
|
||||
<Button variant="outline" type="button" className="h-10" />
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</form>
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
||||
|
||||
import { cn } from "@/registry/bases/base/lib/utils"
|
||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
import { IconPlaceholder } from "@/app/(app)/(create)/components/icon-placeholder"
|
||||
|
||||
function Picker({ ...props }: MenuPrimitive.Root.Props) {
|
||||
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { generateRandomPreset, isPresetCode } from "shadcn/preset"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function PresetHandler() {
|
||||
const router = useRouter()
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function PresetPicker({
|
||||
presets,
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
const PREVIEW_ITEMS = [
|
||||
{ label: "01", value: "preview-02" },
|
||||
@@ -2,21 +2,22 @@
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { CMD_K_FORWARD_TYPE } from "@/app/(app)/create/components/action-menu"
|
||||
import { CMD_K_FORWARD_TYPE } from "@/app/(app)/(create)/components/action-menu"
|
||||
import { CreateDevtools } from "@/app/(app)/(create)/components/create-devtools"
|
||||
import {
|
||||
REDO_FORWARD_TYPE,
|
||||
UNDO_FORWARD_TYPE,
|
||||
} from "@/app/(app)/create/components/history-buttons"
|
||||
import { DARK_MODE_FORWARD_TYPE } from "@/app/(app)/create/components/mode-switcher"
|
||||
import { PreviewSwitcher } from "@/app/(app)/create/components/preview-switcher"
|
||||
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(app)/create/components/random-button"
|
||||
import { sendToIframe } from "@/app/(app)/create/hooks/use-iframe-sync"
|
||||
import { OPEN_PRESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-open-preset"
|
||||
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
|
||||
} from "@/app/(app)/(create)/components/history-buttons"
|
||||
import { DARK_MODE_FORWARD_TYPE } from "@/app/(app)/(create)/components/mode-switcher"
|
||||
import { PreviewSwitcher } from "@/app/(app)/(create)/components/preview-switcher"
|
||||
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(app)/(create)/components/random-button"
|
||||
import { sendToIframe } from "@/app/(app)/(create)/hooks/use-iframe-sync"
|
||||
import { OPEN_PRESET_FORWARD_TYPE } from "@/app/(app)/(create)/hooks/use-open-preset"
|
||||
import { RESET_FORWARD_TYPE } from "@/app/(app)/(create)/hooks/use-reset"
|
||||
import {
|
||||
serializeDesignSystemSearchParams,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
|
||||
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
|
||||
@@ -160,6 +161,7 @@ export function Preview() {
|
||||
title="Preview"
|
||||
/>
|
||||
</div>
|
||||
<CreateDevtools />
|
||||
<PreviewSwitcher />
|
||||
</div>
|
||||
)
|
||||
@@ -50,17 +50,17 @@ import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/styles/base-nova/ui/toggle-group"
|
||||
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
||||
import { usePresetCode } from "@/app/(app)/(create)/hooks/use-design-system"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
import {
|
||||
getFramework,
|
||||
getTemplateValue,
|
||||
NO_MONOREPO_FRAMEWORKS,
|
||||
TEMPLATES,
|
||||
} from "@/app/(app)/create/lib/templates"
|
||||
} from "@/app/(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>'
|
||||
@@ -148,7 +148,8 @@ export function ProjectForm({
|
||||
|
||||
const commands = React.useMemo(() => {
|
||||
const presetFlag = ` --preset ${presetCode}`
|
||||
const baseFlag = params.base !== "radix" ? ` --base ${params.base}` : ""
|
||||
const baseFlag =
|
||||
params.base !== DEFAULT_CONFIG.base ? ` --base ${params.base}` : ""
|
||||
const templateFlag = ` --template ${framework}`
|
||||
const monorepoFlag = isMonorepo ? " --monorepo" : ""
|
||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { RADII, type RadiusValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function RadiusPicker({
|
||||
isMobile,
|
||||
@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { useRandom } from "@/app/(app)/create/hooks/use-random"
|
||||
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
|
||||
import { useRandom } from "@/app/(app)/(create)/hooks/use-random"
|
||||
import { RESET_FORWARD_TYPE } from "@/app/(app)/(create)/hooks/use-reset"
|
||||
|
||||
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/styles/base-nova/ui/alert-dialog"
|
||||
import { useReset } from "@/app/(app)/create/hooks/use-reset"
|
||||
import { useReset } from "@/app/(app)/(create)/hooks/use-reset"
|
||||
|
||||
export function ResetDialog() {
|
||||
const { showResetDialog, setShowResetDialog, confirmReset } = useReset()
|
||||
@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { usePresetCode } from "@/app/(app)/(create)/hooks/use-design-system"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function ShareButton() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { type Style, type StyleName } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function StylePicker({
|
||||
styles,
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import { LockButton } from "@/app/(app)/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(app)/create/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function ThemePicker({
|
||||
themes,
|
||||
@@ -38,12 +38,6 @@ export function ThemePicker({
|
||||
[params.theme]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentTheme && themes.length > 0) {
|
||||
setParams({ theme: themes[0].name })
|
||||
}
|
||||
}, [currentTheme, themes, setParams])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
@@ -8,7 +8,7 @@ import { useMounted } from "@/hooks/use-mounted"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
export function V0Button({ className }: { className?: string }) {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
22
apps/v4/app/(app)/(create)/create/layout.tsx
Normal file
22
apps/v4/app/(app)/(create)/create/layout.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { CreateSkeleton } from "@/app/(app)/(create)/components/create-skeleton"
|
||||
import { HistoryProvider } from "@/app/(app)/(create)/hooks/use-history"
|
||||
import { LocksProvider } from "@/app/(app)/(create)/hooks/use-locks"
|
||||
|
||||
export default function CreateLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<LocksProvider>
|
||||
{/* HistoryProvider reads useSearchParams(), which opts this subtree out
|
||||
of prerendering up to this boundary. The fallback is what paints on
|
||||
first load, so it must be the page placeholder, not empty. */}
|
||||
<Suspense fallback={<CreateSkeleton />}>
|
||||
<HistoryProvider>{children}</HistoryProvider>
|
||||
</Suspense>
|
||||
</LocksProvider>
|
||||
)
|
||||
}
|
||||
5
apps/v4/app/(app)/(create)/create/loading.tsx
Normal file
5
apps/v4/app/(app)/(create)/create/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CreateSkeleton } from "@/app/(app)/(create)/components/create-skeleton"
|
||||
|
||||
export default function CreateLoading() {
|
||||
return <CreateSkeleton />
|
||||
}
|
||||
@@ -5,14 +5,14 @@ import dynamic from "next/dynamic"
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
|
||||
import { Customizer } from "@/app/(app)/create/components/customizer"
|
||||
import { PresetHandler } from "@/app/(app)/create/components/preset-handler"
|
||||
import { Preview } from "@/app/(app)/create/components/preview"
|
||||
import { getAllItems } from "@/app/(app)/create/lib/api"
|
||||
import { Customizer } from "@/app/(app)/(create)/components/customizer"
|
||||
import { PresetHandler } from "@/app/(app)/(create)/components/preset-handler"
|
||||
import { Preview } from "@/app/(app)/(create)/components/preview"
|
||||
import { getAllItems } from "@/app/(app)/(create)/lib/api"
|
||||
|
||||
// Only shown on first visit (checks localStorage).
|
||||
const WelcomeDialog = dynamic(() =>
|
||||
import("@/app/(app)/create/components/welcome-dialog").then(
|
||||
import("@/app/(app)/(create)/components/welcome-dialog").then(
|
||||
(m) => m.WelcomeDialog
|
||||
)
|
||||
)
|
||||
@@ -4,8 +4,8 @@ import * as React from "react"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
import useSWR from "swr"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(app)/(create)/lib/utils"
|
||||
|
||||
const ACTION_MENU_OPEN_KEY = "create:action-menu-open"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
// Returns the canonical preset code derived from the current search params.
|
||||
export function usePresetCode() {
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { Suspense } from "react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
type HistoryContextValue = {
|
||||
canGoBack: boolean
|
||||
@@ -15,6 +20,9 @@ const HistoryContext = React.createContext<HistoryContextValue | null>(null)
|
||||
|
||||
// Reads useSearchParams() in its own Suspense boundary so the
|
||||
// provider never blanks out children while search params resolve.
|
||||
// useSearchParams reflects the *settled* preset, which coalesces the
|
||||
// transient values nuqs emits mid-update — reading nuqs state directly
|
||||
// here would record those transients as phantom history entries.
|
||||
function PresetSync({
|
||||
onPresetChange,
|
||||
}: {
|
||||
@@ -31,7 +39,11 @@ function PresetSync({
|
||||
}
|
||||
|
||||
export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
// Write through the shared search-params hook (shallow) instead of
|
||||
// router.replace. router.replace triggers a full server navigation that
|
||||
// resets the preset on prod — the same failure the customizer avoids by
|
||||
// going shallow (#11060).
|
||||
const [, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const [preset, setPreset] = React.useState("")
|
||||
|
||||
@@ -81,17 +93,16 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
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 pathname = window.location.pathname
|
||||
const query = params.toString()
|
||||
router.replace(query ? `${pathname}?${query}` : pathname)
|
||||
}, [router])
|
||||
// The first history entry is "" (no preset in the URL); null clears the
|
||||
// param to restore that state. nuqs accepts null to clear a key, which the
|
||||
// wrapper's input type does not model.
|
||||
setParams(
|
||||
{
|
||||
preset: entriesRef.current[nextIndex] || null,
|
||||
} as Partial<DesignSystemSearchParams>,
|
||||
{ history: "replace" }
|
||||
)
|
||||
}, [setParams])
|
||||
|
||||
const goForward = React.useCallback(() => {
|
||||
if (indexRef.current >= maxIndexRef.current) {
|
||||
@@ -103,17 +114,16 @@ export function HistoryProvider({ children }: { children: React.ReactNode }) {
|
||||
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 pathname = window.location.pathname
|
||||
const query = params.toString()
|
||||
router.replace(query ? `${pathname}?${query}` : pathname)
|
||||
}, [router])
|
||||
// The first history entry is "" (no preset in the URL); null clears the
|
||||
// param to restore that state. nuqs accepts null to clear a key, which the
|
||||
// wrapper's input type does not model.
|
||||
setParams(
|
||||
{
|
||||
preset: entriesRef.current[nextIndex] || null,
|
||||
} as Partial<DesignSystemSearchParams>,
|
||||
{ history: "replace" }
|
||||
)
|
||||
}, [setParams])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import type { DesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import type { DesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
type ParentToIframeMessage = {
|
||||
type: "design-system-params"
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { decodePreset } from "shadcn/preset"
|
||||
|
||||
import {
|
||||
BASE_COLORS,
|
||||
@@ -10,19 +11,30 @@ import {
|
||||
MENU_COLORS,
|
||||
RADII,
|
||||
STYLES,
|
||||
type BaseColorName,
|
||||
type ChartColorName,
|
||||
type FontHeadingValue,
|
||||
type FontValue,
|
||||
type IconLibraryName,
|
||||
type MenuAccentValue,
|
||||
type MenuColorValue,
|
||||
type RadiusValue,
|
||||
type StyleName,
|
||||
type ThemeName,
|
||||
} from "@/registry/config"
|
||||
import { useLocks } from "@/app/(app)/create/hooks/use-locks"
|
||||
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
||||
import { useLocks } from "@/app/(app)/(create)/hooks/use-locks"
|
||||
import { FONTS } from "@/app/(app)/(create)/lib/fonts"
|
||||
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
|
||||
import {
|
||||
applyBias,
|
||||
RANDOMIZE_BIASES,
|
||||
type RandomizeContext,
|
||||
} from "@/app/(app)/create/lib/randomize-biases"
|
||||
} from "@/app/(app)/(create)/lib/randomize-biases"
|
||||
import {
|
||||
isTranslucentMenuColor,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(app)/create/lib/search-params"
|
||||
} from "@/app/(app)/(create)/lib/search-params"
|
||||
import { SHUFFLE_PRESETS } from "@/app/(app)/(create)/lib/shuffle-presets"
|
||||
|
||||
function randomItem<T>(array: readonly T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
@@ -37,7 +49,9 @@ export function useRandom() {
|
||||
paramsRef.current = params
|
||||
}, [params])
|
||||
|
||||
const randomize = React.useCallback(() => {
|
||||
// True-random implementation. Kept, but shuffle is now wired to the
|
||||
// curated preset list below.
|
||||
const randomizeTrueRandom = React.useCallback(() => {
|
||||
const selectedStyle = locks.has("style")
|
||||
? paramsRef.current.style
|
||||
: randomItem(STYLES).name
|
||||
@@ -149,6 +163,60 @@ export function useRandom() {
|
||||
setParams(nextParams)
|
||||
}, [setParams, locks])
|
||||
|
||||
// Picks a random preset from the curated list instead of true random.
|
||||
// Locked params are preserved over the decoded preset values.
|
||||
const randomize = React.useCallback(() => {
|
||||
// Avoid re-picking the preset that is currently applied.
|
||||
const currentCode = getPresetCode(paramsRef.current)
|
||||
const availableCodes = SHUFFLE_PRESETS.filter(
|
||||
(code) => code !== currentCode
|
||||
)
|
||||
const decoded = decodePreset(
|
||||
randomItem(availableCodes.length > 0 ? availableCodes : SHUFFLE_PRESETS)
|
||||
)
|
||||
|
||||
if (!decoded) {
|
||||
return
|
||||
}
|
||||
|
||||
const current = paramsRef.current
|
||||
const nextParams = {
|
||||
style: locks.has("style") ? current.style : (decoded.style as StyleName),
|
||||
baseColor: locks.has("baseColor")
|
||||
? current.baseColor
|
||||
: (decoded.baseColor as BaseColorName),
|
||||
theme: locks.has("theme") ? current.theme : (decoded.theme as ThemeName),
|
||||
chartColor: locks.has("chartColor")
|
||||
? current.chartColor
|
||||
: ((decoded.chartColor ?? decoded.theme) as ChartColorName),
|
||||
iconLibrary: locks.has("iconLibrary")
|
||||
? current.iconLibrary
|
||||
: (decoded.iconLibrary as IconLibraryName),
|
||||
font: locks.has("font") ? current.font : (decoded.font as FontValue),
|
||||
fontHeading: locks.has("fontHeading")
|
||||
? current.fontHeading
|
||||
: (decoded.fontHeading as FontHeadingValue),
|
||||
menuAccent: locks.has("menuAccent")
|
||||
? current.menuAccent
|
||||
: (decoded.menuAccent as MenuAccentValue),
|
||||
menuColor: locks.has("menuColor")
|
||||
? current.menuColor
|
||||
: (decoded.menuColor as MenuColorValue),
|
||||
radius: locks.has("radius")
|
||||
? current.radius
|
||||
: (decoded.radius as RadiusValue),
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -177,5 +245,5 @@ export function useRandom() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { randomize }
|
||||
return { randomize, randomizeTrueRandom }
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
||||
import useSWR from "swr"
|
||||
|
||||
import { DEFAULT_CONFIG, PRESETS } from "@/registry/config"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/(create)/lib/search-params"
|
||||
|
||||
const RESET_DIALOG_KEY = "create:reset-dialog-open"
|
||||
export const RESET_FORWARD_TYPE = "reset-forward"
|
||||
@@ -1,9 +1,8 @@
|
||||
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"
|
||||
import { buildInstructions } from "@/app/(app)/(create)/lib/build-instructions"
|
||||
import { parseDesignSystemConfig } from "@/app/(app)/(create)/lib/parse-config"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
buildRegistryBase,
|
||||
parseRegistryBaseParts,
|
||||
} from "@/registry/config"
|
||||
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||
import { parseDesignSystemConfig } from "@/app/(app)/(create)/lib/parse-config"
|
||||
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -2,9 +2,9 @@ import { after, NextResponse, type NextRequest } from "next/server"
|
||||
import { track } from "@vercel/analytics/server"
|
||||
import { isPresetCode } from "shadcn/preset"
|
||||
|
||||
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||
import { buildV0Payload } from "@/app/(app)/create/lib/v0"
|
||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||
import { parseDesignSystemConfig } from "@/app/(app)/(create)/lib/parse-config"
|
||||
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
|
||||
import { buildV0Payload } from "@/app/(app)/(create)/lib/v0"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -6,7 +6,7 @@ import { BASES, getThemesForBaseColor, type BaseName } from "@/registry/config"
|
||||
import {
|
||||
ALLOWED_ITEM_TYPES,
|
||||
EXCLUDED_ITEMS,
|
||||
} from "@/app/(app)/create/lib/constants"
|
||||
} from "@/app/(app)/(create)/lib/constants"
|
||||
|
||||
export async function getItemsForBase(base: BaseName) {
|
||||
const { Index } = await import("@/registry/bases/__index__")
|
||||
@@ -42,7 +42,8 @@ export async function getBaseComponent(name: string, base: BaseName) {
|
||||
return null
|
||||
}
|
||||
|
||||
return index[name].component
|
||||
const { Components } = await import("@/registry/bases/__components__")
|
||||
return Components[base]?.[name] ?? null
|
||||
}
|
||||
|
||||
export async function getAllItems() {
|
||||
10
apps/v4/app/(app)/(create)/lib/devtools.ts
Normal file
10
apps/v4/app/(app)/(create)/lib/devtools.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { type BaseName } from "@/registry/config"
|
||||
|
||||
export function getDocsPathForItem(base: BaseName, item: string) {
|
||||
if (item.endsWith("-example")) {
|
||||
const component = item.slice(0, -"-example".length)
|
||||
return `/docs/components/${base}/${component}`
|
||||
}
|
||||
|
||||
return "/docs/components"
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
designSystemConfigSchema,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
import { resolvePresetOverrides } from "@/app/(app)/create/lib/preset-query"
|
||||
import { resolvePresetOverrides } from "@/app/(app)/(create)/lib/preset-query"
|
||||
|
||||
// Parses design system config from URL search params.
|
||||
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||
@@ -20,7 +20,7 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||
configInput = {
|
||||
...decoded,
|
||||
...presetOverrides,
|
||||
base: searchParams.get("base") ?? "radix",
|
||||
base: searchParams.get("base") ?? "base",
|
||||
template: searchParams.get("template") ?? undefined,
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
pointer: searchParams.get("pointer") === "true",
|
||||
53
apps/v4/app/(app)/(create)/lib/search-params.test.ts
Normal file
53
apps/v4/app/(app)/(create)/lib/search-params.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { DEFAULT_CONFIG } from "@/registry/config"
|
||||
|
||||
import { buildPresetUrlUpdate } from "./search-params"
|
||||
|
||||
describe("buildPresetUrlUpdate", () => {
|
||||
it("encodes design system params into preset and clears individual keys", () => {
|
||||
const update = buildPresetUrlUpdate({
|
||||
...DEFAULT_CONFIG,
|
||||
preset: "b0",
|
||||
template: "next",
|
||||
item: "preview-02",
|
||||
size: 100,
|
||||
custom: false,
|
||||
})
|
||||
|
||||
expect(update.preset).toBeTypeOf("string")
|
||||
expect(update.style).toBeNull()
|
||||
expect(update.theme).toBeNull()
|
||||
expect(update.baseColor).toBeNull()
|
||||
})
|
||||
|
||||
it("preserves template when syncing preset from ?template=start", () => {
|
||||
const update = buildPresetUrlUpdate({
|
||||
...DEFAULT_CONFIG,
|
||||
preset: "b0",
|
||||
template: "start",
|
||||
item: "preview-02",
|
||||
size: 100,
|
||||
custom: false,
|
||||
})
|
||||
|
||||
expect(update.template).toBe("start")
|
||||
expect(update.preset).toBeTypeOf("string")
|
||||
})
|
||||
|
||||
it("applies non-design-system updates from resolvedUpdates", () => {
|
||||
const update = buildPresetUrlUpdate(
|
||||
{
|
||||
...DEFAULT_CONFIG,
|
||||
preset: "b0",
|
||||
template: "next",
|
||||
item: "preview-02",
|
||||
size: 100,
|
||||
custom: false,
|
||||
},
|
||||
{ template: "vite" }
|
||||
)
|
||||
|
||||
expect(update.template).toBe("vite")
|
||||
})
|
||||
})
|
||||
@@ -36,9 +36,9 @@ import {
|
||||
type StyleName,
|
||||
type ThemeName,
|
||||
} from "@/registry/config"
|
||||
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
||||
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
||||
import { resolvePresetOverrides } from "@/app/(app)/create/lib/preset-query"
|
||||
import { FONTS } from "@/app/(app)/(create)/lib/fonts"
|
||||
import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code"
|
||||
import { resolvePresetOverrides } from "@/app/(app)/(create)/lib/preset-query"
|
||||
|
||||
const designSystemSearchParams = {
|
||||
preset: parseAsString.withDefault("b0"),
|
||||
@@ -132,6 +132,9 @@ const NON_DESIGN_SYSTEM_KEYS = [
|
||||
"custom",
|
||||
] as const
|
||||
|
||||
// Shared across hook instances so only one mount effect encodes the initial URL.
|
||||
let hasSyncedPresetToUrl = false
|
||||
|
||||
export const loadDesignSystemSearchParams = createLoader(
|
||||
designSystemSearchParams
|
||||
)
|
||||
@@ -235,13 +238,42 @@ function resolvePresetParams(
|
||||
return normalizeDesignSystemParams(rawParams)
|
||||
}
|
||||
|
||||
export function buildPresetUrlUpdate(
|
||||
merged: DesignSystemSearchParams,
|
||||
resolvedUpdates: Partial<DesignSystemSearchParams> = {}
|
||||
) {
|
||||
const code = getPresetCode(merged)
|
||||
const rawUpdate: Record<string, unknown> = { preset: code }
|
||||
|
||||
for (const key of DESIGN_SYSTEM_KEYS) {
|
||||
rawUpdate[key] = null
|
||||
}
|
||||
|
||||
for (const key of NON_DESIGN_SYSTEM_KEYS) {
|
||||
if (key === "preset") {
|
||||
continue
|
||||
}
|
||||
|
||||
rawUpdate[key] =
|
||||
key in resolvedUpdates
|
||||
? (resolvedUpdates as Record<string, unknown>)[key]
|
||||
: merged[key]
|
||||
}
|
||||
|
||||
return rawUpdate
|
||||
}
|
||||
|
||||
// Wraps nuqs useQueryStates with transparent preset encoding/decoding.
|
||||
// - Reads: if ?preset=CODE is in the URL, decodes it and returns individual values.
|
||||
// - Writes: when design system params are set, encodes them into a preset code.
|
||||
//
|
||||
// Default options use shallow: true so picker selections do not trigger a full
|
||||
// Next.js server navigation. This prevents the customizer panel from flickering
|
||||
// or resetting while the URL update propagates (#10910, #11060).
|
||||
export function useDesignSystemSearchParams(options: Options = {}) {
|
||||
const searchParams = useSearchParams()
|
||||
const [rawParams, rawSetParams] = useQueryStates(designSystemSearchParams, {
|
||||
shallow: false,
|
||||
shallow: true,
|
||||
history: "push",
|
||||
...options,
|
||||
})
|
||||
@@ -257,6 +289,19 @@ export function useDesignSystemSearchParams(options: Options = {}) {
|
||||
paramsRef.current = params
|
||||
}, [params])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasSyncedPresetToUrl || searchParams.has("preset")) {
|
||||
return
|
||||
}
|
||||
|
||||
hasSyncedPresetToUrl = true
|
||||
|
||||
const merged = normalizeDesignSystemParams(paramsRef.current)
|
||||
void rawSetParams(buildPresetUrlUpdate(merged) as RawSetParamsInput, {
|
||||
history: "replace",
|
||||
})
|
||||
}, [rawSetParams, searchParams])
|
||||
|
||||
type RawSetParamsInput = Parameters<typeof rawSetParams>[0]
|
||||
|
||||
const setParams = React.useCallback(
|
||||
@@ -286,24 +331,10 @@ export function useDesignSystemSearchParams(options: Options = {}) {
|
||||
...paramsRef.current,
|
||||
...resolvedUpdates,
|
||||
})
|
||||
// Encode design system fields into a preset code.
|
||||
// Cast needed: merged values may include null from nuqs resets,
|
||||
// but encodePreset handles missing values by falling back to defaults.
|
||||
const code = getPresetCode(merged)
|
||||
// Build update: set preset, clear individual DS params from URL.
|
||||
const rawUpdate: Record<string, unknown> = { preset: code }
|
||||
for (const key of DESIGN_SYSTEM_KEYS) {
|
||||
rawUpdate[key] = null
|
||||
}
|
||||
|
||||
// Pass through non-DS params that were explicitly in the update.
|
||||
for (const key of NON_DESIGN_SYSTEM_KEYS) {
|
||||
if (key in resolvedUpdates) {
|
||||
rawUpdate[key] = (resolvedUpdates as Record<string, unknown>)[key]
|
||||
}
|
||||
}
|
||||
|
||||
return rawSetParams(rawUpdate as RawSetParamsInput, setOptions)
|
||||
return rawSetParams(
|
||||
buildPresetUrlUpdate(merged, resolvedUpdates) as RawSetParamsInput,
|
||||
setOptions
|
||||
)
|
||||
},
|
||||
[rawSetParams]
|
||||
)
|
||||
28
apps/v4/app/(app)/(create)/lib/shuffle-presets.ts
Normal file
28
apps/v4/app/(app)/(create)/lib/shuffle-presets.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Curated preset codes from https://dialectcn.xyz.
|
||||
export const SHUFFLE_PRESETS = [
|
||||
"b6sUj34d9",
|
||||
"b2tqYzpa88",
|
||||
"b1W4tDrk",
|
||||
"b1aIuQ2XC",
|
||||
"b7jsW1RxJ5",
|
||||
"b870VEw0in",
|
||||
"b3Zheoix4U",
|
||||
"b1x9M2c4aI",
|
||||
"b1W7jDEW",
|
||||
"b51GFh7y6",
|
||||
"b2fms620zo",
|
||||
"b1Q5GC",
|
||||
"buKEvLs",
|
||||
"b5rR41Mtnc",
|
||||
"b6tOz2I0x",
|
||||
"b2hNTREGRN",
|
||||
"bdIJ7Sq",
|
||||
"b6TqMNb5Wb",
|
||||
"bJIirQ",
|
||||
"b4aRK5K0fb",
|
||||
"b5HCiD38LI",
|
||||
"bdHjvCi",
|
||||
"b7QDHijUjj",
|
||||
"b4ZVZIPi9h",
|
||||
"b1W4bcno",
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { DEFAULT_CONFIG } from "@/registry/config"
|
||||
import { buildV0Payload } from "@/app/(app)/create/lib/v0"
|
||||
import { buildV0Payload } from "@/app/(app)/(create)/lib/v0"
|
||||
|
||||
vi.mock("shadcn/schema", async () => {
|
||||
return await vi.importActual("shadcn/schema")
|
||||
@@ -17,7 +17,6 @@ const chartData = [
|
||||
{ month: "Feb", amount: 900 },
|
||||
{ month: "Mar", amount: 1300 },
|
||||
{ month: "Apr", amount: 750 },
|
||||
{ month: "May", amount: 1400 },
|
||||
]
|
||||
|
||||
export function ContributionHistory() {
|
||||
@@ -35,13 +34,14 @@ export function ContributionHistory() {
|
||||
role="img"
|
||||
aria-label="Last 6 months of contribution activity"
|
||||
>
|
||||
{chartData.map((item) => (
|
||||
{chartData.map((item, index) => (
|
||||
<div
|
||||
key={item.month}
|
||||
className="flex h-full flex-1 flex-col justify-end gap-2"
|
||||
>
|
||||
<div
|
||||
className="min-h-2 rounded-t-md bg-chart-2"
|
||||
data-index={index}
|
||||
className="data-[index=5]:bg-chart-6 min-h-2 rounded-lg data-[index=0]:bg-chart-1 data-[index=1]:bg-chart-2 data-[index=2]:bg-chart-3 data-[index=3]:bg-chart-4 data-[index=4]:bg-chart-5"
|
||||
style={{ height: `${(item.amount / maxAmount) * 100}%` }}
|
||||
/>
|
||||
<span className="text-center text-xs text-muted-foreground">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { MessageScrollerDemo } from "@/examples/radix/message-scroller-demo"
|
||||
|
||||
import { AccountAccess } from "./account-access"
|
||||
import { AnalyticsCard } from "./analytics-card"
|
||||
import { ClaimableBalance } from "./claimable-balance"
|
||||
@@ -93,17 +95,20 @@ export function CardsDemo() {
|
||||
<ClaimableBalance />
|
||||
<DividendIncome />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) 3xl:flex!">
|
||||
<div className="hidden flex-col gap-(--gap) min-[1400px]:flex">
|
||||
<NewMilestone />
|
||||
<PayoutThreshold />
|
||||
<AccountAccess />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) md:flex">
|
||||
<QrConnect />
|
||||
<TransferFunds />
|
||||
<div className="**:[.text-center.text-xs]:hidden">
|
||||
<MessageScrollerDemo />
|
||||
</div>
|
||||
{/* <TransferFunds /> */}
|
||||
<Payments />
|
||||
</div>
|
||||
<div className="hidden flex-col gap-(--gap) min-[1400px]:flex">
|
||||
<div className="hidden flex-col gap-(--gap) min-[1900px]:flex">
|
||||
<EmptyDistributeTrack />
|
||||
<AnalyticsCard />
|
||||
<NotificationSettings />
|
||||
|
||||
@@ -117,7 +117,7 @@ export function UIElements() {
|
||||
</span>
|
||||
<span className="flex md:hidden style-sera:md:flex">Dialog</span>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogContent size="sm" className="theme-neutral">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
|
||||
@@ -60,23 +60,23 @@ export default function IndexPage() {
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<div className="container-wrapper flex-1 pb-6 md:px-0">
|
||||
<div className="container-wrapper flex-1 p-0">
|
||||
<div className="container overflow-hidden md:px-0 lg:max-w-none">
|
||||
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
|
||||
<section className="-mx-4 w-[140vw] overflow-hidden md:hidden">
|
||||
<Image
|
||||
src="/r/styles/new-york-v4/dashboard-01-light.png"
|
||||
width={1400}
|
||||
height={875}
|
||||
src="/images/full-light.png"
|
||||
width={2560}
|
||||
height={2764}
|
||||
alt="Dashboard"
|
||||
className="block dark:hidden"
|
||||
className="block h-auto w-full dark:hidden"
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
src="/r/styles/new-york-v4/dashboard-01-dark.png"
|
||||
width={1400}
|
||||
height={875}
|
||||
src="/images/full-dark.png"
|
||||
width={2560}
|
||||
height={2764}
|
||||
alt="Dashboard"
|
||||
className="hidden dark:block"
|
||||
className="hidden h-auto w-full dark:block"
|
||||
priority
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { HistoryProvider } from "@/app/(app)/create/hooks/use-history"
|
||||
import { LocksProvider } from "@/app/(app)/create/hooks/use-locks"
|
||||
|
||||
export default function CreateLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<LocksProvider>
|
||||
<Suspense>
|
||||
<HistoryProvider>{children}</HistoryProvider>
|
||||
</Suspense>
|
||||
</LocksProvider>
|
||||
)
|
||||
}
|
||||
@@ -185,7 +185,7 @@ export default async function Page(props: {
|
||||
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-(--sidebar-width) flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
|
||||
<div className="h-(--top-spacing) shrink-0"></div>
|
||||
{doc.toc?.length ? (
|
||||
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
|
||||
<div className="flex scroll-fade scrollbar-none flex-col gap-8 overflow-y-auto px-8">
|
||||
<DocsTableOfContents toc={doc.toc} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NextResponse, type NextRequest } from "next/server"
|
||||
|
||||
import { processMdxForLLMs } from "@/lib/llm"
|
||||
import { source } from "@/lib/source"
|
||||
import { getActiveStyle, type Style } from "@/registry/_legacy-styles"
|
||||
import { type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
@@ -26,7 +26,7 @@ export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ slug?: string[] }> }
|
||||
) {
|
||||
const [{ slug }, activeStyle] = await Promise.all([params, getActiveStyle()])
|
||||
const { slug } = await params
|
||||
|
||||
const page = source.getPage(slug)
|
||||
|
||||
@@ -34,7 +34,8 @@ export async function GET(
|
||||
notFound()
|
||||
}
|
||||
|
||||
const effectiveStyle = getStyleFromSlug(slug, activeStyle.name)
|
||||
// Default to the base style. Legacy content pins new-york-v4 per tag.
|
||||
const effectiveStyle = getStyleFromSlug(slug, "base-nova")
|
||||
|
||||
const processedContent = processMdxForLLMs(
|
||||
await page.data.getText("raw"),
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"use client"
|
||||
|
||||
export { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
||||
@@ -1 +0,0 @@
|
||||
export * from "@/app/(app)/create/lib/fonts"
|
||||
@@ -1 +0,0 @@
|
||||
export * from "@/app/(app)/create/lib/search-params"
|
||||
74
apps/v4/app/(view)/examples/[base]/[name]/page.tsx
Normal file
74
apps/v4/app/(view)/examples/[base]/[name]/page.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { type Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
import { ExamplesComponents } from "@/examples/__components__"
|
||||
import { ExamplesIndex } from "@/examples/__index__"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
|
||||
export const dynamicParams = true
|
||||
export const revalidate = 3600
|
||||
|
||||
function getExample(base: string, name: string) {
|
||||
const item = ExamplesIndex[base]?.[name]
|
||||
const Component = ExamplesComponents[base]?.[name]
|
||||
if (!item || !Component) {
|
||||
return null
|
||||
}
|
||||
return { item, Component }
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ base: string; name: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { base, name } = await params
|
||||
const example = getExample(base, name)
|
||||
|
||||
if (!example) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const title = example.item.name
|
||||
|
||||
return {
|
||||
title,
|
||||
openGraph: {
|
||||
title,
|
||||
type: "article",
|
||||
url: absoluteUrl(`/examples/${base}/${title}`),
|
||||
images: [
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: "@shadcn",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ExamplePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ base: string; name: string }>
|
||||
}) {
|
||||
const { base, name } = await params
|
||||
const example = getExample(base, name)
|
||||
|
||||
if (!example) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { Component } = example
|
||||
|
||||
return <Component />
|
||||
}
|
||||
@@ -6,16 +6,17 @@ import { siteConfig } from "@/lib/config"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||
import { BASES, type Base } from "@/registry/config"
|
||||
import { ActionMenuScript } from "@/app/(app)/create/components/action-menu"
|
||||
import { DesignSystemProvider } from "@/app/(app)/create/components/design-system-provider"
|
||||
import { HistoryScript } from "@/app/(app)/create/components/history-buttons"
|
||||
import { DarkModeScript } from "@/app/(app)/create/components/mode-switcher"
|
||||
import { OpenPresetScript } from "@/app/(app)/create/components/open-preset"
|
||||
import { PreviewStyle } from "@/app/(app)/create/components/preview-style"
|
||||
import { RandomizeScript } from "@/app/(app)/create/components/random-button"
|
||||
import { getBaseComponent, getBaseItem } from "@/app/(app)/create/lib/api"
|
||||
import { ActionMenuScript } from "@/app/(app)/(create)/components/action-menu"
|
||||
import { DesignSystemProvider } from "@/app/(app)/(create)/components/design-system-provider"
|
||||
import { HistoryScript } from "@/app/(app)/(create)/components/history-buttons"
|
||||
import { DarkModeScript } from "@/app/(app)/(create)/components/mode-switcher"
|
||||
import { OpenPresetScript } from "@/app/(app)/(create)/components/open-preset"
|
||||
import { PreviewStyle } from "@/app/(app)/(create)/components/preview-style"
|
||||
import { RandomizeScript } from "@/app/(app)/(create)/components/random-button"
|
||||
import { getBaseComponent, getBaseItem } from "@/app/(app)/(create)/lib/api"
|
||||
|
||||
import "@/app/style-registry.css"
|
||||
import "streamdown/styles.css"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PreviewFontVariables } from "@/app/(create)/preview/font-variables"
|
||||
import { previewFontVariables } from "@/app/(create)/preview/fonts"
|
||||
import { PreviewFontVariables } from "@/app/(view)/preview/font-variables"
|
||||
import { previewFontVariables } from "@/app/(view)/preview/fonts"
|
||||
|
||||
export default function PreviewLayout({
|
||||
children,
|
||||
@@ -3,6 +3,8 @@
|
||||
@import "shadcn/tailwind.css";
|
||||
@import "./legacy-themes.css";
|
||||
|
||||
@source "../node_modules/streamdown/dist/*.js";
|
||||
|
||||
@custom-variant style-vega (&:where(.style-vega *));
|
||||
@custom-variant style-nova (&:where(.style-nova *));
|
||||
@custom-variant style-lyra (&:where(.style-lyra *));
|
||||
@@ -168,6 +170,7 @@
|
||||
@apply overscroll-y-none;
|
||||
}
|
||||
body {
|
||||
position: relative;
|
||||
font-synthesis-weight: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
@@ -284,6 +287,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] code,
|
||||
[data-rehype-pretty-code-figure] code span {
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings:
|
||||
"liga" 0,
|
||||
"calt" 0;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-title] {
|
||||
border-bottom: color-mix(in oklab, var(--border) 30%, transparent);
|
||||
border-bottom-width: 1px;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user