mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-12 18:31:46 +00:00
Compare commits
204 Commits
shadcn@4.0
...
shadcn@4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14bb486174 | ||
|
|
12b49c986f | ||
|
|
64c8cd99ee | ||
|
|
7d718ddaa9 | ||
|
|
5570b3e24a | ||
|
|
214b1b8479 | ||
|
|
8bd161d453 | ||
|
|
64b88b6cdb | ||
|
|
6a070bf8c5 | ||
|
|
124495f0df | ||
|
|
43f64065b7 | ||
|
|
8bec9c1234 | ||
|
|
ba6ac6ec63 | ||
|
|
b75796ed76 | ||
|
|
d82b4a7d98 | ||
|
|
5b79499d23 | ||
|
|
d78ff8b858 | ||
|
|
ef78384bfd | ||
|
|
d3ab7fb00b | ||
|
|
bebc4356af | ||
|
|
14bc966fee | ||
|
|
6a4b27b80d | ||
|
|
c5b4080649 | ||
|
|
408b25c82a | ||
|
|
228b0e3ecd | ||
|
|
f900bd57d0 | ||
|
|
6b190c6a18 | ||
|
|
c43bc4f5d6 | ||
|
|
9cd14a684f | ||
|
|
fc1675e54d | ||
|
|
a5abe1aa0f | ||
|
|
031998436f | ||
|
|
29cb65c26b | ||
|
|
179c0c0b23 | ||
|
|
03430e03bf | ||
|
|
169682d87a | ||
|
|
336eee688e | ||
|
|
32e4827559 | ||
|
|
7a81328b23 | ||
|
|
5b40b9de5a | ||
|
|
e327cef2c1 | ||
|
|
563d572ba0 | ||
|
|
687f09817b | ||
|
|
31dbc6fc91 | ||
|
|
8db2be8b09 | ||
|
|
a8bd00466a | ||
|
|
e78bb7b4f3 | ||
|
|
acaa0953df | ||
|
|
632e2c012e | ||
|
|
78f6a8b0f0 | ||
|
|
a9f997d00a | ||
|
|
dbe1fa76b3 | ||
|
|
74c4c7508b | ||
|
|
4809da6f9c | ||
|
|
7ffefce9e0 | ||
|
|
6cad522930 | ||
|
|
d683b05d7f | ||
|
|
e000e17856 | ||
|
|
1be8f98c46 | ||
|
|
6e476e4756 | ||
|
|
e2d36a3a7d | ||
|
|
a97ebe54f1 | ||
|
|
b2cc0dfe59 | ||
|
|
af99d4ebd3 | ||
|
|
a0a072dcdd | ||
|
|
447c7aac06 | ||
|
|
752615f231 | ||
|
|
f9b365bc7f | ||
|
|
17a1a9093a | ||
|
|
8159e98075 | ||
|
|
6a527b3e75 | ||
|
|
ebe689e85c | ||
|
|
8b683b44e6 | ||
|
|
725ca574f6 | ||
|
|
1dc39e2484 | ||
|
|
090556691c | ||
|
|
8e9f781cdb | ||
|
|
9d7c205442 | ||
|
|
902379fa3e | ||
|
|
94dcf37add | ||
|
|
843a5e2334 | ||
|
|
cdaad392ae | ||
|
|
49abe0d594 | ||
|
|
eeb33ae9c9 | ||
|
|
55fa1bb7cc | ||
|
|
90bbbb7993 | ||
|
|
fc9705665c | ||
|
|
52c477e118 | ||
|
|
41a4573002 | ||
|
|
f813fb5884 | ||
|
|
429c001412 | ||
|
|
cd7743cbc1 | ||
|
|
cadc3f96de | ||
|
|
a74515d6e1 | ||
|
|
0df9af0d75 | ||
|
|
e653c1a833 | ||
|
|
8b819e1db4 | ||
|
|
5236bfdf07 | ||
|
|
7e93eb81ea | ||
|
|
abf1555a65 | ||
|
|
584db77fee | ||
|
|
3faa91d670 | ||
|
|
2bf8ef86b9 | ||
|
|
624a4fe320 | ||
|
|
5508b5e4ec | ||
|
|
3af2ba80e8 | ||
|
|
40a00278ab | ||
|
|
5ab89f3ae3 | ||
|
|
40dc195fad | ||
|
|
ca374ad0a0 | ||
|
|
bc9f556c38 | ||
|
|
d06e54d2bb | ||
|
|
65ddce2886 | ||
|
|
f413598ba3 | ||
|
|
34c04d5f01 | ||
|
|
0029b3b6f7 | ||
|
|
75cc35272a | ||
|
|
821ac7ee4d | ||
|
|
8df46c4ded | ||
|
|
2303ce2372 | ||
|
|
cf672a9575 | ||
|
|
5ee4567353 | ||
|
|
6f72dba9c4 | ||
|
|
cd717896fa | ||
|
|
d47562cc08 | ||
|
|
aff5d7f0c1 | ||
|
|
4c0be13dcc | ||
|
|
afa410e47f | ||
|
|
7ee55e8bd3 | ||
|
|
e3c9a3f9dc | ||
|
|
aa841e35cf | ||
|
|
598fb2f55a | ||
|
|
aa786280a3 | ||
|
|
07fd9d0ea4 | ||
|
|
6ad0590d87 | ||
|
|
ff51e9ca3c | ||
|
|
7958cc6a33 | ||
|
|
2871e15418 | ||
|
|
c7d57548e5 | ||
|
|
dac13c90f2 | ||
|
|
50d8b764a9 | ||
|
|
90fc0b2dff | ||
|
|
d9d43d5b3b | ||
|
|
ce2c3ca688 | ||
|
|
3bd1bbe858 | ||
|
|
448fb0bc06 | ||
|
|
09a84892d9 | ||
|
|
16da5f2a56 | ||
|
|
f5f2a02eda | ||
|
|
dad006aa1e | ||
|
|
20a94ddb77 | ||
|
|
ae733168cd | ||
|
|
49616d2e16 | ||
|
|
7bc47bb858 | ||
|
|
e149aac756 | ||
|
|
62abc6be99 | ||
|
|
0072c9801f | ||
|
|
729708ad2e | ||
|
|
a4c6504c96 | ||
|
|
1bd5f3d7c8 | ||
|
|
3d6ea09c50 | ||
|
|
f45b8f3066 | ||
|
|
ad99fc9a73 | ||
|
|
da05ee321c | ||
|
|
de497a36bb | ||
|
|
882a9cb145 | ||
|
|
65cb5b49ff | ||
|
|
ae6f2e67aa | ||
|
|
d13d42d434 | ||
|
|
67c99dd33c | ||
|
|
ce012faf1e | ||
|
|
554a1a69a7 | ||
|
|
e489552614 | ||
|
|
8386198073 | ||
|
|
9c570f1435 | ||
|
|
ed2d9a6728 | ||
|
|
f336513d18 | ||
|
|
5755d6aa1f | ||
|
|
e363e343b7 | ||
|
|
fe955258c3 | ||
|
|
f5ac4a0d2a | ||
|
|
97ed7eb35c | ||
|
|
6909385aea | ||
|
|
8dabe113fa | ||
|
|
f5556230f1 | ||
|
|
327551f8b6 | ||
|
|
52f72b9cf7 | ||
|
|
35657b4d5f | ||
|
|
c1b92c3175 | ||
|
|
b7afa9ba73 | ||
|
|
4e416dea5e | ||
|
|
16a0473b10 | ||
|
|
4210d1ab05 | ||
|
|
d99fcf4a1c | ||
|
|
bc8626c6f8 | ||
|
|
1df2bf4d9b | ||
|
|
4193e3c78f | ||
|
|
51fc7f5457 | ||
|
|
7ae522e610 | ||
|
|
e1a0ec3061 | ||
|
|
f8222528eb | ||
|
|
b142bd2fd5 | ||
|
|
4fa8f9b4c2 | ||
|
|
24205601e1 |
22
.cursor/rules/registry-bases-parity.mdc
Normal file
22
.cursor/rules/registry-bases-parity.mdc
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
description: Keep registry base and radix trees in sync when editing shared UI
|
||||
globs: apps/v4/registry/bases/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Registry bases: Base UI ↔ Radix parity
|
||||
|
||||
`apps/v4/registry/bases/base` and `apps/v4/registry/bases/radix` are **parallel registries**. Anything that exists in both trees for the same purpose (preview blocks, mirrored examples, shared card layouts, etc.) **must stay in sync**.
|
||||
|
||||
## When editing
|
||||
|
||||
- If you change a file under **`bases/base/...`**, apply the **same behavioral and visual change** to the matching path under **`bases/radix/...`** (and the reverse).
|
||||
- Only diverge where APIs differ (e.g. import paths like `@/registry/bases/base/ui/*` vs `@/registry/bases/radix/ui/*`, or Base UI vs Radix component props).
|
||||
- Do **not** update only one side unless the user explicitly asks for a single-base change.
|
||||
|
||||
## Typical mirrored paths
|
||||
|
||||
- `blocks/preview/**` — preview cards and blocks
|
||||
- Parallel `ui/*` components when both exist for the same component
|
||||
|
||||
After edits, briefly confirm both trees were updated (or state why one side is intentionally unchanged).
|
||||
78
.github/workflows/deprecated.yml
vendored
78
.github/workflows/deprecated.yml
vendored
@@ -1,78 +0,0 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
files: |
|
||||
apps/www/**
|
||||
files_ignore: |
|
||||
apps/www/public/r/**
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Comment on PR if www files changed
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
|
||||
const wwwFiles = changedFiles.filter(file =>
|
||||
file.startsWith('apps/www/') &&
|
||||
!file.startsWith('apps/www/public/r/') &&
|
||||
file !== 'apps/www/package.json'
|
||||
);
|
||||
|
||||
if (wwwFiles.length > 0) {
|
||||
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
|
||||
|
||||
Consider applying the change to \`apps/v4\` if relevant.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
// Add deprecated label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['deprecated']
|
||||
});
|
||||
} else {
|
||||
// Remove deprecated label if no www files are changed
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'deprecated'
|
||||
});
|
||||
} catch (error) {
|
||||
// Label doesn't exist, which is fine
|
||||
console.log('Deprecated label not found, skipping removal');
|
||||
}
|
||||
}
|
||||
76
.github/workflows/validate-registries.yml
vendored
76
.github/workflows/validate-registries.yml
vendored
@@ -13,6 +13,44 @@ on:
|
||||
- "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
|
||||
@@ -28,6 +66,44 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Block reserved registry namespaces
|
||||
env:
|
||||
RESERVED_NAMESPACES: "@shadcn,@ui,@blocks,@components,@block,@component,@util,@utils,@registry,@lib,@hook,@hooks,@theme,@themes,@chart,@charts"
|
||||
run: |
|
||||
node <<'EOF'
|
||||
const fs = require("node:fs")
|
||||
|
||||
const files = [
|
||||
"apps/v4/public/r/registries.json",
|
||||
"apps/v4/registry/directory.json",
|
||||
]
|
||||
const reservedNamespaces = new Set(
|
||||
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
|
||||
)
|
||||
|
||||
function readNames(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
|
||||
(entry) => entry.name
|
||||
)
|
||||
}
|
||||
|
||||
const violations = files.flatMap((filePath) => {
|
||||
return readNames(filePath)
|
||||
.filter((name) => reservedNamespaces.has(name))
|
||||
.map((name) => `${filePath}: ${name}`)
|
||||
})
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Reserved registry namespaces are not allowed:")
|
||||
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation}`)
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
EOF
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,6 +15,7 @@ build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
@@ -43,3 +44,4 @@ tsconfig.tsbuildinfo
|
||||
.notes
|
||||
.playwright-mcp
|
||||
shadcn-workspace
|
||||
.codex-artifacts
|
||||
|
||||
@@ -5,3 +5,4 @@ build
|
||||
.contentlayer
|
||||
**/fixtures
|
||||
deprecated
|
||||
apps/v4/registry/styles/**/*.css
|
||||
|
||||
@@ -5,3 +5,4 @@ build
|
||||
.contentlayer
|
||||
registry/__index__.tsx
|
||||
content/docs/components/calendar.mdx
|
||||
registry/styles/**/*.css
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
@@ -13,11 +15,10 @@ import {
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
import { RadioGroup, RadioGroupItem } from "@/examples/radix/ui/radio-group"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
import { Input } from "@/styles/radix-nova/ui/input"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/radix-nova/ui/radio-group"
|
||||
import { Switch } from "@/styles/radix-nova/ui/switch"
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const [gpuCount, setGpuCount] = React.useState(8)
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -15,18 +27,7 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
} from "@/styles/radix-nova/ui/tooltip"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
|
||||
export function ButtonGroupNested() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
} from "@/styles/radix-nova/ui/popover"
|
||||
import { Separator } from "@/styles/radix-nova/ui/separator"
|
||||
import { Textarea } from "@/styles/radix-nova/ui/textarea"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/examples/radix/ui/avatar"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
} from "@/styles/radix-nova/ui/avatar"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -12,8 +14,7 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
} from "@/styles/radix-nova/ui/empty"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
|
||||
|
||||
export function FieldCheckbox() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
import { Input } from "@/styles/radix-nova/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/examples/radix/ui/select"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
} from "@/styles/radix-nova/ui/select"
|
||||
import { Textarea } from "@/styles/radix-nova/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card, CardContent } from "@/examples/radix/ui/card"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Card, CardContent } from "@/styles/radix-nova/ui/card"
|
||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/radix/ui/field"
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
|
||||
const options = [
|
||||
{
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
|
||||
import { Slider } from "@/examples/radix/ui/slider"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
import { Slider } from "@/styles/radix-nova/ui/slider"
|
||||
|
||||
export function FieldSlider() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FieldSeparator } from "@/examples/radix/ui/field"
|
||||
import { FieldSeparator } from "@/styles/radix-nova/ui/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Label } from "@/examples/radix/ui/label"
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import { Label } from "@/styles/radix-nova/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
} from "@/styles/radix-nova/ui/popover"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
@@ -11,15 +14,13 @@ import {
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import { Separator } from "@/styles/radix-nova/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
} from "@/styles/radix-nova/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
@@ -88,7 +89,7 @@ export function InputGroupDemo() {
|
||||
<InputGroupInput placeholder="@shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
<IconCheck className="size-3 text-background" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
@@ -6,8 +8,7 @@ import {
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/examples/radix/ui/item"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
} from "@/styles/radix-nova/ui/item"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/styles/radix-nova/ui/avatar"
|
||||
import { Badge } from "@/styles/radix-nova/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -10,7 +26,7 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/examples/radix/ui/command"
|
||||
} from "@/styles/radix-nova/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
@@ -23,36 +39,25 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
} from "@/styles/radix-nova/ui/popover"
|
||||
import { Switch } from "@/styles/radix-nova/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
} from "@/styles/radix-nova/ui/tooltip"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
import { Badge } from "@/styles/radix-nova/ui/badge"
|
||||
import { Spinner } from "@/styles/radix-nova/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
} from "@/styles/radix-nova/ui/empty"
|
||||
import { Spinner } from "@/styles/radix-nova/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function BlocksLayout({
|
||||
<a href="#blocks">Browse Blocks</a>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link href="/docs/blocks">Add a block</Link>
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { mdxComponents } from "@/mdx-components"
|
||||
import { IconRss } from "@tabler/icons-react"
|
||||
|
||||
import { getChangelogPages, type ChangelogPageData } from "@/lib/changelog"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
@@ -79,7 +79,7 @@ export default function ChangelogPage() {
|
||||
})}
|
||||
{olderPages.length > 0 && (
|
||||
<div id="more-updates" className="mb-24 scroll-mt-24">
|
||||
<h2 className="font-heading mb-6 text-xl font-semibold tracking-tight">
|
||||
<h2 className="mb-6 font-heading text-xl font-semibold tracking-tight">
|
||||
More Updates
|
||||
</h2>
|
||||
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
@@ -13,13 +16,13 @@ import {
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/base/ui-rtl/field"
|
||||
import { Input } from "@/examples/base/ui-rtl/input"
|
||||
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui-rtl/radio-group"
|
||||
import { Switch } from "@/examples/base/ui-rtl/switch"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/field"
|
||||
import { Input } from "@/styles/base-nova/ui-rtl/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/styles/base-nova/ui-rtl/radio-group"
|
||||
import { Switch } from "@/styles/base-nova/ui-rtl/switch"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/base/ui-rtl/dropdown-menu"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
@@ -30,6 +14,22 @@ import {
|
||||
} from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/dropdown-menu"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/base/ui-rtl/input-group"
|
||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/base/ui-rtl/tooltip"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/tooltip"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/base/ui-rtl/popover"
|
||||
import { Separator } from "@/examples/base/ui-rtl/separator"
|
||||
import { Textarea } from "@/examples/base/ui-rtl/textarea"
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/popover"
|
||||
import { Separator } from "@/styles/base-nova/ui-rtl/separator"
|
||||
import { Textarea } from "@/styles/base-nova/ui-rtl/textarea"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/examples/base/ui-rtl/avatar"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
} from "@/styles/base-nova/ui-rtl/avatar"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -14,10 +17,7 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/base/ui-rtl/empty"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/empty"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
|
||||
import { Field, FieldLabel } from "@/examples/base/ui-rtl/field"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Checkbox } from "@/styles/base-nova/ui-rtl/checkbox"
|
||||
import { Field, FieldLabel } from "@/styles/base-nova/ui-rtl/field"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { Checkbox } from "@/styles/base-nova/ui-rtl/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -10,8 +11,8 @@ import {
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/examples/base/ui-rtl/field"
|
||||
import { Input } from "@/examples/base/ui-rtl/input"
|
||||
} from "@/styles/base-nova/ui-rtl/field"
|
||||
import { Input } from "@/styles/base-nova/ui-rtl/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -19,10 +20,8 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/examples/base/ui-rtl/select"
|
||||
import { Textarea } from "@/examples/base/ui-rtl/textarea"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/select"
|
||||
import { Textarea } from "@/styles/base-nova/ui-rtl/textarea"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
@@ -140,7 +139,7 @@ export function FieldDemo() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rtl-exp-month">{t.month}</FieldLabel>
|
||||
<Select defaultValue="" items={months}>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="rtl-exp-month">
|
||||
<SelectValue placeholder="MM" />
|
||||
</SelectTrigger>
|
||||
@@ -157,7 +156,7 @@ export function FieldDemo() {
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rtl-exp-year">{t.year}</FieldLabel>
|
||||
<Select defaultValue="" items={years}>
|
||||
<Select defaultValue="">
|
||||
<SelectTrigger id="rtl-exp-year">
|
||||
<SelectValue placeholder="YYYY" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "@/examples/base/ui-rtl/card"
|
||||
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Card, CardContent } from "@/styles/base-nova/ui-rtl/card"
|
||||
import { Checkbox } from "@/styles/base-nova/ui-rtl/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -10,9 +11,7 @@ import {
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/base/ui-rtl/field"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/field"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/examples/base/ui-rtl/field"
|
||||
import { Slider } from "@/examples/base/ui-rtl/slider"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/field"
|
||||
import { Slider } from "@/styles/base-nova/ui-rtl/slider"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { DirectionProvider } from "@/examples/base/ui-rtl/direction"
|
||||
import { FieldSeparator } from "@/examples/base/ui-rtl/field"
|
||||
|
||||
import {
|
||||
LanguageProvider,
|
||||
LanguageSelector,
|
||||
useLanguageContext,
|
||||
} from "@/components/language-selector"
|
||||
import { DirectionProvider } from "@/styles/base-nova/ui-rtl/direction"
|
||||
import { FieldSeparator } from "@/styles/base-nova/ui-rtl/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/base/ui-rtl/input-group"
|
||||
import { Label } from "@/examples/base/ui-rtl/label"
|
||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
||||
import { Label } from "@/styles/base-nova/ui-rtl/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/base/ui-rtl/popover"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/popover"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/base/ui-rtl/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/base/ui-rtl/input-group"
|
||||
import { Separator } from "@/examples/base/ui-rtl/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/base/ui-rtl/tooltip"
|
||||
import {
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
@@ -29,6 +9,26 @@ import {
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
||||
import { Separator } from "@/styles/base-nova/ui-rtl/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/tooltip"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
@@ -8,10 +11,7 @@ import {
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/examples/base/ui-rtl/item"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/item"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,47 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/examples/base/ui-rtl/avatar"
|
||||
import { Badge } from "@/examples/base/ui-rtl/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/examples/base/ui-rtl/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/base/ui-rtl/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/examples/base/ui-rtl/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/base/ui-rtl/input-group"
|
||||
import { Popover, PopoverContent } from "@/examples/base/ui-rtl/popover"
|
||||
import { Switch } from "@/examples/base/ui-rtl/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/base/ui-rtl/tooltip"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
@@ -55,6 +14,47 @@ import {
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/styles/base-nova/ui-rtl/avatar"
|
||||
import { Badge } from "@/styles/base-nova/ui-rtl/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/styles/base-nova/ui-rtl/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/styles/base-nova/ui-rtl/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
||||
import { Popover, PopoverContent } from "@/styles/base-nova/ui-rtl/popover"
|
||||
import { Switch } from "@/styles/base-nova/ui-rtl/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/tooltip"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/examples/base/ui-rtl/badge"
|
||||
import { Spinner } from "@/examples/base/ui-rtl/spinner"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Badge } from "@/styles/base-nova/ui-rtl/badge"
|
||||
import { Spinner } from "@/styles/base-nova/ui-rtl/spinner"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -8,10 +9,8 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/base/ui-rtl/empty"
|
||||
import { Spinner } from "@/examples/base/ui-rtl/spinner"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
} from "@/styles/base-nova/ui-rtl/empty"
|
||||
import { Spinner } from "@/styles/base-nova/ui-rtl/spinner"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -82,6 +82,11 @@ export function MenuAccentPicker({
|
||||
key={accent.value}
|
||||
value={accent.value}
|
||||
closeOnClick={isMobile}
|
||||
disabled={
|
||||
accent.value === "bold" &&
|
||||
(params.menuColor === "default-translucent" ||
|
||||
params.menuColor === "inverted-translucent")
|
||||
}
|
||||
>
|
||||
{accent.label}
|
||||
</PickerRadioItem>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import Script from "next/script"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import {
|
||||
Command,
|
||||
CommandDialog,
|
||||
@@ -9,9 +11,7 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/examples/base/ui/command"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
} from "@/styles/base-nova/ui/command"
|
||||
import { useActionMenu } from "@/app/(create)/hooks/use-action-menu"
|
||||
|
||||
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
||||
|
||||
136
apps/v4/app/(create)/components/chart-color-picker.tsx
Normal file
136
apps/v4/app/(create)/components/chart-color-picker.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import {
|
||||
BASE_COLORS,
|
||||
getThemesForBaseColor,
|
||||
type ChartColorName,
|
||||
} from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function ChartColorPicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const availableChartColors = React.useMemo(
|
||||
() => getThemesForBaseColor(params.baseColor),
|
||||
[params.baseColor]
|
||||
)
|
||||
|
||||
const currentChartColor = React.useMemo(
|
||||
() =>
|
||||
availableChartColors.find((theme) => theme.name === params.chartColor),
|
||||
[availableChartColors, params.chartColor]
|
||||
)
|
||||
|
||||
const currentChartColorIsBaseColor = React.useMemo(
|
||||
() => BASE_COLORS.find((baseColor) => baseColor.name === params.chartColor),
|
||||
[params.chartColor]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentChartColor && availableChartColors.length > 0) {
|
||||
setParams({ chartColor: availableChartColors[0].name })
|
||||
}
|
||||
}, [currentChartColor, availableChartColors, setParams])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Chart Color</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{currentChartColor?.title}
|
||||
</div>
|
||||
</div>
|
||||
{mounted && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
currentChartColor?.cssVars?.dark?.[
|
||||
currentChartColorIsBaseColor
|
||||
? "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 md:right-2.5"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-92"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentChartColor?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ chartColor: value as ChartColorName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{availableChartColors
|
||||
.filter((theme) =>
|
||||
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
|
||||
)
|
||||
.map((theme) => (
|
||||
<PickerRadioItem
|
||||
key={theme.name}
|
||||
value={theme.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{theme.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{availableChartColors
|
||||
.filter(
|
||||
(theme) =>
|
||||
!BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
)
|
||||
.map((theme) => (
|
||||
<PickerRadioItem
|
||||
key={theme.name}
|
||||
value={theme.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{theme.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="chartColor"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
"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 { Button } from "@/styles/base-nova/ui/button"
|
||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||
|
||||
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { getThemesForBaseColor, STYLES } from "@/registry/config"
|
||||
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, STYLES } from "@/registry/config"
|
||||
} from "@/styles/base-nova/ui/card"
|
||||
import { FieldGroup, FieldSeparator } from "@/styles/base-nova/ui/field"
|
||||
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 { ChartColorPicker } from "@/app/(create)/components/chart-color-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 { ProjectForm } from "@/app/(create)/components/project-form"
|
||||
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
|
||||
import { RandomButton } from "@/app/(create)/components/random-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 { FONT_HEADING_OPTIONS, FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function Customizer({
|
||||
@@ -57,22 +55,40 @@ export function Customizer({
|
||||
<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} />
|
||||
<FieldGroup className="flex-row gap-2.5 py-px **:data-[slot=field-separator]:-mx-4 **:data-[slot=field-separator]:w-auto md:flex-col md:gap-3.25">
|
||||
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
|
||||
<StylePicker
|
||||
styles={STYLES}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<ThemePicker
|
||||
themes={availableThemes}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<ChartColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<FontPicker
|
||||
label="Heading"
|
||||
param="fontHeading"
|
||||
fonts={FONT_HEADING_OPTIONS}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<FontPicker
|
||||
label="Font"
|
||||
param="font"
|
||||
fonts={FONTS}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
</FieldGroup>
|
||||
|
||||
@@ -6,7 +6,6 @@ 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"
|
||||
@@ -15,6 +14,46 @@ import {
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
|
||||
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
|
||||
|
||||
type RegistryThemeCssVars = NonNullable<
|
||||
ReturnType<typeof buildRegistryTheme>["cssVars"]
|
||||
>
|
||||
|
||||
function removeManagedBodyClasses(body: Element) {
|
||||
for (const className of Array.from(body.classList)) {
|
||||
if (
|
||||
MANAGED_BODY_CLASS_PREFIXES.some((prefix) => className.startsWith(prefix))
|
||||
) {
|
||||
body.classList.remove(className)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildCssRule(selector: string, cssVars?: Record<string, string>) {
|
||||
const declarations = Object.entries(cssVars ?? {})
|
||||
.filter(([, value]) => Boolean(value))
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
if (!declarations) {
|
||||
return `${selector} {}\n`
|
||||
}
|
||||
|
||||
return `${selector} {\n${declarations}\n}\n`
|
||||
}
|
||||
|
||||
function buildThemeCssText(cssVars: RegistryThemeCssVars) {
|
||||
return [
|
||||
buildCssRule(":root", {
|
||||
...(cssVars.theme ?? {}),
|
||||
...(cssVars.light ?? {}),
|
||||
}),
|
||||
buildCssRule(".dark", cssVars.dark),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export function DesignSystemProvider({
|
||||
children,
|
||||
}: {
|
||||
@@ -24,19 +63,65 @@ export function DesignSystemProvider({
|
||||
shallow: true, // No need to go through the server…
|
||||
history: "replace", // …or push updates into the iframe history.
|
||||
})
|
||||
const [liveSearchParams, setLiveSearchParams] = React.useState(searchParams)
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
const { style, theme, font, baseColor, menuAccent, menuColor, radius } =
|
||||
liveSearchParams
|
||||
const {
|
||||
style,
|
||||
theme,
|
||||
font,
|
||||
fontHeading,
|
||||
baseColor,
|
||||
chartColor,
|
||||
menuAccent,
|
||||
menuColor,
|
||||
radius,
|
||||
} = searchParams
|
||||
const effectiveRadius = style === "lyra" ? "none" : radius
|
||||
const selectedFont = React.useMemo(
|
||||
() => FONTS.find((fontOption) => fontOption.value === font),
|
||||
[font]
|
||||
)
|
||||
const selectedHeadingFont = React.useMemo(() => {
|
||||
if (fontHeading === "inherit" || fontHeading === font) {
|
||||
return selectedFont
|
||||
}
|
||||
|
||||
return FONTS.find((fontOption) => fontOption.value === fontHeading)
|
||||
}, [font, fontHeading, selectedFont])
|
||||
const initialFontSansRef = React.useRef<string | null>(null)
|
||||
const initialFontHeadingRef = React.useRef<string | null>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
setLiveSearchParams(searchParams)
|
||||
}, [searchParams])
|
||||
initialFontSansRef.current =
|
||||
document.documentElement.style.getPropertyValue("--font-sans")
|
||||
initialFontHeadingRef.current =
|
||||
document.documentElement.style.getPropertyValue("--font-heading")
|
||||
|
||||
return () => {
|
||||
removeManagedBodyClasses(document.body)
|
||||
document.getElementById(THEME_STYLE_ELEMENT_ID)?.remove()
|
||||
|
||||
if (initialFontSansRef.current) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-sans",
|
||||
initialFontSansRef.current
|
||||
)
|
||||
} else {
|
||||
document.documentElement.style.removeProperty("--font-sans")
|
||||
}
|
||||
|
||||
if (initialFontHeadingRef.current) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-heading",
|
||||
initialFontHeadingRef.current
|
||||
)
|
||||
} else {
|
||||
document.documentElement.style.removeProperty("--font-heading")
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleDesignSystemMessage = React.useCallback(
|
||||
(nextParams: DesignSystemSearchParams) => {
|
||||
setLiveSearchParams(nextParams)
|
||||
setSearchParams(nextParams)
|
||||
},
|
||||
[setSearchParams]
|
||||
@@ -46,11 +131,7 @@ export function DesignSystemProvider({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (style === "lyra" && radius !== "none") {
|
||||
setLiveSearchParams((prev) => ({
|
||||
...prev,
|
||||
radius: "none",
|
||||
}))
|
||||
setSearchParams({ radius: "none" as RadiusValue })
|
||||
setSearchParams({ radius: "none" })
|
||||
}
|
||||
}, [style, radius, setSearchParams])
|
||||
|
||||
@@ -63,27 +144,36 @@ export function DesignSystemProvider({
|
||||
const body = document.body
|
||||
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
removeManagedBodyClasses(body)
|
||||
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
|
||||
document.documentElement.style.setProperty("--font-sans", fontFamily)
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-sans",
|
||||
selectedFont.font.style.fontFamily
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedHeadingFont) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-heading",
|
||||
selectedHeadingFont.font.style.fontFamily
|
||||
)
|
||||
}
|
||||
|
||||
setIsReady(true)
|
||||
}, [style, theme, font, baseColor])
|
||||
}, [
|
||||
style,
|
||||
theme,
|
||||
font,
|
||||
fontHeading,
|
||||
baseColor,
|
||||
selectedFont,
|
||||
selectedHeadingFont,
|
||||
])
|
||||
|
||||
const registryTheme = React.useMemo(() => {
|
||||
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
|
||||
@@ -94,12 +184,13 @@ export function DesignSystemProvider({
|
||||
...DEFAULT_CONFIG,
|
||||
baseColor,
|
||||
theme,
|
||||
chartColor,
|
||||
menuAccent,
|
||||
radius: effectiveRadius,
|
||||
}
|
||||
|
||||
return buildRegistryTheme(config)
|
||||
}, [baseColor, theme, menuAccent, effectiveRadius])
|
||||
}, [baseColor, theme, chartColor, menuAccent, effectiveRadius])
|
||||
|
||||
// Use useLayoutEffect for synchronous CSS var updates.
|
||||
React.useLayoutEffect(() => {
|
||||
@@ -107,69 +198,83 @@ export function DesignSystemProvider({
|
||||
return
|
||||
}
|
||||
|
||||
const styleId = "design-system-theme-vars"
|
||||
let styleElement = document.getElementById(
|
||||
styleId
|
||||
THEME_STYLE_ELEMENT_ID
|
||||
) as HTMLStyleElement | null
|
||||
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement("style")
|
||||
styleElement.id = styleId
|
||||
styleElement.id = THEME_STYLE_ELEMENT_ID
|
||||
document.head.appendChild(styleElement)
|
||||
}
|
||||
|
||||
const {
|
||||
light: lightVars,
|
||||
dark: darkVars,
|
||||
theme: themeVars,
|
||||
} = registryTheme.cssVars
|
||||
|
||||
let cssText = ":root {\n"
|
||||
// Add theme vars (shared across light/dark).
|
||||
if (themeVars) {
|
||||
Object.entries(themeVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
cssText += ` --${key}: ${value};\n`
|
||||
}
|
||||
})
|
||||
}
|
||||
// Add light mode vars.
|
||||
if (lightVars) {
|
||||
Object.entries(lightVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
cssText += ` --${key}: ${value};\n`
|
||||
}
|
||||
})
|
||||
}
|
||||
cssText += "}\n\n"
|
||||
|
||||
cssText += ".dark {\n"
|
||||
if (darkVars) {
|
||||
Object.entries(darkVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
cssText += ` --${key}: ${value};\n`
|
||||
}
|
||||
})
|
||||
}
|
||||
cssText += "}\n"
|
||||
|
||||
styleElement.textContent = cssText
|
||||
styleElement.textContent = buildThemeCssText(registryTheme.cssVars)
|
||||
}, [registryTheme])
|
||||
|
||||
// Handle menu color inversion by adding/removing dark class to elements with cn-menu-target.
|
||||
React.useEffect(() => {
|
||||
// useLayoutEffect to apply classes synchronously before paint, avoiding flash.
|
||||
React.useLayoutEffect(() => {
|
||||
if (!menuColor) {
|
||||
return
|
||||
}
|
||||
|
||||
const isInvertedMenu =
|
||||
menuColor === "inverted" || menuColor === "inverted-translucent"
|
||||
const isTranslucentMenu =
|
||||
menuColor === "default-translucent" ||
|
||||
menuColor === "inverted-translucent"
|
||||
let frameId = 0
|
||||
|
||||
const updateMenuElements = () => {
|
||||
const menuElements = document.querySelectorAll(".cn-menu-target")
|
||||
menuElements.forEach((element) => {
|
||||
if (menuColor === "inverted") {
|
||||
element.classList.add("dark")
|
||||
} else {
|
||||
element.classList.remove("dark")
|
||||
const allElements = document.querySelectorAll<HTMLElement>(
|
||||
".cn-menu-target, [data-menu-translucent]"
|
||||
)
|
||||
|
||||
if (allElements.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Disable transitions while toggling classes.
|
||||
allElements.forEach((element) => {
|
||||
element.style.transition = "none"
|
||||
})
|
||||
|
||||
allElements.forEach((element) => {
|
||||
if (element.classList.contains("cn-menu-target")) {
|
||||
if (isInvertedMenu) {
|
||||
element.classList.add("dark")
|
||||
} else {
|
||||
element.classList.remove("dark")
|
||||
}
|
||||
}
|
||||
|
||||
// When translucent is enabled, move from data-attr to class so styles apply.
|
||||
// When disabled, move back to a data-attr so the element stays queryable
|
||||
// for future toggles without losing its identity as a menu element.
|
||||
if (isTranslucentMenu) {
|
||||
element.classList.add("cn-menu-translucent")
|
||||
element.removeAttribute("data-menu-translucent")
|
||||
} else if (element.classList.contains("cn-menu-translucent")) {
|
||||
element.classList.remove("cn-menu-translucent")
|
||||
element.setAttribute("data-menu-translucent", "")
|
||||
}
|
||||
})
|
||||
|
||||
// Force a reflow, then re-enable transitions.
|
||||
void document.body.offsetHeight
|
||||
allElements.forEach((element) => {
|
||||
element.style.transition = ""
|
||||
})
|
||||
}
|
||||
|
||||
const scheduleMenuUpdate = () => {
|
||||
if (frameId) {
|
||||
return
|
||||
}
|
||||
|
||||
frameId = window.requestAnimationFrame(() => {
|
||||
frameId = 0
|
||||
updateMenuElements()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -178,7 +283,7 @@ export function DesignSystemProvider({
|
||||
|
||||
// Watch for new menu elements being added to the DOM.
|
||||
const observer = new MutationObserver(() => {
|
||||
updateMenuElements()
|
||||
scheduleMenuUpdate()
|
||||
})
|
||||
|
||||
observer.observe(document.body, {
|
||||
@@ -188,6 +293,9 @@ export function DesignSystemProvider({
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
if (frameId) {
|
||||
window.cancelAnimationFrame(frameId)
|
||||
}
|
||||
}
|
||||
}, [menuColor])
|
||||
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { type FontValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
@@ -20,28 +13,68 @@ import {
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { type Font } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
type FontPickerOption = {
|
||||
name: string
|
||||
value: string
|
||||
type: string
|
||||
font: {
|
||||
style: {
|
||||
fontFamily: string
|
||||
}
|
||||
} | null
|
||||
}
|
||||
|
||||
export function FontPicker({
|
||||
label,
|
||||
param,
|
||||
fonts,
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
fonts: readonly Font[]
|
||||
label: string
|
||||
param: "font" | "fontHeading"
|
||||
fonts: readonly FontPickerOption[]
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const currentValue = param === "font" ? params.font : params.fontHeading
|
||||
const handleFontChange = React.useCallback(
|
||||
(value: string) => {
|
||||
setParams({
|
||||
[param]: value,
|
||||
} as Partial<DesignSystemSearchParams>)
|
||||
},
|
||||
[param, setParams]
|
||||
)
|
||||
|
||||
const currentFont = React.useMemo(
|
||||
() => fonts.find((font) => font.value === params.font),
|
||||
[fonts, params.font]
|
||||
() => fonts.find((font) => font.value === currentValue),
|
||||
[fonts, currentValue]
|
||||
)
|
||||
const currentBodyFont = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.font),
|
||||
[params.font]
|
||||
)
|
||||
const inheritsBodyFont = param === "fontHeading" && currentValue === "inherit"
|
||||
const displayFontName = inheritsBodyFont
|
||||
? currentBodyFont?.name
|
||||
: currentFont?.name
|
||||
const inheritFontLabel = currentBodyFont ? currentBodyFont.name : "Body font"
|
||||
const groupedFonts = React.useMemo(() => {
|
||||
const groups = new Map<Font["type"], Font[]>()
|
||||
const pickerFonts =
|
||||
param === "fontHeading"
|
||||
? fonts.filter((font) => font.value !== "inherit")
|
||||
: fonts
|
||||
const groups = new Map<string, FontPickerOption[]>()
|
||||
|
||||
for (const font of fonts) {
|
||||
for (const font of pickerFonts) {
|
||||
const existing = groups.get(font.type)
|
||||
if (existing) {
|
||||
existing.push(font)
|
||||
@@ -56,21 +89,25 @@ export function FontPicker({
|
||||
label: `${type.charAt(0).toUpperCase()}${type.slice(1)}`,
|
||||
items,
|
||||
}))
|
||||
}, [fonts])
|
||||
}, [fonts, param])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Font</div>
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{currentFont?.name}
|
||||
{displayFontName}
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
style={{ fontFamily: currentFont?.font.style.fontFamily }}
|
||||
style={{
|
||||
fontFamily:
|
||||
currentFont?.font?.style.fontFamily ??
|
||||
currentBodyFont?.font.style.fontFamily,
|
||||
}}
|
||||
>
|
||||
Aa
|
||||
</div>
|
||||
@@ -82,11 +119,19 @@ export function FontPicker({
|
||||
className="max-h-96"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentFont?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ font: value as FontValue })
|
||||
}}
|
||||
value={currentValue}
|
||||
onValueChange={handleFontChange}
|
||||
>
|
||||
{param === "fontHeading" ? (
|
||||
<>
|
||||
<PickerGroup>
|
||||
<PickerRadioItem value="inherit" closeOnClick={isMobile}>
|
||||
{inheritFontLabel}
|
||||
</PickerRadioItem>
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
</>
|
||||
) : null}
|
||||
{groupedFonts.map((group) => (
|
||||
<PickerGroup key={group.type}>
|
||||
<PickerLabel>{group.label}</PickerLabel>
|
||||
@@ -105,7 +150,7 @@ export function FontPicker({
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="font"
|
||||
param={param}
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"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 { Button } from "@/styles/base-nova/ui/button"
|
||||
import { useHistory } from "@/app/(create)/hooks/use-history"
|
||||
|
||||
export const UNDO_FORWARD_TYPE = "undo-forward"
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
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 "@/examples/base/ui/collapsible"
|
||||
} from "@/styles/base-nova/ui/collapsible"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -15,12 +20,7 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} 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"
|
||||
} from "@/styles/base-nova/ui/sidebar"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"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 { type Button } from "@/styles/base-nova/ui/button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
@@ -73,7 +73,7 @@ export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
|
||||
</PickerItem>
|
||||
<PickerSeparator />
|
||||
<PickerItem onClick={() => setShowResetDialog(true)}>
|
||||
Reset
|
||||
Reset <PickerShortcut>⇧R</PickerShortcut>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerContent>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Menu02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
@@ -10,99 +12,37 @@ import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerLabel,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import {
|
||||
isTranslucentMenuColor,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
const MENU_OPTIONS = [
|
||||
{
|
||||
value: "default" as const,
|
||||
label: "Default",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
className="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"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M8.5 11.5L14.5001 11.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.5 15H13.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 8H15.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "inverted" as const,
|
||||
label: "Inverted",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
className="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"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M8.5 11.5L14.5001 11.5"
|
||||
stroke="var(--foreground)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.5 15H13.5"
|
||||
stroke="var(--foreground)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 8H15.5"
|
||||
stroke="var(--foreground)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
] as const
|
||||
type ColorChoice = "default" | "inverted"
|
||||
type SurfaceChoice = "solid" | "translucent"
|
||||
|
||||
function getMenuColorValue(
|
||||
color: ColorChoice,
|
||||
translucent: boolean
|
||||
): MenuColorValue {
|
||||
if (color === "default") {
|
||||
return translucent ? "default-translucent" : "default"
|
||||
}
|
||||
|
||||
return translucent ? "inverted-translucent" : "inverted"
|
||||
}
|
||||
|
||||
const MENU_OPTIONS: { value: MenuColorValue; label: string }[] = [
|
||||
{ value: "default", label: "Default / Solid" },
|
||||
{ value: "default-translucent", label: "Default / Translucent" },
|
||||
{ value: "inverted", label: "Inverted / Solid" },
|
||||
{ value: "inverted-translucent", label: "Inverted / Translucent" },
|
||||
]
|
||||
|
||||
export function MenuColorPicker({
|
||||
isMobile,
|
||||
@@ -111,25 +51,69 @@ export function MenuColorPicker({
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const lastSolidMenuAccentRef = React.useRef(params.menuAccent)
|
||||
const isDark = mounted && resolvedTheme === "dark"
|
||||
const currentMenu = MENU_OPTIONS.find(
|
||||
(menu) => menu.value === params.menuColor
|
||||
)
|
||||
const colorChoice: ColorChoice =
|
||||
params.menuColor === "inverted" ||
|
||||
params.menuColor === "inverted-translucent"
|
||||
? "inverted"
|
||||
: "default"
|
||||
const surfaceChoice: SurfaceChoice =
|
||||
params.menuColor === "default-translucent" ||
|
||||
params.menuColor === "inverted-translucent"
|
||||
? "translucent"
|
||||
: "solid"
|
||||
|
||||
React.useEffect(() => {
|
||||
if (surfaceChoice === "solid") {
|
||||
lastSolidMenuAccentRef.current = params.menuAccent
|
||||
}
|
||||
}, [params.menuAccent, surfaceChoice])
|
||||
|
||||
const setColor = (color: ColorChoice) => {
|
||||
const nextMenuColor = getMenuColorValue(
|
||||
color,
|
||||
surfaceChoice === "translucent"
|
||||
)
|
||||
|
||||
setParams({
|
||||
menuColor: nextMenuColor,
|
||||
...(isTranslucentMenuColor(nextMenuColor) && { menuAccent: "subtle" }),
|
||||
})
|
||||
}
|
||||
|
||||
const setSurface = (choice: SurfaceChoice) => {
|
||||
const isTranslucent = choice === "translucent"
|
||||
const nextMenuColor = getMenuColorValue(colorChoice, isTranslucent)
|
||||
|
||||
setParams({
|
||||
menuColor: nextMenuColor,
|
||||
menuAccent: isTranslucent ? "subtle" : lastSolidMenuAccentRef.current,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger disabled={mounted && resolvedTheme === "dark"}>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Menu Color</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="text-xs text-muted-foreground">Menu</div>
|
||||
<div className="line-clamp-1 max-w-[80%] truncate text-sm font-medium text-foreground">
|
||||
{currentMenu?.label}
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
<HugeiconsIcon
|
||||
icon={Menu02Icon}
|
||||
strokeWidth={2}
|
||||
className="size-4"
|
||||
/>
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
@@ -137,24 +121,43 @@ export function MenuColorPicker({
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentMenu?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuColor: value as MenuColorValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_OPTIONS.map((menu) => (
|
||||
<PickerRadioItem
|
||||
key={menu.value}
|
||||
value={menu.value}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{menu.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
<PickerGroup>
|
||||
<PickerLabel>Color</PickerLabel>
|
||||
<PickerRadioGroup
|
||||
value={colorChoice}
|
||||
onValueChange={(value) => {
|
||||
setColor(value as ColorChoice)
|
||||
}}
|
||||
>
|
||||
<PickerRadioItem value="default" closeOnClick={isMobile}>
|
||||
Default
|
||||
</PickerRadioItem>
|
||||
<PickerRadioItem
|
||||
value="inverted"
|
||||
closeOnClick={isMobile}
|
||||
disabled={isDark}
|
||||
>
|
||||
Inverted
|
||||
</PickerRadioItem>
|
||||
</PickerRadioGroup>
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerLabel>Appearance</PickerLabel>
|
||||
<PickerRadioGroup
|
||||
value={surfaceChoice}
|
||||
onValueChange={(value) => {
|
||||
setSurface(value as SurfaceChoice)
|
||||
}}
|
||||
>
|
||||
<PickerRadioItem value="solid" closeOnClick={isMobile}>
|
||||
Solid
|
||||
</PickerRadioItem>
|
||||
<PickerRadioItem value="translucent" closeOnClick={isMobile}>
|
||||
Translucent
|
||||
</PickerRadioItem>
|
||||
</PickerRadioGroup>
|
||||
</PickerGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import * as React from "react"
|
||||
import Script from "next/script"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
|
||||
|
||||
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
|
||||
|
||||
@@ -31,8 +31,10 @@ export function PresetPicker({
|
||||
preset.style === params.style &&
|
||||
preset.baseColor === params.baseColor &&
|
||||
preset.theme === params.theme &&
|
||||
preset.chartColor === params.chartColor &&
|
||||
preset.iconLibrary === params.iconLibrary &&
|
||||
preset.font === params.font &&
|
||||
preset.fontHeading === params.fontHeading &&
|
||||
preset.menuAccent === params.menuAccent &&
|
||||
preset.menuColor === params.menuColor &&
|
||||
preset.radius === params.radius
|
||||
@@ -43,8 +45,10 @@ export function PresetPicker({
|
||||
params.style,
|
||||
params.baseColor,
|
||||
params.theme,
|
||||
params.chartColor,
|
||||
params.iconLibrary,
|
||||
params.font,
|
||||
params.fontHeading,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
params.radius,
|
||||
@@ -67,8 +71,10 @@ export function PresetPicker({
|
||||
style: preset.style,
|
||||
baseColor: preset.baseColor,
|
||||
theme: preset.theme,
|
||||
chartColor: preset.chartColor,
|
||||
iconLibrary: preset.iconLibrary,
|
||||
font: preset.font,
|
||||
fontHeading: preset.fontHeading,
|
||||
menuAccent: preset.menuAccent,
|
||||
menuColor: preset.menuColor,
|
||||
radius: preset.radius,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
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 { RESET_FORWARD_TYPE } from "@/app/(create)/hooks/use-reset"
|
||||
import {
|
||||
serializeDesignSystemSearchParams,
|
||||
useDesignSystemSearchParams,
|
||||
@@ -70,6 +71,15 @@ function handleMessage(event: MessageEvent) {
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === RESET_FORWARD_TYPE) {
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: "R",
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === DARK_MODE_FORWARD_TYPE) {
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
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 { BASES, type BaseName } from "@/registry/config"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -10,7 +17,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/examples/base/ui/dialog"
|
||||
} from "@/styles/base-nova/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
@@ -20,21 +27,15 @@ import {
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/base/ui/field"
|
||||
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui/radio-group"
|
||||
import { Switch } from "@/examples/base/ui/switch"
|
||||
} from "@/styles/base-nova/ui/field"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/base-nova/ui/radio-group"
|
||||
import { Switch } from "@/styles/base-nova/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"
|
||||
} from "@/styles/base-nova/ui/tabs"
|
||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
@@ -86,7 +87,7 @@ export function ProjectForm({
|
||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
|
||||
|
||||
return IS_LOCAL_DEV && !process.env.NEXT_PUBLIC_RC
|
||||
return IS_LOCAL_DEV
|
||||
? {
|
||||
pnpm: `shadcn init${flags}`,
|
||||
npm: `shadcn init${flags}`,
|
||||
@@ -129,69 +130,76 @@ export function ProjectForm({
|
||||
<DialogTrigger render={<Button className={cn(className)} />}>
|
||||
Create Project
|
||||
</DialogTrigger>
|
||||
<DialogContent className="min-w-0 sm:max-w-sm">
|
||||
<DialogContent className="dark no-scrollbar max-h-[calc(100svh-2rem)] overflow-y-auto rounded-2xl p-6 shadow-xl **:data-[slot=field-separator]:h-2 sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pick a template and configure your project. Available for all major
|
||||
React frameworks.
|
||||
Pick a template and configure your project.
|
||||
</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,
|
||||
<div>
|
||||
<FieldGroup>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field className="-mt-2 gap-3">
|
||||
<FieldLabel>Template</FieldLabel>
|
||||
<TemplateGrid template={params.template} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field className="-mt-2">
|
||||
<FieldLabel>Base</FieldLabel>
|
||||
<BaseGrid base={params.base} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label" className="sr-only">
|
||||
Options
|
||||
</FieldLegend>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-disabled={hasMonorepo ? undefined : "true"}
|
||||
>
|
||||
<FieldLabel htmlFor="monorepo">
|
||||
<span
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 [&_svg]:fill-current"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: TURBOREPO_LOGO,
|
||||
}}
|
||||
/>
|
||||
Create a monorepo
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="monorepo"
|
||||
checked={params.template?.endsWith("-monorepo") ?? false}
|
||||
disabled={!hasMonorepo}
|
||||
onCheckedChange={(checked) => {
|
||||
const framework = getFramework(params.template ?? "next")
|
||||
setParams({
|
||||
template: getTemplateValue(
|
||||
framework,
|
||||
checked === true
|
||||
) as typeof params.template,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
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">
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="rtl">
|
||||
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
|
||||
Enable RTL support
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="rtl"
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ rtl: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
<DialogFooter className="-mx-6 -mb-6 min-w-0">
|
||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
@@ -201,16 +209,16 @@ export function ProjectForm({
|
||||
packageManager: value as PackageManager,
|
||||
}))
|
||||
}}
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-lg border bg-surface"
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-1 py-1">
|
||||
<TabsList className="font-mono">
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
|
||||
<TabsList className="bg-transparent font-mono">
|
||||
{PACKAGE_MANAGERS.map((manager) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={manager}
|
||||
value={manager}
|
||||
className="data-[state=active]:shadow-none"
|
||||
className="py-0 leading-none data-[state=active]:shadow-none"
|
||||
>
|
||||
{manager}
|
||||
</TabsTrigger>
|
||||
@@ -234,7 +242,7 @@ export function ProjectForm({
|
||||
{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="relative overflow-hidden border-t bg-popover p-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
@@ -281,23 +289,26 @@ const TemplateGrid = React.memo(function TemplateGrid({
|
||||
<RadioGroup
|
||||
value={framework}
|
||||
onValueChange={handleTemplateChange}
|
||||
className="grid grid-cols-3 gap-2"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{TEMPLATES.map((item) => (
|
||||
<FieldLabel
|
||||
key={item.value}
|
||||
htmlFor={`template-${item.value}`}
|
||||
className="py-1"
|
||||
className="block w-full"
|
||||
>
|
||||
<Field className="gap-0" orientation="horizontal">
|
||||
<FieldContent className="flex flex-col items-center justify-center gap-2">
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="w-full rounded-md transition-colors duration-150 hover:bg-neutral-700/45"
|
||||
>
|
||||
<FieldContent className="flex flex-row items-center gap-2 px-2.5 py-1.5">
|
||||
<div
|
||||
className="size-6 text-foreground [&_svg]:size-6 *:[svg]:text-foreground!"
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.logo,
|
||||
}}
|
||||
></div>
|
||||
<FieldTitle className="text-xs">{item.title}</FieldTitle>
|
||||
<FieldTitle>{item.title}</FieldTitle>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={item.value}
|
||||
@@ -310,3 +321,55 @@ const TemplateGrid = React.memo(function TemplateGrid({
|
||||
</RadioGroup>
|
||||
)
|
||||
})
|
||||
|
||||
const BaseGrid = React.memo(function BaseGrid({
|
||||
base,
|
||||
setParams,
|
||||
}: {
|
||||
base: DesignSystemSearchParams["base"]
|
||||
setParams: ReturnType<typeof useDesignSystemSearchParams>[1]
|
||||
}) {
|
||||
const handleBaseChange = React.useCallback(
|
||||
(value: string) => {
|
||||
setParams({ base: value as BaseName })
|
||||
},
|
||||
[setParams]
|
||||
)
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
value={base}
|
||||
onValueChange={handleBaseChange}
|
||||
aria-label="Base"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{BASES.map((item) => (
|
||||
<FieldLabel
|
||||
key={item.name}
|
||||
htmlFor={`base-${item.name}`}
|
||||
className="block w-full"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="w-full rounded-md transition-colors duration-150 hover:bg-neutral-700/45"
|
||||
>
|
||||
<FieldContent className="flex flex-row items-center gap-2 px-2.5 py-1.5">
|
||||
<div
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.meta?.logo ?? "",
|
||||
}}
|
||||
/>
|
||||
<FieldTitle>{item.title}</FieldTitle>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={item.name}
|
||||
id={`base-${item.name}`}
|
||||
className="sr-only absolute"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client"
|
||||
|
||||
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 { cn } from "@/lib/utils"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { useRandom } from "@/app/(create)/hooks/use-random"
|
||||
import { RESET_FORWARD_TYPE } from "@/app/(create)/hooks/use-reset"
|
||||
|
||||
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
|
||||
|
||||
@@ -40,7 +41,7 @@ export function RandomizeScript() {
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
// Forward R key
|
||||
// Forward r key (shuffle) and Shift+R (reset).
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'r' || e.key === 'R') && !e.metaKey && !e.ctrlKey) {
|
||||
if (
|
||||
@@ -53,8 +54,11 @@ export function RandomizeScript() {
|
||||
}
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
var type = e.shiftKey
|
||||
? '${RESET_FORWARD_TYPE}'
|
||||
: '${RANDOMIZE_FORWARD_TYPE}';
|
||||
window.parent.postMessage({
|
||||
type: '${RANDOMIZE_FORWARD_TYPE}',
|
||||
type: type,
|
||||
key: e.key
|
||||
}, '*');
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/examples/base/ui/alert-dialog"
|
||||
|
||||
} from "@/styles/base-nova/ui/alert-dialog"
|
||||
import { useReset } from "@/app/(create)/hooks/use-reset"
|
||||
|
||||
export function ResetDialog() {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"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 "@/styles/base-nova/ui/button"
|
||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
|
||||
@@ -1,13 +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 "@/styles/base-nova/ui/button"
|
||||
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function V0Button({ className }: { className?: string }) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/base/ui/button"
|
||||
|
||||
import { Icons } from "@/components/icons"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -10,9 +12,7 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/examples/base/ui/dialog"
|
||||
|
||||
import { Icons } from "@/components/icons"
|
||||
} from "@/styles/base-nova/ui/dialog"
|
||||
|
||||
const STORAGE_KEY = "shadcn-create-welcome-dialog"
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function CreatePage() {
|
||||
return (
|
||||
<div
|
||||
data-slot="layout"
|
||||
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)]"
|
||||
className="group/layout relative z-10 flex h-svh flex-col overflow-hidden section-soft [--customizer-width:--spacing(48)] [--gap:--spacing(4)] md:[--gap:--spacing(6)] 2xl:[--customizer-width:--spacing(56)]"
|
||||
>
|
||||
<SiteHeader />
|
||||
<main
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { encodePreset, isPresetCode } from "shadcn/preset"
|
||||
|
||||
import { getPresetCode } from "@/app/(create)/lib/preset-code"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
// Returns the current preset code derived from search params.
|
||||
// Returns the canonical preset code derived from the current 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,
|
||||
])
|
||||
return getPresetCode(params)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ export type LockableParam =
|
||||
| "style"
|
||||
| "baseColor"
|
||||
| "theme"
|
||||
| "chartColor"
|
||||
| "iconLibrary"
|
||||
| "font"
|
||||
| "fontHeading"
|
||||
| "menuAccent"
|
||||
| "menuColor"
|
||||
| "radius"
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
MENU_COLORS,
|
||||
RADII,
|
||||
STYLES,
|
||||
type FontHeadingValue,
|
||||
} from "@/registry/config"
|
||||
import { useLocks } from "@/app/(create)/hooks/use-locks"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
@@ -18,7 +19,10 @@ import {
|
||||
RANDOMIZE_BIASES,
|
||||
type RandomizeContext,
|
||||
} from "@/app/(create)/lib/randomize-biases"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import {
|
||||
isTranslucentMenuColor,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
function randomItem<T>(array: readonly T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
@@ -59,32 +63,77 @@ export function useRandom() {
|
||||
const selectedTheme = locks.has("theme")
|
||||
? paramsRef.current.theme
|
||||
: randomItem(availableThemes).name
|
||||
context.theme = selectedTheme
|
||||
|
||||
const availableChartColors = applyBias(
|
||||
getThemesForBaseColor(baseColor),
|
||||
context,
|
||||
RANDOMIZE_BIASES.chartColors
|
||||
)
|
||||
const selectedChartColor = locks.has("chartColor")
|
||||
? paramsRef.current.chartColor
|
||||
: randomItem(availableChartColors).name
|
||||
context.chartColor = selectedChartColor
|
||||
const selectedFont = locks.has("font")
|
||||
? paramsRef.current.font
|
||||
: randomItem(availableFonts).value
|
||||
context.font = selectedFont
|
||||
|
||||
// Pick heading font: ~70% inherit, ~30% distinct with cross-category contrast.
|
||||
let selectedFontHeading: FontHeadingValue
|
||||
if (locks.has("fontHeading")) {
|
||||
selectedFontHeading = paramsRef.current.fontHeading
|
||||
} else if (Math.random() < 0.7) {
|
||||
selectedFontHeading = "inherit"
|
||||
} else {
|
||||
const bodyType = availableFonts.find(
|
||||
(f) => f.value === selectedFont
|
||||
)?.type
|
||||
const contrastFonts = availableFonts.filter(
|
||||
(f) => f.type !== bodyType && f.value !== selectedFont
|
||||
)
|
||||
selectedFontHeading = (
|
||||
contrastFonts.length > 0
|
||||
? randomItem(contrastFonts)
|
||||
: randomItem(availableFonts)
|
||||
).value as FontHeadingValue
|
||||
}
|
||||
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")
|
||||
const lockedMenuAccent = locks.has("menuAccent")
|
||||
? paramsRef.current.menuAccent
|
||||
: randomItem(MENU_ACCENTS).value
|
||||
: undefined
|
||||
const availableMenuColors =
|
||||
!locks.has("menuColor") && lockedMenuAccent === "bold"
|
||||
? MENU_COLORS.filter((menuColor) => {
|
||||
return !isTranslucentMenuColor(menuColor.value)
|
||||
})
|
||||
: MENU_COLORS
|
||||
const selectedMenuColor = locks.has("menuColor")
|
||||
? paramsRef.current.menuColor
|
||||
: randomItem(MENU_COLORS).value
|
||||
: randomItem(availableMenuColors).value
|
||||
const selectedMenuAccent =
|
||||
locks.has("menuAccent") || isTranslucentMenuColor(selectedMenuColor)
|
||||
? paramsRef.current.menuAccent === "bold" &&
|
||||
isTranslucentMenuColor(selectedMenuColor)
|
||||
? "subtle"
|
||||
: paramsRef.current.menuAccent
|
||||
: randomItem(MENU_ACCENTS).value
|
||||
|
||||
context.theme = selectedTheme
|
||||
context.font = selectedFont
|
||||
context.radius = selectedRadius
|
||||
|
||||
const nextParams = {
|
||||
style: selectedStyle,
|
||||
baseColor,
|
||||
theme: selectedTheme,
|
||||
chartColor: selectedChartColor,
|
||||
iconLibrary: selectedIconLibrary,
|
||||
font: selectedFont,
|
||||
fontHeading: selectedFontHeading,
|
||||
menuAccent: selectedMenuAccent,
|
||||
menuColor: selectedMenuColor,
|
||||
radius: selectedRadius,
|
||||
@@ -107,7 +156,7 @@ export function useRandom() {
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if ((e.key === "r" || e.key === "R") && !e.metaKey && !e.ctrlKey) {
|
||||
if (e.key === "r" && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DEFAULT_CONFIG } from "@/registry/config"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const RESET_DIALOG_KEY = "create:reset-dialog-open"
|
||||
export const RESET_FORWARD_TYPE = "reset-forward"
|
||||
|
||||
export function useReset() {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
@@ -24,15 +25,17 @@ export function useReset() {
|
||||
style: DEFAULT_CONFIG.style,
|
||||
baseColor: DEFAULT_CONFIG.baseColor,
|
||||
theme: DEFAULT_CONFIG.theme,
|
||||
chartColor: DEFAULT_CONFIG.chartColor,
|
||||
iconLibrary: DEFAULT_CONFIG.iconLibrary,
|
||||
font: DEFAULT_CONFIG.font,
|
||||
fontHeading: DEFAULT_CONFIG.fontHeading,
|
||||
menuAccent: DEFAULT_CONFIG.menuAccent,
|
||||
menuColor: DEFAULT_CONFIG.menuColor,
|
||||
radius: DEFAULT_CONFIG.radius,
|
||||
template: DEFAULT_CONFIG.template,
|
||||
item: "preview",
|
||||
item: params.item,
|
||||
})
|
||||
}, [setParams, params.base])
|
||||
}, [setParams, params.base, params.item])
|
||||
|
||||
const handleShowResetDialogChange = React.useCallback(
|
||||
(open: boolean) => {
|
||||
@@ -46,6 +49,46 @@ export function useReset() {
|
||||
void setShowResetDialogData(false, { revalidate: false })
|
||||
}, [reset, setShowResetDialogData])
|
||||
|
||||
const showResetDialogRef = React.useRef(showResetDialog)
|
||||
React.useEffect(() => {
|
||||
showResetDialogRef.current = showResetDialog
|
||||
}, [showResetDialog])
|
||||
|
||||
const confirmResetRef = React.useRef(confirmReset)
|
||||
React.useEffect(() => {
|
||||
confirmResetRef.current = confirmReset
|
||||
}, [confirmReset])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "R" && e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
// If the dialog is already open, confirm the reset.
|
||||
if (showResetDialogRef.current) {
|
||||
confirmResetRef.current()
|
||||
return
|
||||
}
|
||||
|
||||
handleShowResetDialogChange(true)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => {
|
||||
document.removeEventListener("keydown", down)
|
||||
}
|
||||
}, [handleShowResetDialogChange])
|
||||
|
||||
return {
|
||||
reset,
|
||||
showResetDialog,
|
||||
|
||||
@@ -3,13 +3,17 @@ import dedent from "dedent"
|
||||
import { UI_COMPONENTS } from "@/lib/components"
|
||||
import {
|
||||
buildRegistryBase,
|
||||
fonts,
|
||||
getBodyFont,
|
||||
getHeadingFont,
|
||||
getInheritedHeadingFontValue,
|
||||
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 normalizedFontHeading =
|
||||
config.fontHeading === config.font ? "inherit" : config.fontHeading
|
||||
|
||||
const dependencies = [
|
||||
...(registryBase.dependencies ?? []),
|
||||
@@ -25,13 +29,23 @@ export function buildInstructions(config: DesignSystemConfig) {
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
const font = fonts.find((f) => f.name === `font-${config.font}`)
|
||||
const font = getBodyFont(config.font)
|
||||
const headingFont =
|
||||
normalizedFontHeading === "inherit"
|
||||
? undefined
|
||||
: getHeadingFont(normalizedFontHeading)
|
||||
|
||||
const sections = [
|
||||
buildDependenciesSection(dependencies),
|
||||
buildUtilsSection(),
|
||||
buildCssSection(lightVars, darkVars),
|
||||
buildFontSection(font),
|
||||
buildCssSection(
|
||||
lightVars,
|
||||
darkVars,
|
||||
normalizedFontHeading === "inherit"
|
||||
? getInheritedHeadingFontValue(config.font)
|
||||
: "var(--font-heading)"
|
||||
),
|
||||
buildFontSection(font, headingFont),
|
||||
buildComponentsJsonSection(config),
|
||||
buildAvailableComponentsSection(config),
|
||||
config.rtl ? buildRtlSection(config) : null,
|
||||
@@ -67,7 +81,11 @@ function buildUtilsSection() {
|
||||
`
|
||||
}
|
||||
|
||||
function buildCssSection(lightVars: string, darkVars: string) {
|
||||
function buildCssSection(
|
||||
lightVars: string,
|
||||
darkVars: string,
|
||||
fontHeadingValue: string
|
||||
) {
|
||||
return dedent`
|
||||
## Step 3: Set up CSS
|
||||
|
||||
@@ -80,6 +98,7 @@ function buildCssSection(lightVars: string, darkVars: string) {
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-sans);
|
||||
--font-heading: ${fontHeadingValue};
|
||||
--font-mono: var(--font-mono);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
@@ -142,40 +161,74 @@ function buildCssSection(lightVars: string, darkVars: string) {
|
||||
`
|
||||
}
|
||||
|
||||
function buildFontSection(font: (typeof fonts)[number] | undefined) {
|
||||
function buildFontSection(
|
||||
font: ReturnType<typeof getBodyFont>,
|
||||
headingFont: ReturnType<typeof getHeadingFont>
|
||||
) {
|
||||
if (!font) {
|
||||
return null
|
||||
}
|
||||
|
||||
const googleFontsUrl = `https://fonts.google.com/specimen/${font.font.import.replace(/_/g, "+")}`
|
||||
const headingGoogleFontsUrl = headingFont
|
||||
? `https://fonts.google.com/specimen/${headingFont.font.import.replace(/_/g, "+")}`
|
||||
: null
|
||||
const nextImports = headingFont
|
||||
? `${font.font.import}, ${headingFont.font.import}`
|
||||
: font.font.import
|
||||
const nextDeclarations = [
|
||||
`const fontSans = ${font.font.import}({`,
|
||||
` subsets: ["latin"],`,
|
||||
` variable: "${font.font.variable}",`,
|
||||
`})`,
|
||||
headingFont ? `const fontHeading = ${headingFont.font.import}({` : null,
|
||||
headingFont ? ` subsets: ["latin"],` : null,
|
||||
headingFont ? ` variable: "${headingFont.font.variable}",` : null,
|
||||
headingFont ? `})` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
const nextHtmlClassName = headingFont
|
||||
? 'className={fontSans.variable + " " + fontHeading.variable}'
|
||||
: `className={fontSans.variable}`
|
||||
const otherFrameworkCss = [
|
||||
":root {",
|
||||
` ${font.font.variable}: ${font.font.family};`,
|
||||
...(headingFont
|
||||
? [` ${headingFont.font.variable}: ${headingFont.font.family};`]
|
||||
: []),
|
||||
"}",
|
||||
].join("\n")
|
||||
const headingSection = headingFont
|
||||
? dedent`
|
||||
|
||||
This config also uses **${headingFont.title.replace(" (Heading)", "")}** for headings (\`${headingFont.font.variable}\`).
|
||||
`
|
||||
: ""
|
||||
|
||||
return dedent`
|
||||
## Step 4: Set up the font
|
||||
|
||||
This config uses **${font.title}** (\`${font.font.variable}\`).
|
||||
${headingSection}
|
||||
|
||||
### Next.js
|
||||
|
||||
\`\`\`tsx
|
||||
import { ${font.font.import} } from "next/font/google"
|
||||
import { ${nextImports} } from "next/font/google"
|
||||
|
||||
const fontSans = ${font.font.import}({
|
||||
subsets: ["latin"],
|
||||
variable: "${font.font.variable}",
|
||||
})
|
||||
${nextDeclarations}
|
||||
|
||||
// Add fontSans.variable to your <html> className.
|
||||
// <html className={fontSans.variable}>
|
||||
// Add the font variable classes to your <html> className.
|
||||
// <html ${nextHtmlClassName}>
|
||||
\`\`\`
|
||||
|
||||
### Other frameworks
|
||||
|
||||
Add the font from [Google Fonts](${googleFontsUrl}) and set the \`${font.font.variable}\` CSS variable to the font family:
|
||||
Add the font${headingFont ? "s" : ""} from [Google Fonts](${googleFontsUrl})${headingGoogleFontsUrl ? ` and [Google Fonts](${headingGoogleFontsUrl})` : ""} and set the CSS variable${headingFont ? "s" : ""} to the font famil${headingFont ? "ies" : "y"}:
|
||||
|
||||
\`\`\`css
|
||||
:root {
|
||||
${font.font.variable}: ${font.font.family};
|
||||
}
|
||||
${otherFrameworkCss}
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
21
apps/v4/app/(create)/init/parse-config.test.ts
Normal file
21
apps/v4/app/(create)/init/parse-config.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { parseDesignSystemConfig } from "./parse-config"
|
||||
|
||||
describe("parseDesignSystemConfig", () => {
|
||||
it("honors explicit fontHeading and chartColor overrides when a preset is present", () => {
|
||||
const result = parseDesignSystemConfig(
|
||||
new URLSearchParams(
|
||||
"preset=a0&fontHeading=playfair-display&chartColor=emerald"
|
||||
)
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
expect(result.data.fontHeading).toBe("playfair-display")
|
||||
expect(result.data.chartColor).toBe("emerald")
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
designSystemConfigSchema,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
import { resolvePresetOverrides } from "@/app/(create)/lib/preset-query"
|
||||
|
||||
// Parses design system config from URL search params.
|
||||
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||
@@ -15,8 +16,10 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||
if (!decoded) {
|
||||
return { success: false as const, error: "Invalid preset code" }
|
||||
}
|
||||
const presetOverrides = resolvePresetOverrides(searchParams, decoded)
|
||||
configInput = {
|
||||
...decoded,
|
||||
...presetOverrides,
|
||||
base: searchParams.get("base") ?? "radix",
|
||||
template: searchParams.get("template") ?? undefined,
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
@@ -28,7 +31,9 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||
iconLibrary: searchParams.get("iconLibrary"),
|
||||
baseColor: searchParams.get("baseColor"),
|
||||
theme: searchParams.get("theme"),
|
||||
chartColor: searchParams.get("chartColor") ?? undefined,
|
||||
font: searchParams.get("font"),
|
||||
fontHeading: searchParams.get("fontHeading") ?? undefined,
|
||||
menuAccent: searchParams.get("menuAccent"),
|
||||
menuColor: searchParams.get("menuColor"),
|
||||
radius: searchParams.get("radius"),
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { NextResponse, type NextRequest } from "next/server"
|
||||
import { track } from "@vercel/analytics/server"
|
||||
import { isPresetCode } from "shadcn/preset"
|
||||
import { registryItemSchema } from "shadcn/schema"
|
||||
|
||||
import { buildRegistryBase } from "@/registry/config"
|
||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||
import { getPresetCode } from "@/app/(create)/lib/preset-code"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -14,6 +16,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const rawPreset = searchParams.get("preset")
|
||||
const presetCode =
|
||||
rawPreset && isPresetCode(rawPreset)
|
||||
? rawPreset
|
||||
: getPresetCode(result.data)
|
||||
|
||||
const registryBase = buildRegistryBase(result.data)
|
||||
const parseResult = registryItemSchema.safeParse(registryBase)
|
||||
|
||||
@@ -27,10 +35,12 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
track("create_app", {
|
||||
...result.data,
|
||||
preset: searchParams.get("preset") ?? "",
|
||||
})
|
||||
if (searchParams.get("track") === "1") {
|
||||
track("create_app", {
|
||||
...result.data,
|
||||
preset: presetCode,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(parseResult.data)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { after, NextResponse, type NextRequest } from "next/server"
|
||||
import { track } from "@vercel/analytics/server"
|
||||
import { isPresetCode } from "shadcn/preset"
|
||||
|
||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||
import { getPresetCode } from "@/app/(create)/lib/preset-code"
|
||||
import { buildV0Payload } from "@/app/(create)/lib/v0"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
@@ -13,11 +15,17 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const rawPreset = searchParams.get("preset")
|
||||
const presetCode =
|
||||
rawPreset && isPresetCode(rawPreset)
|
||||
? rawPreset
|
||||
: getPresetCode(result.data)
|
||||
|
||||
// Defer analytics to after response is sent.
|
||||
after(() => {
|
||||
track("create_open_in_v0", {
|
||||
...result.data,
|
||||
preset: searchParams.get("preset") ?? "",
|
||||
preset: presetCode,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -3,21 +3,37 @@ import {
|
||||
Figtree,
|
||||
Geist,
|
||||
Geist_Mono,
|
||||
IBM_Plex_Sans,
|
||||
Instrument_Sans,
|
||||
Inter,
|
||||
JetBrains_Mono,
|
||||
Lora,
|
||||
Manrope,
|
||||
Merriweather,
|
||||
Montserrat,
|
||||
Noto_Sans,
|
||||
Noto_Serif,
|
||||
Nunito_Sans,
|
||||
Outfit,
|
||||
Oxanium,
|
||||
Playfair_Display,
|
||||
Public_Sans,
|
||||
Raleway,
|
||||
Roboto,
|
||||
Roboto_Slab,
|
||||
Source_Sans_3,
|
||||
Space_Grotesk,
|
||||
} from "next/font/google"
|
||||
|
||||
import { FONT_DEFINITIONS, type FontName } from "@/lib/font-definitions"
|
||||
|
||||
type PreviewFont = ReturnType<typeof Inter>
|
||||
|
||||
const geistSans = Geist({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-sans",
|
||||
})
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
@@ -38,21 +54,6 @@ const figtree = Figtree({
|
||||
variable: "--font-figtree",
|
||||
})
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-jetbrains-mono",
|
||||
})
|
||||
|
||||
const geistSans = Geist({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-sans",
|
||||
})
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-mono",
|
||||
})
|
||||
|
||||
const roboto = Roboto({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-roboto",
|
||||
@@ -78,6 +79,51 @@ const outfit = Outfit({
|
||||
variable: "--font-outfit",
|
||||
})
|
||||
|
||||
const oxanium = Oxanium({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-oxanium",
|
||||
})
|
||||
|
||||
const manrope = Manrope({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-manrope",
|
||||
})
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-space-grotesk",
|
||||
})
|
||||
|
||||
const montserrat = Montserrat({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-montserrat",
|
||||
})
|
||||
|
||||
const ibmPlexSans = IBM_Plex_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-ibm-plex-sans",
|
||||
})
|
||||
|
||||
const sourceSans3 = Source_Sans_3({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-source-sans-3",
|
||||
})
|
||||
|
||||
const instrumentSans = Instrument_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-instrument-sans",
|
||||
})
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-jetbrains-mono",
|
||||
})
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-geist-mono",
|
||||
})
|
||||
|
||||
const notoSerif = Noto_Serif({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-noto-serif",
|
||||
@@ -103,109 +149,85 @@ const playfairDisplay = Playfair_Display({
|
||||
variable: "--font-playfair-display",
|
||||
})
|
||||
|
||||
const PREVIEW_FONTS = {
|
||||
geist: geistSans,
|
||||
inter,
|
||||
"noto-sans": notoSans,
|
||||
"nunito-sans": nunitoSans,
|
||||
figtree,
|
||||
roboto,
|
||||
raleway,
|
||||
"dm-sans": dmSans,
|
||||
"public-sans": publicSans,
|
||||
outfit,
|
||||
oxanium,
|
||||
manrope,
|
||||
"space-grotesk": spaceGrotesk,
|
||||
montserrat,
|
||||
"ibm-plex-sans": ibmPlexSans,
|
||||
"source-sans-3": sourceSans3,
|
||||
"instrument-sans": instrumentSans,
|
||||
"jetbrains-mono": jetbrainsMono,
|
||||
"geist-mono": geistMono,
|
||||
"noto-serif": notoSerif,
|
||||
"roboto-slab": robotoSlab,
|
||||
merriweather,
|
||||
lora,
|
||||
"playfair-display": playfairDisplay,
|
||||
} satisfies Record<FontName, PreviewFont>
|
||||
|
||||
function createFontOption(name: FontName) {
|
||||
const definition = FONT_DEFINITIONS.find((font) => font.name === name)
|
||||
|
||||
if (!definition) {
|
||||
throw new Error(`Unknown font definition: ${name}`)
|
||||
}
|
||||
|
||||
return {
|
||||
name: definition.title,
|
||||
value: definition.name,
|
||||
font: PREVIEW_FONTS[name],
|
||||
type: definition.type,
|
||||
} as const
|
||||
}
|
||||
|
||||
export const FONTS = [
|
||||
{
|
||||
name: "Geist",
|
||||
value: "geist",
|
||||
font: geistSans,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
value: "inter",
|
||||
font: inter,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "Noto Sans",
|
||||
value: "noto-sans",
|
||||
font: notoSans,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "Nunito Sans",
|
||||
value: "nunito-sans",
|
||||
font: nunitoSans,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "Figtree",
|
||||
value: "figtree",
|
||||
font: figtree,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "Roboto",
|
||||
value: "roboto",
|
||||
font: roboto,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "Raleway",
|
||||
value: "raleway",
|
||||
font: raleway,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "DM Sans",
|
||||
value: "dm-sans",
|
||||
font: dmSans,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "Public Sans",
|
||||
value: "public-sans",
|
||||
font: publicSans,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "Outfit",
|
||||
value: "outfit",
|
||||
font: outfit,
|
||||
type: "sans",
|
||||
},
|
||||
{
|
||||
name: "Geist Mono",
|
||||
value: "geist-mono",
|
||||
font: geistMono,
|
||||
type: "mono",
|
||||
},
|
||||
{
|
||||
name: "JetBrains Mono",
|
||||
value: "jetbrains-mono",
|
||||
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",
|
||||
},
|
||||
createFontOption("geist"),
|
||||
createFontOption("inter"),
|
||||
createFontOption("noto-sans"),
|
||||
createFontOption("nunito-sans"),
|
||||
createFontOption("figtree"),
|
||||
createFontOption("roboto"),
|
||||
createFontOption("raleway"),
|
||||
createFontOption("dm-sans"),
|
||||
createFontOption("public-sans"),
|
||||
createFontOption("outfit"),
|
||||
createFontOption("oxanium"),
|
||||
createFontOption("manrope"),
|
||||
createFontOption("space-grotesk"),
|
||||
createFontOption("montserrat"),
|
||||
createFontOption("ibm-plex-sans"),
|
||||
createFontOption("source-sans-3"),
|
||||
createFontOption("instrument-sans"),
|
||||
createFontOption("geist-mono"),
|
||||
createFontOption("jetbrains-mono"),
|
||||
createFontOption("noto-serif"),
|
||||
createFontOption("roboto-slab"),
|
||||
createFontOption("merriweather"),
|
||||
createFontOption("lora"),
|
||||
createFontOption("playfair-display"),
|
||||
] as const
|
||||
|
||||
export type Font = (typeof FONTS)[number]
|
||||
|
||||
export const FONT_HEADING_OPTIONS = [
|
||||
{
|
||||
name: "Inherit",
|
||||
value: "inherit",
|
||||
font: null,
|
||||
type: "default",
|
||||
},
|
||||
...FONTS,
|
||||
] as const
|
||||
|
||||
export type FontHeadingOption = (typeof FONT_HEADING_OPTIONS)[number]
|
||||
|
||||
34
apps/v4/app/(create)/lib/preset-code.ts
Normal file
34
apps/v4/app/(create)/lib/preset-code.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { encodePreset, type PresetConfig } from "shadcn/preset"
|
||||
|
||||
import { type DesignSystemConfig } from "@/registry/config"
|
||||
|
||||
type PresetCodeConfig = Pick<
|
||||
DesignSystemConfig,
|
||||
| "style"
|
||||
| "baseColor"
|
||||
| "theme"
|
||||
| "chartColor"
|
||||
| "iconLibrary"
|
||||
| "font"
|
||||
| "fontHeading"
|
||||
| "radius"
|
||||
| "menuAccent"
|
||||
| "menuColor"
|
||||
>
|
||||
|
||||
export function getPresetCode(config: PresetCodeConfig) {
|
||||
const presetConfig: Partial<PresetConfig> = {
|
||||
style: config.style as PresetConfig["style"],
|
||||
baseColor: config.baseColor as PresetConfig["baseColor"],
|
||||
theme: config.theme as PresetConfig["theme"],
|
||||
chartColor: config.chartColor as PresetConfig["chartColor"],
|
||||
iconLibrary: config.iconLibrary as PresetConfig["iconLibrary"],
|
||||
font: config.font as PresetConfig["font"],
|
||||
fontHeading: config.fontHeading as PresetConfig["fontHeading"],
|
||||
radius: config.radius as PresetConfig["radius"],
|
||||
menuAccent: config.menuAccent as PresetConfig["menuAccent"],
|
||||
menuColor: config.menuColor as PresetConfig["menuColor"],
|
||||
}
|
||||
|
||||
return encodePreset(presetConfig)
|
||||
}
|
||||
34
apps/v4/app/(create)/lib/preset-query.test.ts
Normal file
34
apps/v4/app/(create)/lib/preset-query.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { resolvePresetOverrides } from "./preset-query"
|
||||
|
||||
describe("resolvePresetOverrides", () => {
|
||||
it("prefers explicit fontHeading and chartColor query params", () => {
|
||||
const overrides = resolvePresetOverrides(
|
||||
new URLSearchParams("fontHeading=playfair-display&chartColor=emerald"),
|
||||
{
|
||||
theme: "neutral",
|
||||
chartColor: "blue",
|
||||
fontHeading: "inherit",
|
||||
}
|
||||
)
|
||||
|
||||
expect(overrides).toEqual({
|
||||
fontHeading: "playfair-display",
|
||||
chartColor: "emerald",
|
||||
})
|
||||
})
|
||||
|
||||
it("falls back to decoded preset values when no overrides are present", () => {
|
||||
const overrides = resolvePresetOverrides(new URLSearchParams(), {
|
||||
theme: "neutral",
|
||||
chartColor: "blue",
|
||||
fontHeading: "inherit",
|
||||
})
|
||||
|
||||
expect(overrides).toEqual({
|
||||
fontHeading: "inherit",
|
||||
chartColor: "blue",
|
||||
})
|
||||
})
|
||||
})
|
||||
30
apps/v4/app/(create)/lib/preset-query.ts
Normal file
30
apps/v4/app/(create)/lib/preset-query.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { V1_CHART_COLOR_MAP, type PresetConfig } from "shadcn/preset"
|
||||
|
||||
import { type ChartColorName, type FontHeadingValue } from "@/registry/config"
|
||||
|
||||
type SearchParamsLike = Pick<URLSearchParams, "get" | "has">
|
||||
|
||||
export function resolvePresetOverrides(
|
||||
searchParams: SearchParamsLike,
|
||||
decoded: Pick<PresetConfig, "theme" | "chartColor" | "fontHeading">
|
||||
) {
|
||||
const hasFontHeadingOverride = searchParams.has("fontHeading")
|
||||
const hasChartColorOverride = searchParams.has("chartColor")
|
||||
|
||||
const fontHeading = hasFontHeadingOverride
|
||||
? ((searchParams.get("fontHeading") ??
|
||||
decoded.fontHeading) as FontHeadingValue)
|
||||
: decoded.fontHeading
|
||||
|
||||
const chartColor = hasChartColorOverride
|
||||
? ((searchParams.get("chartColor") ??
|
||||
decoded.chartColor ??
|
||||
V1_CHART_COLOR_MAP[decoded.theme] ??
|
||||
decoded.theme) as ChartColorName)
|
||||
: (decoded.chartColor ?? V1_CHART_COLOR_MAP[decoded.theme] ?? decoded.theme)
|
||||
|
||||
return {
|
||||
fontHeading,
|
||||
chartColor,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
BaseColorName,
|
||||
Radius,
|
||||
StyleName,
|
||||
Theme,
|
||||
ThemeName,
|
||||
} from "@/registry/config"
|
||||
|
||||
@@ -12,6 +13,7 @@ export type RandomizeContext = {
|
||||
style?: StyleName
|
||||
baseColor?: BaseColorName
|
||||
theme?: ThemeName
|
||||
chartColor?: string
|
||||
iconLibrary?: string
|
||||
font?: string
|
||||
menuAccent?: string
|
||||
@@ -26,12 +28,30 @@ export type BiasFilter<T> = (
|
||||
|
||||
export type RandomizeBiases = {
|
||||
baseColors?: BiasFilter<BaseColor>
|
||||
chartColors?: BiasFilter<Theme>
|
||||
fonts?: BiasFilter<(typeof FONTS)[number]>
|
||||
radius?: BiasFilter<Radius>
|
||||
// Add more bias filters as needed:
|
||||
// styles?: BiasFilter<Style>
|
||||
// themes?: BiasFilter<Theme>
|
||||
// etc.
|
||||
}
|
||||
|
||||
// Theme → chart color pairings for randomization.
|
||||
const CHART_COLOR_PAIRINGS: Record<string, string[]> = {
|
||||
red: ["teal", "sky"],
|
||||
orange: ["teal", "blue"],
|
||||
amber: ["cyan", "indigo"],
|
||||
yellow: ["sky", "violet"],
|
||||
lime: ["indigo", "pink"],
|
||||
green: ["purple", "rose"],
|
||||
emerald: ["purple", "red"],
|
||||
teal: ["fuchsia", "red"],
|
||||
cyan: ["rose", "amber"],
|
||||
sky: ["red", "yellow"],
|
||||
blue: ["orange", "yellow"],
|
||||
indigo: ["amber", "yellow"],
|
||||
violet: ["yellow", "lime"],
|
||||
purple: ["green", "lime"],
|
||||
fuchsia: ["lime", "teal"],
|
||||
pink: ["green", "cyan"],
|
||||
rose: ["emerald", "sky"],
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,21 +71,25 @@ export const RANDOMIZE_BIASES: RandomizeBiases = {
|
||||
return fonts
|
||||
},
|
||||
radius: (radii, context) => {
|
||||
// When style is lyra, always use "none" radius
|
||||
// When style is lyra, always use "none" radius.
|
||||
if (context.style === "lyra") {
|
||||
return radii.filter((radius) => radius.name === "none")
|
||||
}
|
||||
|
||||
return radii
|
||||
},
|
||||
// Add more biases here as needed:
|
||||
// Example: When baseColor is "blue", prefer certain themes
|
||||
// themes: (themes, context) => {
|
||||
// if (context.baseColor === "blue") {
|
||||
// return themes.filter(theme => theme.name.includes("dark"))
|
||||
// }
|
||||
// return themes
|
||||
// },
|
||||
chartColors: (chartColors, context) => {
|
||||
// When theme has a pairing, restrict chart colors to the paired values.
|
||||
const pairing = context.theme ? CHART_COLOR_PAIRINGS[context.theme] : null
|
||||
if (pairing) {
|
||||
const filtered = chartColors.filter((c) => pairing.includes(c.name))
|
||||
if (filtered.length > 0) {
|
||||
return filtered
|
||||
}
|
||||
}
|
||||
|
||||
return chartColors
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useQueryStates } from "nuqs"
|
||||
import {
|
||||
createLoader,
|
||||
@@ -10,12 +11,13 @@ import {
|
||||
type inferParserType,
|
||||
type Options,
|
||||
} from "nuqs/server"
|
||||
import { decodePreset, encodePreset, isPresetCode } from "shadcn/preset"
|
||||
import { decodePreset, isPresetCode } from "shadcn/preset"
|
||||
|
||||
import {
|
||||
BASE_COLORS,
|
||||
BASES,
|
||||
DEFAULT_CONFIG,
|
||||
getThemesForBaseColor,
|
||||
iconLibraries,
|
||||
MENU_ACCENTS,
|
||||
MENU_COLORS,
|
||||
@@ -24,6 +26,8 @@ import {
|
||||
THEMES,
|
||||
type BaseColorName,
|
||||
type BaseName,
|
||||
type ChartColorName,
|
||||
type FontHeadingValue,
|
||||
type FontValue,
|
||||
type IconLibraryName,
|
||||
type MenuAccentValue,
|
||||
@@ -33,9 +37,11 @@ import {
|
||||
type ThemeName,
|
||||
} from "@/registry/config"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { getPresetCode } from "@/app/(create)/lib/preset-code"
|
||||
import { resolvePresetOverrides } from "@/app/(create)/lib/preset-query"
|
||||
|
||||
const designSystemSearchParams = {
|
||||
preset: parseAsString.withDefault("a0"),
|
||||
preset: parseAsString.withDefault("b0"),
|
||||
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
|
||||
DEFAULT_CONFIG.base
|
||||
),
|
||||
@@ -49,9 +55,16 @@ const designSystemSearchParams = {
|
||||
theme: parseAsStringLiteral<ThemeName>(THEMES.map((t) => t.name)).withDefault(
|
||||
DEFAULT_CONFIG.theme
|
||||
),
|
||||
chartColor: parseAsStringLiteral<ChartColorName>(
|
||||
THEMES.map((t) => t.name)
|
||||
).withDefault(DEFAULT_CONFIG.chartColor ?? "neutral"),
|
||||
font: parseAsStringLiteral<FontValue>(FONTS.map((f) => f.value)).withDefault(
|
||||
DEFAULT_CONFIG.font
|
||||
),
|
||||
fontHeading: parseAsStringLiteral<FontHeadingValue>([
|
||||
"inherit",
|
||||
...FONTS.map((f) => f.value),
|
||||
]).withDefault(DEFAULT_CONFIG.fontHeading),
|
||||
baseColor: parseAsStringLiteral<BaseColorName>(
|
||||
BASE_COLORS.map((b) => b.name)
|
||||
).withDefault(DEFAULT_CONFIG.baseColor),
|
||||
@@ -87,13 +100,24 @@ const DESIGN_SYSTEM_KEYS = [
|
||||
"style",
|
||||
"baseColor",
|
||||
"theme",
|
||||
"chartColor",
|
||||
"iconLibrary",
|
||||
"font",
|
||||
"fontHeading",
|
||||
"radius",
|
||||
"menuAccent",
|
||||
"menuColor",
|
||||
] as const
|
||||
|
||||
function normalizeFontHeading(
|
||||
font: FontValue,
|
||||
fontHeading: FontHeadingValue
|
||||
): FontHeadingValue {
|
||||
// Persist "same as body" as an explicit inherit sentinel so the body font
|
||||
// can change later without freezing headings to a concrete previous value.
|
||||
return fontHeading === font ? "inherit" : fontHeading
|
||||
}
|
||||
|
||||
// Non-design-system keys that get passed through as-is.
|
||||
// `base` is not encoded in preset codes — it's an architectural choice, not visual.
|
||||
const NON_DESIGN_SYSTEM_KEYS = [
|
||||
@@ -118,26 +142,111 @@ export type DesignSystemSearchParams = inferParserType<
|
||||
typeof designSystemSearchParams
|
||||
>
|
||||
|
||||
export function isTranslucentMenuColor(
|
||||
menuColor?: MenuColorValue | null
|
||||
): menuColor is "default-translucent" | "inverted-translucent" {
|
||||
return (
|
||||
menuColor === "default-translucent" || menuColor === "inverted-translucent"
|
||||
)
|
||||
}
|
||||
|
||||
function normalizePartialDesignSystemParams(
|
||||
params: Partial<DesignSystemSearchParams>
|
||||
): Partial<DesignSystemSearchParams> {
|
||||
if (
|
||||
params.menuAccent === "bold" &&
|
||||
isTranslucentMenuColor(params.menuColor ?? undefined)
|
||||
) {
|
||||
return {
|
||||
...params,
|
||||
menuAccent: "subtle",
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
function normalizeDesignSystemParams(
|
||||
params: DesignSystemSearchParams
|
||||
): DesignSystemSearchParams {
|
||||
let result = {
|
||||
...params,
|
||||
fontHeading: normalizeFontHeading(params.font, params.fontHeading),
|
||||
}
|
||||
|
||||
// Validate theme and chartColor against baseColor.
|
||||
if (result.baseColor) {
|
||||
const available = getThemesForBaseColor(result.baseColor)
|
||||
const themeValid = available.some((t) => t.name === result.theme)
|
||||
const chartColorValid = available.some((t) => t.name === result.chartColor)
|
||||
|
||||
if (!themeValid || !chartColorValid) {
|
||||
const fallback = (available[0]?.name ?? result.baseColor) as ThemeName
|
||||
result = {
|
||||
...result,
|
||||
...(!themeValid && { theme: fallback }),
|
||||
...(!chartColorValid && { chartColor: fallback as ChartColorName }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
result.menuAccent === "bold" &&
|
||||
isTranslucentMenuColor(result.menuColor)
|
||||
) {
|
||||
return {
|
||||
...result,
|
||||
menuAccent: "subtle",
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// If preset param exists, decode it and overlay on raw params.
|
||||
// V1 presets don't encode chartColor — fall back to the colored
|
||||
// theme that base-color themes originally borrowed charts from.
|
||||
type SearchParamsLike = Pick<URLSearchParams, "get" | "has">
|
||||
|
||||
function resolvePresetParams(
|
||||
rawParams: DesignSystemSearchParams,
|
||||
searchParams: SearchParamsLike
|
||||
) {
|
||||
if (rawParams.preset && isPresetCode(rawParams.preset)) {
|
||||
const decoded = decodePreset(rawParams.preset)
|
||||
if (decoded) {
|
||||
const presetOverrides = resolvePresetOverrides(searchParams, decoded)
|
||||
return normalizeDesignSystemParams({
|
||||
...decoded,
|
||||
...presetOverrides,
|
||||
base: rawParams.base,
|
||||
item: rawParams.item,
|
||||
preset: rawParams.preset,
|
||||
template: rawParams.template,
|
||||
rtl: rawParams.rtl,
|
||||
size: rawParams.size,
|
||||
custom: rawParams.custom,
|
||||
})
|
||||
}
|
||||
}
|
||||
return normalizeDesignSystemParams(rawParams)
|
||||
}
|
||||
|
||||
// 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.
|
||||
export function useDesignSystemSearchParams(options: Options = {}) {
|
||||
const searchParams = useSearchParams()
|
||||
const [rawParams, rawSetParams] = useQueryStates(designSystemSearchParams, {
|
||||
shallow: false,
|
||||
history: "push",
|
||||
...options,
|
||||
})
|
||||
|
||||
// If preset param exists, decode it and overlay on raw params.
|
||||
const params = React.useMemo(() => {
|
||||
if (rawParams.preset && isPresetCode(rawParams.preset)) {
|
||||
const decoded = decodePreset(rawParams.preset)
|
||||
if (decoded) {
|
||||
return { ...rawParams, ...decoded }
|
||||
}
|
||||
}
|
||||
return rawParams
|
||||
}, [rawParams])
|
||||
const params = React.useMemo(
|
||||
() => resolvePresetParams(rawParams, searchParams),
|
||||
[rawParams, searchParams]
|
||||
)
|
||||
|
||||
// Use ref so setParams callback stays stable across renders.
|
||||
const paramsRef = React.useRef(params)
|
||||
@@ -156,8 +265,9 @@ export function useDesignSystemSearchParams(options: Options = {}) {
|
||||
) => Partial<DesignSystemSearchParams>),
|
||||
setOptions?: Options
|
||||
) => {
|
||||
const resolvedUpdates =
|
||||
const resolvedUpdates = normalizePartialDesignSystemParams(
|
||||
typeof updates === "function" ? updates(paramsRef.current) : updates
|
||||
)
|
||||
|
||||
const hasDesignSystemUpdate = DESIGN_SYSTEM_KEYS.some(
|
||||
(key) => key in resolvedUpdates
|
||||
@@ -169,22 +279,14 @@ export function useDesignSystemSearchParams(options: Options = {}) {
|
||||
}
|
||||
|
||||
// Merge current decoded values with updates.
|
||||
const merged = { ...paramsRef.current, ...resolvedUpdates }
|
||||
|
||||
const merged = normalizeDesignSystemParams({
|
||||
...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 = encodePreset({
|
||||
style: merged.style ?? undefined,
|
||||
baseColor: merged.baseColor ?? undefined,
|
||||
theme: merged.theme ?? undefined,
|
||||
iconLibrary: merged.iconLibrary ?? undefined,
|
||||
font: merged.font ?? undefined,
|
||||
radius: merged.radius ?? undefined,
|
||||
menuAccent: merged.menuAccent ?? undefined,
|
||||
menuColor: merged.menuColor ?? undefined,
|
||||
} as Parameters<typeof encodePreset>[0])
|
||||
|
||||
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) {
|
||||
|
||||
@@ -7,7 +7,7 @@ export const TEMPLATES = [
|
||||
{
|
||||
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>',
|
||||
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(--color-neutral-800)" 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>',
|
||||
},
|
||||
{
|
||||
value: "start",
|
||||
|
||||
115
apps/v4/app/(create)/lib/v0.test.ts
Normal file
115
apps/v4/app/(create)/lib/v0.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { DEFAULT_CONFIG } from "@/registry/config"
|
||||
import { buildV0Payload } from "@/app/(create)/lib/v0"
|
||||
|
||||
vi.mock("shadcn/schema", async () => {
|
||||
return await vi.importActual("shadcn/schema")
|
||||
})
|
||||
|
||||
vi.mock("shadcn/utils", async () => {
|
||||
const actual = (await vi.importActual("shadcn/utils")) as {
|
||||
transformFont: unknown
|
||||
}
|
||||
|
||||
return {
|
||||
transformFont: actual.transformFont,
|
||||
transformIcons: async ({ sourceFile }: { sourceFile: unknown }) =>
|
||||
sourceFile,
|
||||
transformMenu: async ({ sourceFile }: { sourceFile: unknown }) =>
|
||||
sourceFile,
|
||||
transformRender: async ({ sourceFile }: { sourceFile: unknown }) =>
|
||||
sourceFile,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("@/registry/bases/__index__", () => ({
|
||||
Index: {
|
||||
base: {
|
||||
card: {
|
||||
name: "card",
|
||||
type: "registry:ui",
|
||||
},
|
||||
},
|
||||
radix: {
|
||||
card: {
|
||||
name: "card",
|
||||
type: "registry:ui",
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe("buildV0Payload", () => {
|
||||
beforeEach(() => {
|
||||
process.env.NEXT_PUBLIC_APP_URL = "http://example.test"
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: string | URL | Request) => {
|
||||
const url =
|
||||
typeof input === "string"
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.toString()
|
||||
: input.url
|
||||
const name = url.split("/").pop()?.replace(".json", "") ?? "component"
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name,
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: `registry/base-nova/ui/${name}.tsx`,
|
||||
type: "registry:ui",
|
||||
content: `import * as React from "react"\n\nexport function Component() {\n return <div className="cn-font-heading text-xl" />\n}\n`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
delete process.env.NEXT_PUBLIC_APP_URL
|
||||
})
|
||||
|
||||
it("rewrites cn-font-heading to font-heading when heading inherits the body font", async () => {
|
||||
const payload = await buildV0Payload({
|
||||
...DEFAULT_CONFIG,
|
||||
item: undefined,
|
||||
fontHeading: "inherit",
|
||||
})
|
||||
|
||||
const cardFile = payload.files?.find(
|
||||
(file) => file.target === "components/ui/card.tsx"
|
||||
)
|
||||
|
||||
expect(cardFile?.content).toContain("font-heading")
|
||||
expect(cardFile?.content).not.toContain("cn-font-heading")
|
||||
})
|
||||
|
||||
it("rewrites cn-font-heading to font-heading when a distinct heading font is selected", async () => {
|
||||
const payload = await buildV0Payload({
|
||||
...DEFAULT_CONFIG,
|
||||
item: undefined,
|
||||
fontHeading: "playfair-display",
|
||||
})
|
||||
|
||||
const cardFile = payload.files?.find(
|
||||
(file) => file.target === "components/ui/card.tsx"
|
||||
)
|
||||
|
||||
expect(cardFile?.content).toContain("font-heading")
|
||||
expect(cardFile?.content).not.toContain("cn-font-heading")
|
||||
})
|
||||
})
|
||||
@@ -5,19 +5,28 @@ import {
|
||||
type configSchema,
|
||||
type RegistryItem,
|
||||
} from "shadcn/schema"
|
||||
import { transformIcons, transformMenu, transformRender } from "shadcn/utils"
|
||||
import { Project, ScriptKind } from "ts-morph"
|
||||
import {
|
||||
transformFont,
|
||||
transformIcons,
|
||||
transformMenu,
|
||||
transformRender,
|
||||
} from "shadcn/utils"
|
||||
import { Project, ScriptKind, type SourceFile } from "ts-morph"
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
buildRegistryBase,
|
||||
fonts,
|
||||
getBodyFont,
|
||||
getHeadingFont,
|
||||
getInheritedHeadingFontValue,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
|
||||
const { Index } = await import("@/registry/bases/__index__")
|
||||
|
||||
const THEME_INLINE = `--font-sans: var(--font-sans);
|
||||
function buildThemeInline(fontHeadingValue: string) {
|
||||
return `--font-sans: var(--font-sans);
|
||||
--font-heading: ${fontHeadingValue};
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
--color-background: var(--background);
|
||||
@@ -59,6 +68,7 @@ const THEME_INLINE = `--font-sans: var(--font-sans);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);`
|
||||
}
|
||||
|
||||
// Static file — parsed once at module level.
|
||||
const themeProviderFile = registryItemFileSchema.parse({
|
||||
@@ -148,7 +158,20 @@ const ALIASES = {
|
||||
hooks: "@/hooks",
|
||||
} as const
|
||||
|
||||
const transformers = [transformIcons, transformMenu, transformRender]
|
||||
type V0Transformer = (opts: {
|
||||
filename: string
|
||||
raw: string
|
||||
sourceFile: SourceFile
|
||||
config: z.infer<typeof configSchema>
|
||||
supportedFontMarkers?: string[]
|
||||
}) => Promise<unknown>
|
||||
|
||||
const transformers: V0Transformer[] = [
|
||||
transformIcons as V0Transformer,
|
||||
transformMenu as V0Transformer,
|
||||
transformRender as V0Transformer,
|
||||
transformFont as V0Transformer,
|
||||
]
|
||||
|
||||
function getStyle(designSystemConfig: DesignSystemConfig) {
|
||||
return `${designSystemConfig.base}-${designSystemConfig.style}`
|
||||
@@ -156,10 +179,18 @@ function getStyle(designSystemConfig: DesignSystemConfig) {
|
||||
|
||||
export async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
|
||||
const registryBase = buildRegistryBase(designSystemConfig)
|
||||
const normalizedFontHeading =
|
||||
designSystemConfig.fontHeading === designSystemConfig.font
|
||||
? "inherit"
|
||||
: designSystemConfig.fontHeading
|
||||
|
||||
// Only buildComponentFiles is async — run sync builders directly.
|
||||
const globalsCss = buildGlobalsCss(registryBase)
|
||||
const layoutFile = buildLayoutFile(designSystemConfig)
|
||||
const globalsCss = buildGlobalsCss(
|
||||
registryBase,
|
||||
designSystemConfig.font,
|
||||
normalizedFontHeading
|
||||
)
|
||||
const layoutFile = buildLayoutFile(designSystemConfig, normalizedFontHeading)
|
||||
const componentFiles = await buildComponentFiles(designSystemConfig)
|
||||
|
||||
const dependencies = [...(registryBase.dependencies ?? []), "next-themes"]
|
||||
@@ -181,7 +212,11 @@ export async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
|
||||
})
|
||||
}
|
||||
|
||||
function buildGlobalsCss(registryBase: RegistryItem) {
|
||||
function buildGlobalsCss(
|
||||
registryBase: RegistryItem,
|
||||
font: DesignSystemConfig["font"],
|
||||
fontHeading: DesignSystemConfig["fontHeading"]
|
||||
) {
|
||||
const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
@@ -194,11 +229,15 @@ function buildGlobalsCss(registryBase: RegistryItem) {
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
${THEME_INLINE}
|
||||
}
|
||||
@theme inline {
|
||||
${buildThemeInline(
|
||||
fontHeading === "inherit"
|
||||
? getInheritedHeadingFontValue(font)
|
||||
: "var(--font-heading)"
|
||||
)}
|
||||
}
|
||||
|
||||
:root {
|
||||
${lightVars}
|
||||
@@ -332,18 +371,23 @@ function buildPackageJson(dependencies: string[]) {
|
||||
})
|
||||
}
|
||||
|
||||
function buildLayoutFile(designSystemConfig: DesignSystemConfig) {
|
||||
const font = fonts.find(
|
||||
(font) => font.name === `font-${designSystemConfig.font}`
|
||||
)
|
||||
function buildLayoutFile(
|
||||
designSystemConfig: DesignSystemConfig,
|
||||
fontHeading: DesignSystemConfig["fontHeading"]
|
||||
) {
|
||||
const font = getBodyFont(designSystemConfig.font)
|
||||
if (!font) {
|
||||
throw new Error(`Font "${designSystemConfig.font}" not found`)
|
||||
}
|
||||
|
||||
const headingFont =
|
||||
fontHeading === "inherit" ? undefined : getHeadingFont(fontHeading)
|
||||
|
||||
// Derive const name from the font's CSS variable (e.g. --font-sans → fontSans).
|
||||
const constName = font.font.variable
|
||||
.replace(/^--/, "")
|
||||
.replace(/-./g, (m) => m[1].toUpperCase())
|
||||
const headingConstName = "fontHeading"
|
||||
|
||||
// Add font-serif or font-mono class to body when needed. Sans is the default.
|
||||
const fontClass =
|
||||
@@ -354,14 +398,26 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) {
|
||||
: ""
|
||||
|
||||
const bodyClassName = fontClass ? `antialiased ${fontClass}` : "antialiased"
|
||||
const imports = headingFont
|
||||
? Array.from(new Set([font.font.import, headingFont.font.import])).join(
|
||||
", "
|
||||
)
|
||||
: font.font.import
|
||||
const headingDeclaration = headingFont
|
||||
? `\nconst ${headingConstName} = ${headingFont.font.import}({subsets:['latin'],variable:'${headingFont.font.variable}'});\n`
|
||||
: ""
|
||||
const htmlClassName = headingFont
|
||||
? `{${constName}.variable + " " + ${headingConstName}.variable}`
|
||||
: `{${constName}.variable}`
|
||||
|
||||
const content = dedent`
|
||||
import type { Metadata } from "next";
|
||||
import { ${font.font.import} } from "next/font/google";
|
||||
import { ${imports} } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
const ${constName} = ${font.font.import}({subsets:['latin'],variable:'${font.font.variable}'});
|
||||
${headingDeclaration}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
@@ -374,7 +430,7 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) {
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning className={${constName}.variable}>
|
||||
<html lang="en" suppressHydrationWarning className=${htmlClassName}>
|
||||
<body
|
||||
className="${bodyClassName}"
|
||||
>
|
||||
@@ -401,13 +457,14 @@ async function buildComponentFiles(designSystemConfig: DesignSystemConfig) {
|
||||
// Build config once for all components.
|
||||
const config = buildTransformConfig(designSystemConfig)
|
||||
|
||||
// Fetch UI components and the item component in parallel.
|
||||
const [registryItemFiles, itemComponentFile] = await Promise.all([
|
||||
// Fetch UI components, the demo, and the item component in parallel.
|
||||
const [registryItemFiles, demoFile, itemComponentFile] = await Promise.all([
|
||||
Promise.all(
|
||||
allItemsForBase.map((name) =>
|
||||
getRegistryItemFile(name, designSystemConfig, config)
|
||||
)
|
||||
),
|
||||
getRegistryItemFile("demo", designSystemConfig, config),
|
||||
designSystemConfig.item
|
||||
? getRegistryItemFile(designSystemConfig.item, designSystemConfig, config)
|
||||
: null,
|
||||
@@ -415,29 +472,24 @@ async function buildComponentFiles(designSystemConfig: DesignSystemConfig) {
|
||||
|
||||
const files = [...registryItemFiles.filter(Boolean)]
|
||||
|
||||
// Include the demo component.
|
||||
if (demoFile) {
|
||||
files.push({
|
||||
...demoFile,
|
||||
target: "components/demo.tsx",
|
||||
type: "registry:component",
|
||||
})
|
||||
}
|
||||
|
||||
const pageFile = {
|
||||
path: "app/page.tsx",
|
||||
type: "registry:page",
|
||||
target: "app/page.tsx",
|
||||
content: dedent`
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Demo } from "@/components/demo"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh p-6">
|
||||
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
|
||||
<div>
|
||||
<h1 className="font-medium">Project ready!</h1>
|
||||
<p>You may now add components and start building.</p>
|
||||
<p>We've already added the button component for you.</p>
|
||||
<Button className="mt-2">Button</Button>
|
||||
</div>
|
||||
<div className="font-mono text-xs text-muted-foreground">
|
||||
(Press <kbd>d</kbd> to toggle dark mode)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <Demo />
|
||||
}
|
||||
`,
|
||||
}
|
||||
@@ -557,6 +609,7 @@ async function transformFileContent(
|
||||
raw: content,
|
||||
sourceFile,
|
||||
config,
|
||||
supportedFontMarkers: ["cn-font-heading"],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ export default async function BlockPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative bg-background">
|
||||
<PreventScrollOnFocusScript />
|
||||
<PreviewStyle />
|
||||
<ActionMenuScript />
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { Label, Pie, PieChart, Sector } from "recharts"
|
||||
import { type PieSectorDataItem } from "recharts/types/polar/Pie"
|
||||
import type {
|
||||
PieSectorDataItem,
|
||||
PieSectorShapeProps,
|
||||
} from "recharts/types/polar/Pie"
|
||||
|
||||
import {
|
||||
Card,
|
||||
@@ -77,6 +80,26 @@ export function ChartVisitors() {
|
||||
)
|
||||
const months = React.useMemo(() => desktopData.map((item) => item.month), [])
|
||||
|
||||
const renderPieShape = React.useCallback(
|
||||
({ index, outerRadius = 0, ...props }: PieSectorShapeProps) => {
|
||||
if (index === activeIndex) {
|
||||
return (
|
||||
<g>
|
||||
<Sector {...props} outerRadius={outerRadius + 10} />
|
||||
<Sector
|
||||
{...props}
|
||||
outerRadius={outerRadius + 25}
|
||||
innerRadius={outerRadius + 12}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
return <Sector {...props} outerRadius={outerRadius} />
|
||||
},
|
||||
[activeIndex]
|
||||
)
|
||||
|
||||
return (
|
||||
<Card data-chart={id}>
|
||||
<ChartStyle id={id} config={chartConfig} />
|
||||
@@ -136,20 +159,7 @@ export function ChartVisitors() {
|
||||
nameKey="month"
|
||||
innerRadius={60}
|
||||
strokeWidth={5}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={({
|
||||
outerRadius = 0,
|
||||
...props
|
||||
}: PieSectorDataItem) => (
|
||||
<g>
|
||||
<Sector {...props} outerRadius={outerRadius + 10} />
|
||||
<Sector
|
||||
{...props}
|
||||
outerRadius={outerRadius + 25}
|
||||
innerRadius={outerRadius + 12}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
shape={renderPieShape}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
|
||||
@@ -86,7 +86,7 @@ export function ChartBarMixed() {
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar dataKey="visitors" layout="vertical" radius={5} />
|
||||
<Bar dataKey="visitors" radius={5} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
import "@/styles/legacy-themes.css"
|
||||
import "@/app/legacy-themes.css"
|
||||
|
||||
import { ComponentPreview } from "./component-preview"
|
||||
|
||||
|
||||
381
apps/v4/app/globals.css
Normal file
381
apps/v4/app/globals.css
Normal file
@@ -0,0 +1,381 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
@import "./legacy-themes.css";
|
||||
|
||||
@import "../registry/styles/style-vega.css" layer(base);
|
||||
@import "../registry/styles/style-nova.css" layer(base);
|
||||
@import "../registry/styles/style-lyra.css" layer(base);
|
||||
@import "../registry/styles/style-maia.css" layer(base);
|
||||
@import "../registry/styles/style-mira.css" layer(base);
|
||||
|
||||
@custom-variant style-vega (&:where(.style-vega *));
|
||||
@custom-variant style-nova (&:where(.style-nova *));
|
||||
@custom-variant style-lyra (&:where(.style-lyra *));
|
||||
@custom-variant style-maia (&:where(.style-maia *));
|
||||
@custom-variant style-mira (&:where(.style-mira *));
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@custom-variant fixed (&:is(.layout-fixed *));
|
||||
|
||||
@theme inline {
|
||||
--breakpoint-3xl: 1600px;
|
||||
--breakpoint-4xl: 2000px;
|
||||
--font-sans: var(--font-sans);
|
||||
--font-heading: var(--font-heading);
|
||||
--font-mono: var(--font-mono);
|
||||
--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);
|
||||
--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);
|
||||
--color-surface: var(--surface);
|
||||
--color-surface-foreground: var(--surface-foreground);
|
||||
--color-code: var(--code);
|
||||
--color-code-foreground: var(--code-foreground);
|
||||
--color-code-highlight: var(--code-highlight);
|
||||
--color-code-number: var(--code-number);
|
||||
--color-selection: var(--selection);
|
||||
--color-selection-foreground: var(--selection-foreground);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.97 0.01 17);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: var(--color-blue-300);
|
||||
--chart-2: var(--color-blue-500);
|
||||
--chart-3: var(--color-blue-600);
|
||||
--chart-4: var(--color-blue-700);
|
||||
--chart-5: var(--color-blue-800);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--surface: oklch(0.98 0 0);
|
||||
--surface-foreground: var(--foreground);
|
||||
--code: var(--surface);
|
||||
--code-foreground: var(--surface-foreground);
|
||||
--code-highlight: oklch(0.96 0 0);
|
||||
--code-number: oklch(0.56 0 0);
|
||||
--selection: oklch(0.145 0 0);
|
||||
--selection-foreground: oklch(1 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.371 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.58 0.22 27);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: var(--color-blue-300);
|
||||
--chart-2: var(--color-blue-500);
|
||||
--chart-3: var(--color-blue-600);
|
||||
--chart-4: var(--color-blue-700);
|
||||
--chart-5: var(--color-blue-800);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
--surface: oklch(0.2 0 0);
|
||||
--surface-foreground: oklch(0.708 0 0);
|
||||
--code: var(--surface);
|
||||
--code-foreground: var(--surface-foreground);
|
||||
--code-highlight: oklch(0.27 0 0);
|
||||
--code-number: oklch(0.72 0 0);
|
||||
--selection: oklch(0.922 0 0);
|
||||
--selection-foreground: oklch(0.205 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
::selection {
|
||||
@apply bg-selection text-selection-foreground;
|
||||
}
|
||||
html {
|
||||
@apply overscroll-y-none;
|
||||
}
|
||||
body {
|
||||
font-synthesis-weight: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
.cn-font-heading {
|
||||
@apply font-heading;
|
||||
}
|
||||
|
||||
[data-slot="layout"] {
|
||||
@apply overscroll-none;
|
||||
}
|
||||
|
||||
@supports (font: -apple-system-body) and (-webkit-appearance: none) {
|
||||
[data-wrapper] {
|
||||
@apply min-[1800px]:border-t;
|
||||
}
|
||||
}
|
||||
|
||||
a:active,
|
||||
button:active {
|
||||
@apply opacity-60 md:opacity-100;
|
||||
}
|
||||
|
||||
[data-lang="ar"] {
|
||||
font-family: var(--font-ar);
|
||||
}
|
||||
|
||||
[data-lang="he"] {
|
||||
font-family: var(--font-he);
|
||||
}
|
||||
}
|
||||
|
||||
@utility border-grid {
|
||||
@apply border-border/50 dark:border-border;
|
||||
}
|
||||
|
||||
@utility section-soft {
|
||||
@apply from-background to-surface/40 dark:bg-background 3xl:fixed:bg-none bg-linear-to-b;
|
||||
}
|
||||
|
||||
@utility theme-container {
|
||||
@apply font-sans;
|
||||
}
|
||||
|
||||
@utility container-wrapper {
|
||||
@apply 3xl:fixed:max-w-[calc(var(--breakpoint-2xl)+2rem)] mx-auto w-full px-2;
|
||||
}
|
||||
|
||||
@utility container {
|
||||
@apply 3xl:max-w-screen-2xl mx-auto max-w-[1400px] px-4 lg:px-8;
|
||||
}
|
||||
|
||||
@utility no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@utility border-ghost {
|
||||
@apply after:border-border relative after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten;
|
||||
}
|
||||
|
||||
@utility step {
|
||||
counter-increment: step;
|
||||
|
||||
&:before {
|
||||
@apply border-background bg-muted mr-2 inline-flex size-6 items-center justify-center rounded-full text-center -indent-px font-mono text-sm font-medium md:absolute md:mt-[-4px] md:ml-[-50px] md:size-9 md:border-4;
|
||||
content: counter(step);
|
||||
}
|
||||
}
|
||||
|
||||
@utility extend-touch-target {
|
||||
@media (pointer: coarse) {
|
||||
@apply relative touch-manipulation after:absolute after:-inset-2;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.steps {
|
||||
&:first-child {
|
||||
@apply !mt-0;
|
||||
}
|
||||
|
||||
&:first-child > h3:first-child {
|
||||
@apply !mt-0;
|
||||
}
|
||||
|
||||
> h3 {
|
||||
@apply !mt-8;
|
||||
}
|
||||
|
||||
> h3 + p {
|
||||
@apply !mt-2;
|
||||
}
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] {
|
||||
background-color: var(--color-code);
|
||||
color: var(--color-code-foreground);
|
||||
border-radius: var(--radius-xl);
|
||||
border-width: 0px;
|
||||
border-color: var(--border);
|
||||
margin-top: calc(var(--spacing) * 6);
|
||||
overflow: hidden;
|
||||
font-size: var(--text-sm);
|
||||
outline: none;
|
||||
position: relative;
|
||||
@apply -mx-1 md:-mx-1;
|
||||
|
||||
&:has([data-rehype-pretty-code-title]) [data-slot="copy-button"] {
|
||||
top: calc(var(--spacing) * 1.5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-title] {
|
||||
border-bottom: color-mix(in oklab, var(--border) 30%, transparent);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
padding-block: calc(var(--spacing) * 2.5);
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
font-size: var(--text-sm);
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-code-foreground);
|
||||
}
|
||||
|
||||
[data-line-numbers] {
|
||||
display: grid;
|
||||
min-width: 100%;
|
||||
white-space: pre;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
counter-reset: line;
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
|
||||
[data-line-numbers] [data-line]::before {
|
||||
font-size: var(--text-sm);
|
||||
counter-increment: line;
|
||||
content: counter(line);
|
||||
display: inline-block;
|
||||
width: calc(var(--spacing) * 16);
|
||||
padding-right: calc(var(--spacing) * 6);
|
||||
text-align: right;
|
||||
color: var(--color-code-number);
|
||||
background-color: var(--color-code);
|
||||
position: sticky;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
[data-line-numbers] [data-highlighted-line][data-line]::before {
|
||||
background-color: var(--color-code-highlight);
|
||||
}
|
||||
|
||||
[data-line] {
|
||||
padding-top: calc(var(--spacing) * 0.5);
|
||||
padding-bottom: calc(var(--spacing) * 0.5);
|
||||
min-height: calc(var(--spacing) * 1);
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
[data-line] span {
|
||||
color: var(--shiki-light);
|
||||
|
||||
@variant dark {
|
||||
color: var(--shiki-dark) !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-highlighted-line],
|
||||
[data-highlighted-chars] {
|
||||
position: relative;
|
||||
background-color: var(--color-code-highlight);
|
||||
}
|
||||
|
||||
[data-highlighted-line] {
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background-color: color-mix(
|
||||
in oklab,
|
||||
var(--muted-foreground) 50%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[data-highlighted-chars] {
|
||||
border-radius: var(--radius-sm);
|
||||
padding-inline: 0.3rem;
|
||||
padding-block: 0.1rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.dialog-ring {
|
||||
@apply rounded-xl border-none bg-clip-padding shadow-2xl ring-4 ring-neutral-200/80 dark:bg-neutral-900 dark:ring-neutral-800;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { TooltipProvider as BaseTooltipProvider } from "@/registry/bases/base/ui
|
||||
import { Toaster } from "@/registry/bases/radix/ui/sonner"
|
||||
import { TooltipProvider as RadixTooltipProvider } from "@/registry/bases/radix/ui/tooltip"
|
||||
|
||||
import "@/styles/globals.css"
|
||||
import "@/app/globals.css"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/base/ui/popover"
|
||||
import { IconAlertCircle } from "@tabler/icons-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -21,6 +16,11 @@ import { DirectionProvider as BaseDirectionProvider } from "@/registry/bases/bas
|
||||
import { DirectionProvider as RadixDirectionProvider } from "@/registry/bases/radix/ui/direction"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/base-nova/ui/popover"
|
||||
|
||||
export function ComponentPreviewTabs({
|
||||
className,
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function StarsCount() {
|
||||
const formattedCount =
|
||||
json.stargazers_count >= 1000
|
||||
? `${Math.round(json.stargazers_count / 1000)}k`
|
||||
: json.stargazers_count.toLocaleString()
|
||||
: json.stargazers_count?.toLocaleString()
|
||||
|
||||
return (
|
||||
<span className="w-fit text-xs text-muted-foreground tabular-nums">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -8,9 +10,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/examples/base/ui/select"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
} from "@/styles/base-nova/ui/select"
|
||||
|
||||
export type Language = "en" | "ar" | "he"
|
||||
|
||||
|
||||
@@ -7,12 +7,6 @@ import { useTheme } from "next-themes"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useMetaColor } from "@/hooks/use-meta-color"
|
||||
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"
|
||||
|
||||
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
|
||||
|
||||
@@ -34,67 +28,34 @@ export function ModeSwitcher({
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}, [resolvedTheme, setTheme])
|
||||
|
||||
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 (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={variant}
|
||||
size="icon"
|
||||
className={cn("group/toggle extend-touch-target size-8", className)}
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-2 pr-1">
|
||||
Toggle Mode <Kbd>D</Kbd>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant={variant}
|
||||
size="icon"
|
||||
className={cn("group/toggle extend-touch-target size-8", className)}
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
|
||||
export function OpenInV0Cta({ className }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
@@ -19,7 +19,7 @@ export function OpenInV0Cta({ className }: React.ComponentProps<"div">) {
|
||||
Vercel provides tools and infrastructure to deploy apps and features at
|
||||
scale.
|
||||
</div>
|
||||
<Button size="sm" className="mt-2 w-fit">
|
||||
<Button variant="outline" size="sm" className="mt-2 w-fit">
|
||||
Deploy Now
|
||||
</Button>
|
||||
<a
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { PlusSignIcon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
@@ -14,6 +13,7 @@ import { MobileNav } from "@/components/mobile-nav"
|
||||
import { ModeSwitcher } from "@/components/mode-switcher"
|
||||
import { SiteConfig } from "@/components/site-config"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ProjectForm } from "@/app/(create)/components/project-form"
|
||||
import { V0Button } from "@/app/(create)/components/v0-button"
|
||||
|
||||
|
||||
@@ -1,7 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes"
|
||||
|
||||
function ThemeShortcut() {
|
||||
const { setTheme, resolvedTheme } = useTheme()
|
||||
|
||||
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()
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [resolvedTheme, setTheme])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
@@ -16,6 +48,7 @@ export function ThemeProvider({
|
||||
enableColorScheme
|
||||
{...props}
|
||||
>
|
||||
<ThemeShortcut />
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { Label } from "@/examples/base/ui/label"
|
||||
import { THEMES } from "@/lib/themes"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useThemeConfig } from "@/components/active-theme"
|
||||
import { Label } from "@/styles/base-nova/ui/label"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -9,11 +12,7 @@ import {
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/examples/base/ui/select"
|
||||
|
||||
import { THEMES } from "@/lib/themes"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useThemeConfig } from "@/components/active-theme"
|
||||
} from "@/styles/base-nova/ui/select"
|
||||
|
||||
import { CopyCodeButton } from "./theme-customizer"
|
||||
|
||||
|
||||
@@ -74,21 +74,21 @@ Path to the CSS file that imports Tailwind CSS into your project.
|
||||
|
||||
### tailwind.baseColor
|
||||
|
||||
This is used to generate the default color palette for your components. **This cannot be changed after initialization.**
|
||||
This is used to generate the default theme tokens for your components. **This cannot be changed after initialization.**
|
||||
|
||||
```json title="components.json"
|
||||
{
|
||||
"tailwind": {
|
||||
"baseColor": "gray" | "neutral" | "slate" | "stone" | "zinc" | "mauve" | "olive" | "mist" | "taupe"
|
||||
"baseColor": "neutral" | "stone" | "zinc" | "mauve" | "olive" | "mist" | "taupe"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### tailwind.cssVariables
|
||||
|
||||
You can choose between using CSS variables or Tailwind CSS utility classes for theming.
|
||||
We use and recommend CSS variables for theming.
|
||||
|
||||
To use utility classes for theming set `tailwind.cssVariables` to `false`. For CSS variables, set `tailwind.cssVariables` to `true`.
|
||||
Set `tailwind.cssVariables` to `true` to generate semantic theme tokens like `background`, `foreground`, and `primary`. Set it to `false` to generate inline Tailwind color utilities instead.
|
||||
|
||||
```json title="components.json"
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ description: Every component recreated in Figma. With customizable props, typogr
|
||||
|
||||
## Paid
|
||||
|
||||
- [shadcn/ui kit](https://shadcndesign.com) by [ Matt Wierzbicki](https://x.com/matsugfx) - A premium, always up-to-date UI kit for Figma - shadcn/ui compatible and optimized for smooth design-to-dev handoff.
|
||||
- [Shadcraft UI Kit](https://shadcraft.com) - The most advanced shadcn-compatible kit with instant theming via [tweakcn](https://tweakcn.com), a pro library of components and templates, and complete coverage of shadcn components and blocks.
|
||||
- [shadcn/ui kit](https://shadcndesign.com) by [Matt Wierzbicki](https://x.com/matsugfx) - A premium, always up-to-date UI kit for Figma - shadcn/ui compatible and optimized for smooth design-to-dev handoff.
|
||||
- [shadcncraft Design System](https://shadcncraft.com) - Production-ready shadcn/ui kit with Pro React blocks and 1:1 Figma alignment. Design and ship faster with tweakcn theming, AI-assisted workflows, and Figma to React export, built for real product UIs.
|
||||
- [shadcn/studio UI Kit](https://shadcnstudio.com/figma) - Accelerate design & development with a shadcn/ui compatible Figma kit with updated components, 550+ blocks, 10+ templates, 20+ themes, and an AI tool that converts designs into shadcn/ui code.
|
||||
- [Shadcnblocks.com](https://www.shadcnblocks.com) - A Premium Shadcn Figma UI Kit with components, 500+ pro blocks, shadcn theme variables, light/dark mode and Figma MCP ready.
|
||||
|
||||
@@ -27,7 +27,7 @@ shadcn/ui hands you the actual component code. You have full control to customiz
|
||||
|
||||
_In a typical library, if you need to change a button’s behavior, you have to override styles or wrap the component. With shadcn/ui, you simply edit the button code directly._
|
||||
|
||||
<Accordion collapsible>
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="faq-1" className="border-none">
|
||||
<AccordionTrigger>
|
||||
How do I pull upstream updates in an Open Code approach?
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Your project is ready!
|
||||
description: You've created a new project with shadcn/ui.
|
||||
---
|
||||
|
||||
Here's a few things you can do to get started building with shadcn/ui.
|
||||
Here are a few things you can do to get started building with shadcn/ui.
|
||||
|
||||
## Add Components
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user