Compare commits

..

2 Commits

Author SHA1 Message Date
shadcn
69a88f9579 Merge branch 'main' into shadcn/code-format 2026-03-15 12:48:37 +04:00
shadcn
4e8263d7a3 ci: add code-format action 2026-03-15 12:47:01 +04:00
2330 changed files with 7868 additions and 86719 deletions

View File

@@ -1,22 +0,0 @@
---
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).

125
.github/workflows/code-format.yml vendored Normal file
View File

@@ -0,0 +1,125 @@
name: Code format
on:
workflow_run:
workflows: ["Code check"]
types:
- completed
permissions:
actions: read
contents: write
pull-requests: read
jobs:
format:
if: |
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
name: Run pnpm format:write
steps:
- name: Inspect failed workflow run
id: meta
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.workflow_run.pull_requests[0]
if (!pr) {
core.notice("Skipping because the failed run is not attached to a pull request.")
core.setOutput("should_run", "false")
return
}
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
})
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
per_page: 100,
})
const formatJob = jobs.jobs.find((job) => job.name === "pnpm format:check")
const sameRepo =
pullRequest.head.repo.full_name === `${context.repo.owner}/${context.repo.repo}`
const shouldRun = formatJob?.conclusion === "failure" && sameRepo
if (!formatJob) {
core.notice("Skipping because the format job could not be found in the failed run.")
} else if (formatJob.conclusion !== "failure") {
core.notice(
`Skipping because the format job concluded with "${formatJob.conclusion}".`
)
}
if (!sameRepo) {
core.notice(
`Skipping PR #${pullRequest.number} because its head branch lives in ${pullRequest.head.repo.full_name}.`
)
}
core.setOutput("head_ref", pullRequest.head.ref)
core.setOutput("should_run", shouldRun ? "true" : "false")
- name: Checkout pull request branch
if: steps.meta.outputs.should_run == 'true'
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.meta.outputs.head_ref }}
- name: Install Node.js
if: steps.meta.outputs.should_run == 'true'
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm
if: steps.meta.outputs.should_run == 'true'
uses: pnpm/action-setup@v4
with:
version: 9.0.6
run_install: false
- name: Get pnpm store directory
if: steps.meta.outputs.should_run == 'true'
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
if: steps.meta.outputs.should_run == 'true'
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
if: steps.meta.outputs.should_run == 'true'
run: pnpm install
- name: Apply formatting
if: steps.meta.outputs.should_run == 'true'
run: pnpm format:write
- name: Commit formatting changes
if: steps.meta.outputs.should_run == 'true'
run: |
if git diff --quiet; then
echo "No formatting changes to commit."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "style: apply automated formatting"
git push origin HEAD:${{ steps.meta.outputs.head_ref }}

2
.gitignore vendored
View File

@@ -15,7 +15,6 @@ build
# misc
.DS_Store
.eslintcache
*.pem
# debug
@@ -44,4 +43,3 @@ tsconfig.tsbuildinfo
.notes
.playwright-mcp
shadcn-workspace
.codex-artifacts

View File

@@ -1,10 +1,8 @@
"use client"
import * as React from "react"
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 { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import {
Field,
FieldContent,
@@ -15,10 +13,11 @@ import {
FieldSeparator,
FieldSet,
FieldTitle,
} 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"
} 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"
export function AppearanceSettings() {
const [gpuCount, setGpuCount] = React.useState(8)

View File

@@ -1,20 +1,8 @@
"use client"
import * as React from "react"
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 { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
@@ -27,7 +15,18 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/styles/radix-nova/ui/dropdown-menu"
} from "@/examples/radix/ui/dropdown-menu"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
export function ButtonGroupDemo() {
const [label, setLabel] = React.useState("personal")

View File

@@ -1,21 +1,20 @@
"use client"
import * as React from "react"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { Button } from "@/styles/radix-nova/ui/button"
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
import { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/styles/radix-nova/ui/input-group"
} from "@/examples/radix/ui/input-group"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/styles/radix-nova/ui/tooltip"
} from "@/examples/radix/ui/tooltip"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
export function ButtonGroupInputGroup() {
const [voiceEnabled, setVoiceEnabled] = React.useState(false)

View File

@@ -1,10 +1,9 @@
"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>

View File

@@ -1,14 +1,13 @@
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { Button } from "@/styles/radix-nova/ui/button"
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
import { Button } from "@/examples/radix/ui/button"
import { ButtonGroup } from "@/examples/radix/ui/button-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/styles/radix-nova/ui/popover"
import { Separator } from "@/styles/radix-nova/ui/separator"
import { Textarea } from "@/styles/radix-nova/ui/textarea"
} 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"
export function ButtonGroupPopover() {
return (

View File

@@ -1,12 +1,10 @@
import { PlusIcon } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/styles/radix-nova/ui/avatar"
import { Button } from "@/styles/radix-nova/ui/button"
} from "@/examples/radix/ui/avatar"
import { Button } from "@/examples/radix/ui/button"
import {
Empty,
EmptyContent,
@@ -14,7 +12,8 @@ import {
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/styles/radix-nova/ui/empty"
} from "@/examples/radix/ui/empty"
import { PlusIcon } from "lucide-react"
export function EmptyAvatarGroup() {
return (

View File

@@ -1,5 +1,5 @@
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
import { Checkbox } from "@/examples/radix/ui/checkbox"
import { Field, FieldLabel } from "@/examples/radix/ui/field"
export function FieldCheckbox() {
return (

View File

@@ -1,5 +1,5 @@
import { Button } from "@/styles/radix-nova/ui/button"
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
import { Button } from "@/examples/radix/ui/button"
import { Checkbox } from "@/examples/radix/ui/checkbox"
import {
Field,
FieldDescription,
@@ -8,8 +8,8 @@ import {
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/styles/radix-nova/ui/field"
import { Input } from "@/styles/radix-nova/ui/input"
} from "@/examples/radix/ui/field"
import { Input } from "@/examples/radix/ui/input"
import {
Select,
SelectContent,
@@ -17,8 +17,8 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "@/styles/radix-nova/ui/select"
import { Textarea } from "@/styles/radix-nova/ui/textarea"
} from "@/examples/radix/ui/select"
import { Textarea } from "@/examples/radix/ui/textarea"
export function FieldDemo() {
return (

View File

@@ -1,5 +1,5 @@
import { Card, CardContent } from "@/styles/radix-nova/ui/card"
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
import { Card, CardContent } from "@/examples/radix/ui/card"
import { Checkbox } from "@/examples/radix/ui/checkbox"
import {
Field,
FieldDescription,
@@ -8,7 +8,7 @@ import {
FieldLegend,
FieldSet,
FieldTitle,
} from "@/styles/radix-nova/ui/field"
} from "@/examples/radix/ui/field"
const options = [
{

View File

@@ -1,13 +1,8 @@
"use client"
import { useState } from "react"
import {
Field,
FieldDescription,
FieldTitle,
} from "@/styles/radix-nova/ui/field"
import { Slider } from "@/styles/radix-nova/ui/slider"
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
import { Slider } from "@/examples/radix/ui/slider"
export function FieldSlider() {
const [value, setValue] = useState([200, 800])

View File

@@ -1,4 +1,4 @@
import { FieldSeparator } from "@/styles/radix-nova/ui/field"
import { FieldSeparator } from "@/examples/radix/ui/field"
import { AppearanceSettings } from "./appearance-settings"
import { ButtonGroupDemo } from "./button-group-demo"

View File

@@ -1,20 +1,19 @@
"use client"
import * as React from "react"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/styles/radix-nova/ui/input-group"
import { Label } from "@/styles/radix-nova/ui/label"
} from "@/examples/radix/ui/input-group"
import { Label } from "@/examples/radix/ui/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/styles/radix-nova/ui/popover"
} from "@/examples/radix/ui/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
export function InputGroupButtonExample() {
const [isFavorite, setIsFavorite] = React.useState(false)

View File

@@ -1,12 +1,9 @@
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/styles/radix-nova/ui/dropdown-menu"
} from "@/examples/radix/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
@@ -14,13 +11,15 @@ import {
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/styles/radix-nova/ui/input-group"
import { Separator } from "@/styles/radix-nova/ui/separator"
} from "@/examples/radix/ui/input-group"
import { Separator } from "@/examples/radix/ui/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/styles/radix-nova/ui/tooltip"
} from "@/examples/radix/ui/tooltip"
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
export function InputGroupDemo() {
return (
@@ -89,7 +88,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-background" />
<IconCheck className="size-3 text-white" />
</div>
</InputGroupAddon>
</InputGroup>

View File

@@ -1,6 +1,4 @@
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { Button } from "@/styles/radix-nova/ui/button"
import { Button } from "@/examples/radix/ui/button"
import {
Item,
ItemActions,
@@ -8,7 +6,8 @@ import {
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/styles/radix-nova/ui/item"
} from "@/examples/radix/ui/item"
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
export function ItemDemo() {
return (

View File

@@ -1,24 +1,8 @@
"use client"
import { useMemo, useState } from "react"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/styles/radix-nova/ui/avatar"
import { Badge } from "@/styles/radix-nova/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
import { Badge } from "@/examples/radix/ui/badge"
import {
Command,
CommandEmpty,
@@ -26,7 +10,7 @@ import {
CommandInput,
CommandItem,
CommandList,
} from "@/styles/radix-nova/ui/command"
} from "@/examples/radix/ui/command"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -39,25 +23,36 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/styles/radix-nova/ui/dropdown-menu"
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
} from "@/examples/radix/ui/dropdown-menu"
import { Field, FieldLabel } from "@/examples/radix/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/styles/radix-nova/ui/input-group"
} from "@/examples/radix/ui/input-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/styles/radix-nova/ui/popover"
import { Switch } from "@/styles/radix-nova/ui/switch"
} from "@/examples/radix/ui/popover"
import { Switch } from "@/examples/radix/ui/switch"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/styles/radix-nova/ui/tooltip"
} from "@/examples/radix/ui/tooltip"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
const SAMPLE_DATA = {
mentionable: [

View File

@@ -1,5 +1,5 @@
import { Badge } from "@/styles/radix-nova/ui/badge"
import { Spinner } from "@/styles/radix-nova/ui/spinner"
import { Badge } from "@/examples/radix/ui/badge"
import { Spinner } from "@/examples/radix/ui/spinner"
export function SpinnerBadge() {
return (

View File

@@ -1,4 +1,4 @@
import { Button } from "@/styles/radix-nova/ui/button"
import { Button } from "@/examples/radix/ui/button"
import {
Empty,
EmptyContent,
@@ -6,8 +6,8 @@ import {
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/styles/radix-nova/ui/empty"
import { Spinner } from "@/styles/radix-nova/ui/spinner"
} from "@/examples/radix/ui/empty"
import { Spinner } from "@/examples/radix/ui/spinner"
export function SpinnerEmpty() {
return (

View File

@@ -56,7 +56,7 @@ export default function BlocksLayout({
<a href="#blocks">Browse Blocks</a>
</Button>
<Button asChild variant="ghost" size="sm">
<Link href="/docs/components">View Components</Link>
<Link href="/docs/blocks">Add a block</Link>
</Button>
</PageActions>
</PageHeader>

View File

@@ -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="mb-6 font-heading text-xl font-semibold tracking-tight">
<h2 className="font-heading mb-6 text-xl font-semibold tracking-tight">
More Updates
</h2>
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">

View File

@@ -1,11 +1,8 @@
"use client"
import * as React from "react"
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 { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
Field,
FieldContent,
@@ -16,13 +13,13 @@ import {
FieldSeparator,
FieldSet,
FieldTitle,
} 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"
} 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"
const translations = {
ar: {

View File

@@ -1,21 +1,8 @@
"use client"
import * as React from "react"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} 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 { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
DropdownMenu,
DropdownMenuContent,
@@ -29,7 +16,20 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/styles/base-nova/ui-rtl/dropdown-menu"
} from "@/examples/base/ui-rtl/dropdown-menu"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {

View File

@@ -1,22 +1,22 @@
"use client"
import * as React from "react"
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 { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/styles/base-nova/ui-rtl/input-group"
} from "@/examples/base/ui-rtl/input-group"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/styles/base-nova/ui-rtl/tooltip"
} from "@/examples/base/ui-rtl/tooltip"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {

View File

@@ -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: {

View File

@@ -1,17 +1,17 @@
"use client"
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 { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} 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"
} 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"
const translations = {
ar: {

View File

@@ -1,15 +1,12 @@
"use client"
import { PlusIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/styles/base-nova/ui-rtl/avatar"
import { Button } from "@/styles/base-nova/ui-rtl/button"
} from "@/examples/base/ui-rtl/avatar"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Empty,
EmptyContent,
@@ -17,7 +14,10 @@ import {
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/styles/base-nova/ui-rtl/empty"
} from "@/examples/base/ui-rtl/empty"
import { PlusIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {

View File

@@ -1,8 +1,9 @@
"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: {

View File

@@ -1,8 +1,7 @@
"use client"
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 { Button } from "@/examples/base/ui-rtl/button"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import {
Field,
FieldDescription,
@@ -11,8 +10,8 @@ import {
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/styles/base-nova/ui-rtl/field"
import { Input } from "@/styles/base-nova/ui-rtl/input"
} from "@/examples/base/ui-rtl/field"
import { Input } from "@/examples/base/ui-rtl/input"
import {
Select,
SelectContent,
@@ -20,8 +19,10 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "@/styles/base-nova/ui-rtl/select"
import { Textarea } from "@/styles/base-nova/ui-rtl/textarea"
} from "@/examples/base/ui-rtl/select"
import { Textarea } from "@/examples/base/ui-rtl/textarea"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
@@ -139,7 +140,7 @@ export function FieldDemo() {
<div className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="rtl-exp-month">{t.month}</FieldLabel>
<Select defaultValue="">
<Select defaultValue="" items={months}>
<SelectTrigger id="rtl-exp-month">
<SelectValue placeholder="MM" />
</SelectTrigger>
@@ -156,7 +157,7 @@ export function FieldDemo() {
</Field>
<Field>
<FieldLabel htmlFor="rtl-exp-year">{t.year}</FieldLabel>
<Select defaultValue="">
<Select defaultValue="" items={years}>
<SelectTrigger id="rtl-exp-year">
<SelectValue placeholder="YYYY" />
</SelectTrigger>

View File

@@ -1,8 +1,7 @@
"use client"
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 { Card, CardContent } from "@/examples/base/ui-rtl/card"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import {
Field,
FieldDescription,
@@ -11,7 +10,9 @@ import {
FieldLegend,
FieldSet,
FieldTitle,
} from "@/styles/base-nova/ui-rtl/field"
} from "@/examples/base/ui-rtl/field"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {

View File

@@ -1,14 +1,14 @@
"use client"
import { useState } from "react"
import { useLanguageContext } from "@/components/language-selector"
import {
Field,
FieldDescription,
FieldTitle,
} from "@/styles/base-nova/ui-rtl/field"
import { Slider } from "@/styles/base-nova/ui-rtl/slider"
} from "@/examples/base/ui-rtl/field"
import { Slider } from "@/examples/base/ui-rtl/slider"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {

View File

@@ -1,12 +1,13 @@
"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"

View File

@@ -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 "@/styles/base-nova/ui-rtl/input-group"
import { Label } from "@/styles/base-nova/ui-rtl/label"
} from "@/examples/base/ui-rtl/input-group"
import { Label } from "@/examples/base/ui-rtl/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/styles/base-nova/ui-rtl/popover"
} from "@/examples/base/ui-rtl/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {

View File

@@ -1,5 +1,25 @@
"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,
@@ -9,26 +29,6 @@ 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: {

View File

@@ -1,9 +1,6 @@
"use client"
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
import { Button } from "@/styles/base-nova/ui-rtl/button"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Item,
ItemActions,
@@ -11,7 +8,10 @@ import {
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/styles/base-nova/ui-rtl/item"
} from "@/examples/base/ui-rtl/item"
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {

View File

@@ -1,6 +1,47 @@
"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,
@@ -14,47 +55,6 @@ 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: {

View File

@@ -1,8 +1,9 @@
"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: {

View File

@@ -1,7 +1,6 @@
"use client"
import { useLanguageContext } from "@/components/language-selector"
import { Button } from "@/styles/base-nova/ui-rtl/button"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Empty,
EmptyContent,
@@ -9,8 +8,10 @@ import {
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/styles/base-nova/ui-rtl/empty"
import { Spinner } from "@/styles/base-nova/ui-rtl/spinner"
} from "@/examples/base/ui-rtl/empty"
import { Spinner } from "@/examples/base/ui-rtl/spinner"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {

View File

@@ -1,8 +1,6 @@
"use client"
import Script from "next/script"
import { type RegistryItem } from "shadcn/schema"
import {
Command,
CommandDialog,
@@ -11,7 +9,9 @@ import {
CommandInput,
CommandItem,
CommandList,
} from "@/styles/base-nova/ui/command"
} from "@/examples/base/ui/command"
import { type RegistryItem } from "shadcn/schema"
import { useActionMenu } from "@/app/(create)/hooks/use-action-menu"
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"

View File

@@ -1,136 +0,0 @@
"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>
)
}

View File

@@ -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>) {

View File

@@ -1,34 +1,36 @@
"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 "@/styles/base-nova/ui/card"
import { FieldGroup, FieldSeparator } from "@/styles/base-nova/ui/field"
} 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"
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 { FONT_HEADING_OPTIONS, FONTS } from "@/app/(create)/lib/fonts"
import { FONTS } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function Customizer({
@@ -55,40 +57,22 @@ 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 **: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} />}
<FieldGroup className="flex-row gap-2.5 py-px md:flex-col md:gap-3.25">
<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>

View File

@@ -64,37 +64,18 @@ export function DesignSystemProvider({
history: "replace", // …or push updates into the iframe history.
})
const [isReady, setIsReady] = React.useState(false)
const {
style,
theme,
font,
fontHeading,
baseColor,
chartColor,
menuAccent,
menuColor,
radius,
} = searchParams
const { style, theme, font, baseColor, 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(() => {
initialFontSansRef.current =
document.documentElement.style.getPropertyValue("--font-sans")
initialFontHeadingRef.current =
document.documentElement.style.getPropertyValue("--font-heading")
return () => {
removeManagedBodyClasses(document.body)
@@ -105,18 +86,10 @@ export function DesignSystemProvider({
"--font-sans",
initialFontSansRef.current
)
} else {
document.documentElement.style.removeProperty("--font-sans")
return
}
if (initialFontHeadingRef.current) {
document.documentElement.style.setProperty(
"--font-heading",
initialFontHeadingRef.current
)
} else {
document.documentElement.style.removeProperty("--font-heading")
}
document.documentElement.style.removeProperty("--font-sans")
}
}, [])
@@ -151,29 +124,12 @@ export function DesignSystemProvider({
// 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.
if (selectedFont) {
document.documentElement.style.setProperty(
"--font-sans",
selectedFont.font.style.fontFamily
)
}
if (selectedHeadingFont) {
document.documentElement.style.setProperty(
"--font-heading",
selectedHeadingFont.font.style.fontFamily
)
const fontFamily = selectedFont.font.style.fontFamily
document.documentElement.style.setProperty("--font-sans", fontFamily)
}
setIsReady(true)
}, [
style,
theme,
font,
fontHeading,
baseColor,
selectedFont,
selectedHeadingFont,
])
}, [style, theme, font, baseColor, selectedFont])
const registryTheme = React.useMemo(() => {
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
@@ -184,13 +140,12 @@ export function DesignSystemProvider({
...DEFAULT_CONFIG,
baseColor,
theme,
chartColor,
menuAccent,
radius: effectiveRadius,
}
return buildRegistryTheme(config)
}, [baseColor, theme, chartColor, menuAccent, effectiveRadius])
}, [baseColor, theme, menuAccent, effectiveRadius])
// Use useLayoutEffect for synchronous CSS var updates.
React.useLayoutEffect(() => {

View File

@@ -2,6 +2,13 @@
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,
@@ -13,68 +20,28 @@ import {
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
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
}
import { type Font } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function FontPicker({
label,
param,
fonts,
isMobile,
anchorRef,
}: {
label: string
param: "font" | "fontHeading"
fonts: readonly FontPickerOption[]
fonts: readonly Font[]
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 === currentValue),
[fonts, currentValue]
() => fonts.find((font) => font.value === params.font),
[fonts, params.font]
)
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 pickerFonts =
param === "fontHeading"
? fonts.filter((font) => font.value !== "inherit")
: fonts
const groups = new Map<string, FontPickerOption[]>()
const groups = new Map<Font["type"], Font[]>()
for (const font of pickerFonts) {
for (const font of fonts) {
const existing = groups.get(font.type)
if (existing) {
existing.push(font)
@@ -89,25 +56,21 @@ export function FontPicker({
label: `${type.charAt(0).toUpperCase()}${type.slice(1)}`,
items,
}))
}, [fonts, param])
}, [fonts])
return (
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">{label}</div>
<div className="text-xs text-muted-foreground">Font</div>
<div className="text-sm font-medium text-foreground">
{displayFontName}
{currentFont?.name}
</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 ??
currentBodyFont?.font.style.fontFamily,
}}
style={{ fontFamily: currentFont?.font.style.fontFamily }}
>
Aa
</div>
@@ -119,19 +82,11 @@ export function FontPicker({
className="max-h-96"
>
<PickerRadioGroup
value={currentValue}
onValueChange={handleFontChange}
value={currentFont?.value}
onValueChange={(value) => {
setParams({ font: value as FontValue })
}}
>
{param === "fontHeading" ? (
<>
<PickerGroup>
<PickerRadioItem value="inherit" closeOnClick={isMobile}>
{inheritFontLabel}
</PickerRadioItem>
</PickerGroup>
<PickerSeparator />
</>
) : null}
{groupedFonts.map((group) => (
<PickerGroup key={group.type}>
<PickerLabel>{group.label}</PickerLabel>
@@ -150,7 +105,7 @@ export function FontPicker({
</PickerContent>
</Picker>
<LockButton
param={param}
param="font"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>

View File

@@ -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"

View File

@@ -2,16 +2,11 @@
import * as React from "react"
import Link from "next/link"
import { ChevronRightIcon } from "lucide-react"
import { type RegistryItem } from "shadcn/schema"
import { cn } from "@/lib/utils"
import { type Base } from "@/registry/bases"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/styles/base-nova/ui/collapsible"
} from "@/examples/base/ui/collapsible"
import {
Sidebar,
SidebarContent,
@@ -20,7 +15,12 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/styles/base-nova/ui/sidebar"
} from "@/examples/base/ui/sidebar"
import { ChevronRightIcon } from "lucide-react"
import { type RegistryItem } from "shadcn/schema"
import { cn } from "@/lib/utils"
import { type Base } from "@/registry/bases"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(create)/lib/utils"

View File

@@ -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 <PickerShortcut>R</PickerShortcut>
Reset
</PickerItem>
</PickerGroup>
</PickerContent>

View File

@@ -104,7 +104,7 @@ export function MenuColorPicker({
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Menu</div>
<div className="line-clamp-1 max-w-[80%] truncate text-sm font-medium text-foreground">
<div className="text-sm font-medium text-foreground">
{currentMenu?.label}
</div>
</div>

View File

@@ -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"

View File

@@ -31,10 +31,8 @@ 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
@@ -45,10 +43,8 @@ export function PresetPicker({
params.style,
params.baseColor,
params.theme,
params.chartColor,
params.iconLibrary,
params.font,
params.fontHeading,
params.menuAccent,
params.menuColor,
params.radius,
@@ -71,10 +67,8 @@ 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,

View File

@@ -10,7 +10,6 @@ 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,
@@ -71,15 +70,6 @@ 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", {

View File

@@ -1,14 +1,7 @@
"use client"
import * as React from "react"
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 { Button } from "@/examples/base/ui/button"
import {
Dialog,
DialogContent,
@@ -17,7 +10,7 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/styles/base-nova/ui/dialog"
} from "@/examples/base/ui/dialog"
import {
Field,
FieldContent,
@@ -27,15 +20,21 @@ import {
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/styles/base-nova/ui/field"
import { RadioGroup, RadioGroupItem } from "@/styles/base-nova/ui/radio-group"
import { Switch } from "@/styles/base-nova/ui/switch"
} from "@/examples/base/ui/field"
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui/radio-group"
import { Switch } from "@/examples/base/ui/switch"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/styles/base-nova/ui/tabs"
} from "@/examples/base/ui/tabs"
import { Copy01Icon, Globe02Icon, Tick02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { useConfig } from "@/hooks/use-config"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
import {
useDesignSystemSearchParams,
@@ -87,7 +86,7 @@ export function ProjectForm({
const rtlFlag = params.rtl ? " --rtl" : ""
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
return IS_LOCAL_DEV
return IS_LOCAL_DEV && !process.env.NEXT_PUBLIC_RC
? {
pnpm: `shadcn init${flags}`,
npm: `shadcn init${flags}`,
@@ -130,76 +129,69 @@ export function ProjectForm({
<DialogTrigger render={<Button className={cn(className)} />}>
Create Project
</DialogTrigger>
<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">
<DialogContent className="min-w-0 sm:max-w-sm">
<DialogHeader>
<DialogTitle>Create Project</DialogTitle>
<DialogDescription>
Pick a template and configure your project.
Pick a template and configure your project. Available for all major
React frameworks.
</DialogDescription>
</DialogHeader>
<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,
})
<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,
}}
/>
</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">
Create a monorepo
</FieldLabel>
<Switch
id="monorepo"
checked={params.template?.endsWith("-monorepo") ?? false}
disabled={!hasMonorepo}
onCheckedChange={(checked) => {
const framework = getFramework(params.template ?? "next")
setParams({
template: getTemplateValue(
framework,
checked === true
) as typeof params.template,
})
}}
/>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldLabel htmlFor="rtl">
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
Enable RTL support
</FieldLabel>
<Switch
id="rtl"
checked={params.rtl}
onCheckedChange={(checked) =>
setParams({ rtl: checked === true })
}
/>
</Field>
</FieldSet>
</FieldGroup>
<DialogFooter className="min-w-0">
<div className="flex w-full min-w-0 flex-col gap-3">
<Tabs
value={packageManager}
@@ -209,16 +201,16 @@ export function ProjectForm({
packageManager: value as PackageManager,
}))
}}
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
className="min-w-0 gap-0 overflow-hidden rounded-lg border bg-surface"
>
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
<TabsList className="bg-transparent font-mono">
<div className="flex items-center gap-2 px-1 py-1">
<TabsList className="font-mono">
{PACKAGE_MANAGERS.map((manager) => {
return (
<TabsTrigger
key={manager}
value={manager}
className="py-0 leading-none data-[state=active]:shadow-none"
className="data-[state=active]:shadow-none"
>
{manager}
</TabsTrigger>
@@ -242,7 +234,7 @@ export function ProjectForm({
{Object.entries(commands).map(([key, cmd]) => {
return (
<TabsContent key={key} value={key}>
<div className="relative overflow-hidden border-t bg-popover p-3">
<div className="relative overflow-hidden border-t border-border/50 bg-surface px-3 py-3 text-surface-foreground">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">
{cmd}
@@ -289,26 +281,23 @@ const TemplateGrid = React.memo(function TemplateGrid({
<RadioGroup
value={framework}
onValueChange={handleTemplateChange}
className="grid grid-cols-2 gap-2"
className="grid grid-cols-3 gap-2"
>
{TEMPLATES.map((item) => (
<FieldLabel
key={item.value}
htmlFor={`template-${item.value}`}
className="block w-full"
className="py-1"
>
<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">
<Field className="gap-0" orientation="horizontal">
<FieldContent className="flex flex-col items-center justify-center gap-2">
<div
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
className="size-6 text-foreground [&_svg]:size-6 *:[svg]:text-foreground!"
dangerouslySetInnerHTML={{
__html: item.logo,
}}
></div>
<FieldTitle>{item.title}</FieldTitle>
<FieldTitle className="text-xs">{item.title}</FieldTitle>
</FieldContent>
<RadioGroupItem
value={item.value}
@@ -321,55 +310,3 @@ 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>
)
})

View File

@@ -1,13 +1,12 @@
"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"
@@ -41,7 +40,7 @@ export function RandomizeScript() {
dangerouslySetInnerHTML={{
__html: `
(function() {
// Forward r key (shuffle) and Shift+R (reset).
// Forward R key
document.addEventListener('keydown', function(e) {
if ((e.key === 'r' || e.key === 'R') && !e.metaKey && !e.ctrlKey) {
if (
@@ -54,11 +53,8 @@ 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: type,
type: '${RANDOMIZE_FORWARD_TYPE}',
key: e.key
}, '*');
}

View File

@@ -9,7 +9,8 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/styles/base-nova/ui/alert-dialog"
} from "@/examples/base/ui/alert-dialog"
import { useReset } from "@/app/(create)/hooks/use-reset"
export function ResetDialog() {

View File

@@ -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"

View File

@@ -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 }) {

View File

@@ -1,9 +1,7 @@
"use client"
import * as React from "react"
import { Icons } from "@/components/icons"
import { Button } from "@/styles/base-nova/ui/button"
import { Button } from "@/examples/base/ui/button"
import {
Dialog,
DialogClose,
@@ -12,7 +10,9 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/styles/base-nova/ui/dialog"
} from "@/examples/base/ui/dialog"
import { Icons } from "@/components/icons"
const STORAGE_KEY = "shadcn-create-welcome-dialog"

View File

@@ -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(48)] [--gap:--spacing(4)] md:[--gap:--spacing(6)] 2xl:[--customizer-width:--spacing(56)]"
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)]"
>
<SiteHeader />
<main

View File

@@ -1,11 +1,40 @@
"use client"
import { getPresetCode } from "@/app/(create)/lib/preset-code"
import * as React from "react"
import { encodePreset, isPresetCode } from "shadcn/preset"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
// Returns the canonical preset code derived from the current search params.
// Returns the current preset code derived from search params.
export function usePresetCode() {
const [params] = useDesignSystemSearchParams()
return getPresetCode(params)
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,
])
}

View File

@@ -6,10 +6,8 @@ export type LockableParam =
| "style"
| "baseColor"
| "theme"
| "chartColor"
| "iconLibrary"
| "font"
| "fontHeading"
| "menuAccent"
| "menuColor"
| "radius"

View File

@@ -10,7 +10,6 @@ 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"
@@ -63,41 +62,9 @@ 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
@@ -124,16 +91,16 @@ export function useRandom() {
: 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,
@@ -156,7 +123,7 @@ export function useRandom() {
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "r" && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
if ((e.key === "r" || e.key === "R") && !e.metaKey && !e.ctrlKey) {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||

View File

@@ -7,7 +7,6 @@ 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()
@@ -25,17 +24,15 @@ 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: params.item,
item: "preview",
})
}, [setParams, params.base, params.item])
}, [setParams, params.base])
const handleShowResetDialogChange = React.useCallback(
(open: boolean) => {
@@ -49,46 +46,6 @@ 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,

View File

@@ -3,17 +3,13 @@ import dedent from "dedent"
import { UI_COMPONENTS } from "@/lib/components"
import {
buildRegistryBase,
getBodyFont,
getHeadingFont,
getInheritedHeadingFontValue,
fonts,
type DesignSystemConfig,
} from "@/registry/config"
// Builds step-by-step markdown instructions for manually setting up a project.
export function buildInstructions(config: DesignSystemConfig) {
const registryBase = buildRegistryBase(config)
const normalizedFontHeading =
config.fontHeading === config.font ? "inherit" : config.fontHeading
const dependencies = [
...(registryBase.dependencies ?? []),
@@ -29,23 +25,13 @@ export function buildInstructions(config: DesignSystemConfig) {
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
const font = getBodyFont(config.font)
const headingFont =
normalizedFontHeading === "inherit"
? undefined
: getHeadingFont(normalizedFontHeading)
const font = fonts.find((f) => f.name === `font-${config.font}`)
const sections = [
buildDependenciesSection(dependencies),
buildUtilsSection(),
buildCssSection(
lightVars,
darkVars,
normalizedFontHeading === "inherit"
? getInheritedHeadingFontValue(config.font)
: "var(--font-heading)"
),
buildFontSection(font, headingFont),
buildCssSection(lightVars, darkVars),
buildFontSection(font),
buildComponentsJsonSection(config),
buildAvailableComponentsSection(config),
config.rtl ? buildRtlSection(config) : null,
@@ -81,11 +67,7 @@ function buildUtilsSection() {
`
}
function buildCssSection(
lightVars: string,
darkVars: string,
fontHeadingValue: string
) {
function buildCssSection(lightVars: string, darkVars: string) {
return dedent`
## Step 3: Set up CSS
@@ -98,7 +80,6 @@ function buildCssSection(
@theme inline {
--font-sans: var(--font-sans);
--font-heading: ${fontHeadingValue};
--font-mono: var(--font-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
@@ -161,74 +142,40 @@ function buildCssSection(
`
}
function buildFontSection(
font: ReturnType<typeof getBodyFont>,
headingFont: ReturnType<typeof getHeadingFont>
) {
function buildFontSection(font: (typeof fonts)[number] | undefined) {
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 { ${nextImports} } from "next/font/google"
import { ${font.font.import} } from "next/font/google"
${nextDeclarations}
const fontSans = ${font.font.import}({
subsets: ["latin"],
variable: "${font.font.variable}",
})
// Add the font variable classes to your <html> className.
// <html ${nextHtmlClassName}>
// Add fontSans.variable to your <html> className.
// <html className={fontSans.variable}>
\`\`\`
### Other frameworks
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"}:
Add the font from [Google Fonts](${googleFontsUrl}) and set the \`${font.font.variable}\` CSS variable to the font family:
\`\`\`css
${otherFrameworkCss}
:root {
${font.font.variable}: ${font.font.family};
}
\`\`\`
`
}

View File

@@ -1,21 +0,0 @@
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")
})
})

View File

@@ -4,7 +4,6 @@ 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) {
@@ -16,10 +15,8 @@ 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",
@@ -31,9 +28,7 @@ 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"),

View File

@@ -1,11 +1,10 @@
import { NextResponse, type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import { isPresetCode } from "shadcn/preset"
import { encodePreset, 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 {
@@ -16,11 +15,21 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: result.error }, { status: 400 })
}
const rawPreset = searchParams.get("preset")
// Use the preset code from the URL if provided, otherwise encode one.
const presetParam = searchParams.get("preset")
const presetCode =
rawPreset && isPresetCode(rawPreset)
? rawPreset
: getPresetCode(result.data)
presetParam && isPresetCode(presetParam)
? presetParam
: encodePreset({
style: result.data.style,
baseColor: result.data.baseColor,
theme: result.data.theme,
iconLibrary: result.data.iconLibrary,
font: result.data.font,
radius: result.data.radius,
menuAccent: result.data.menuAccent,
menuColor: result.data.menuColor,
} as Parameters<typeof encodePreset>[0])
const registryBase = buildRegistryBase(result.data)
const parseResult = registryItemSchema.safeParse(registryBase)

View File

@@ -1,9 +1,7 @@
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) {
@@ -15,17 +13,11 @@ 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: presetCode,
preset: searchParams.get("preset") ?? "",
})
})

View File

@@ -3,37 +3,21 @@ 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",
@@ -54,6 +38,21 @@ 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",
@@ -79,51 +78,6 @@ 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",
@@ -149,85 +103,109 @@ 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 = [
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"),
{
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",
},
] 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]

View File

@@ -1,34 +0,0 @@
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)
}

View File

@@ -1,34 +0,0 @@
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",
})
})
})

View File

@@ -1,30 +0,0 @@
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,
}
}

View File

@@ -3,7 +3,6 @@ import type {
BaseColorName,
Radius,
StyleName,
Theme,
ThemeName,
} from "@/registry/config"
@@ -13,7 +12,6 @@ export type RandomizeContext = {
style?: StyleName
baseColor?: BaseColorName
theme?: ThemeName
chartColor?: string
iconLibrary?: string
font?: string
menuAccent?: string
@@ -28,30 +26,12 @@ export type BiasFilter<T> = (
export type RandomizeBiases = {
baseColors?: BiasFilter<BaseColor>
chartColors?: BiasFilter<Theme>
fonts?: BiasFilter<(typeof FONTS)[number]>
radius?: BiasFilter<Radius>
}
// 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"],
// Add more bias filters as needed:
// styles?: BiasFilter<Style>
// themes?: BiasFilter<Theme>
// etc.
}
/**
@@ -71,25 +51,21 @@ 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
},
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
},
// 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
// },
}
/**

View File

@@ -1,5 +1,4 @@
import * as React from "react"
import { useSearchParams } from "next/navigation"
import { useQueryStates } from "nuqs"
import {
createLoader,
@@ -11,13 +10,12 @@ import {
type inferParserType,
type Options,
} from "nuqs/server"
import { decodePreset, isPresetCode } from "shadcn/preset"
import { decodePreset, encodePreset, isPresetCode } from "shadcn/preset"
import {
BASE_COLORS,
BASES,
DEFAULT_CONFIG,
getThemesForBaseColor,
iconLibraries,
MENU_ACCENTS,
MENU_COLORS,
@@ -26,8 +24,6 @@ import {
THEMES,
type BaseColorName,
type BaseName,
type ChartColorName,
type FontHeadingValue,
type FontValue,
type IconLibraryName,
type MenuAccentValue,
@@ -37,11 +33,9 @@ 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("b0"),
preset: parseAsString.withDefault("a0"),
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
DEFAULT_CONFIG.base
),
@@ -55,16 +49,9 @@ 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),
@@ -100,24 +87,13 @@ 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 = [
@@ -169,83 +145,39 @@ function normalizePartialDesignSystemParams(
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)
params.menuAccent === "bold" &&
isTranslucentMenuColor(params.menuColor)
) {
return {
...result,
...params,
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)
return params
}
// 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(
() => resolvePresetParams(rawParams, searchParams),
[rawParams, searchParams]
() =>
rawParams.preset && isPresetCode(rawParams.preset)
? normalizeDesignSystemParams({
...rawParams,
...(decodePreset(rawParams.preset) ?? {}),
})
: normalizeDesignSystemParams(rawParams),
[rawParams]
)
// Use ref so setParams callback stays stable across renders.
@@ -283,10 +215,21 @@ export function useDesignSystemSearchParams(options: Options = {}) {
...paramsRef.current,
...resolvedUpdates,
})
// Encode design system fields into a preset code.
// Cast needed: merged values may include null from nuqs resets,
// but encodePreset handles missing values by falling back to defaults.
const code = getPresetCode(merged)
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])
// Build update: set preset, clear individual DS params from URL.
const rawUpdate: Record<string, unknown> = { preset: code }
for (const key of DESIGN_SYSTEM_KEYS) {

View File

@@ -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(--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>',
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>',
},
{
value: "start",

View File

@@ -1,115 +0,0 @@
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")
})
})

View File

@@ -5,28 +5,19 @@ import {
type configSchema,
type RegistryItem,
} from "shadcn/schema"
import {
transformFont,
transformIcons,
transformMenu,
transformRender,
} from "shadcn/utils"
import { Project, ScriptKind, type SourceFile } from "ts-morph"
import { transformIcons, transformMenu, transformRender } from "shadcn/utils"
import { Project, ScriptKind } from "ts-morph"
import { z } from "zod"
import {
buildRegistryBase,
getBodyFont,
getHeadingFont,
getInheritedHeadingFontValue,
fonts,
type DesignSystemConfig,
} from "@/registry/config"
const { Index } = await import("@/registry/bases/__index__")
function buildThemeInline(fontHeadingValue: string) {
return `--font-sans: var(--font-sans);
--font-heading: ${fontHeadingValue};
const THEME_INLINE = `--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--color-background: var(--background);
@@ -68,7 +59,6 @@ function buildThemeInline(fontHeadingValue: string) {
--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({
@@ -158,20 +148,7 @@ const ALIASES = {
hooks: "@/hooks",
} as const
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,
]
const transformers = [transformIcons, transformMenu, transformRender]
function getStyle(designSystemConfig: DesignSystemConfig) {
return `${designSystemConfig.base}-${designSystemConfig.style}`
@@ -179,18 +156,10 @@ 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,
designSystemConfig.font,
normalizedFontHeading
)
const layoutFile = buildLayoutFile(designSystemConfig, normalizedFontHeading)
const globalsCss = buildGlobalsCss(registryBase)
const layoutFile = buildLayoutFile(designSystemConfig)
const componentFiles = await buildComponentFiles(designSystemConfig)
const dependencies = [...(registryBase.dependencies ?? []), "next-themes"]
@@ -212,11 +181,7 @@ export async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
})
}
function buildGlobalsCss(
registryBase: RegistryItem,
font: DesignSystemConfig["font"],
fontHeading: DesignSystemConfig["fontHeading"]
) {
function buildGlobalsCss(registryBase: RegistryItem) {
const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
@@ -229,15 +194,11 @@ function buildGlobalsCss(
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@custom-variant dark (&:is(.dark *));
@theme inline {
${buildThemeInline(
fontHeading === "inherit"
? getInheritedHeadingFontValue(font)
: "var(--font-heading)"
)}
}
@theme inline {
${THEME_INLINE}
}
:root {
${lightVars}
@@ -371,23 +332,18 @@ function buildPackageJson(dependencies: string[]) {
})
}
function buildLayoutFile(
designSystemConfig: DesignSystemConfig,
fontHeading: DesignSystemConfig["fontHeading"]
) {
const font = getBodyFont(designSystemConfig.font)
function buildLayoutFile(designSystemConfig: DesignSystemConfig) {
const font = fonts.find(
(font) => font.name === `font-${designSystemConfig.font}`
)
if (!font) {
throw new Error(`Font "${designSystemConfig.font}" not found`)
}
const 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 =
@@ -398,26 +354,14 @@ function buildLayoutFile(
: ""
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 { ${imports} } from "next/font/google";
import { ${font.font.import} } 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",
@@ -430,7 +374,7 @@ function buildLayoutFile(
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning className=${htmlClassName}>
<html lang="en" suppressHydrationWarning className={${constName}.variable}>
<body
className="${bodyClassName}"
>
@@ -457,14 +401,13 @@ async function buildComponentFiles(designSystemConfig: DesignSystemConfig) {
// Build config once for all components.
const config = buildTransformConfig(designSystemConfig)
// Fetch UI components, the demo, and the item component in parallel.
const [registryItemFiles, demoFile, itemComponentFile] = await Promise.all([
// Fetch UI components and the item component in parallel.
const [registryItemFiles, 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,
@@ -472,24 +415,29 @@ 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 { Demo } from "@/components/demo"
import { Button } from "@/components/ui/button"
export default function Page() {
return <Demo />
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&apos;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>
)
}
`,
}
@@ -609,7 +557,6 @@ async function transformFileContent(
raw: content,
sourceFile,
config,
supportedFontMarkers: ["cn-font-heading"],
})
}

View File

@@ -2,10 +2,7 @@
import * as React from "react"
import { Label, Pie, PieChart, Sector } from "recharts"
import type {
PieSectorDataItem,
PieSectorShapeProps,
} from "recharts/types/polar/Pie"
import { type PieSectorDataItem } from "recharts/types/polar/Pie"
import {
Card,
@@ -80,26 +77,6 @@ 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} />
@@ -159,7 +136,20 @@ export function ChartVisitors() {
nameKey="month"
innerRadius={60}
strokeWidth={5}
shape={renderPieShape}
activeIndex={activeIndex}
activeShape={({
outerRadius = 0,
...props
}: PieSectorDataItem) => (
<g>
<Sector {...props} outerRadius={outerRadius + 10} />
<Sector
{...props}
outerRadius={outerRadius + 25}
innerRadius={outerRadius + 12}
/>
</g>
)}
>
<Label
content={({ viewBox }) => {

View File

@@ -86,7 +86,7 @@ export function ChartBarMixed() {
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Bar dataKey="visitors" radius={5} />
<Bar dataKey="visitors" layout="vertical" radius={5} />
</BarChart>
</ChartContainer>
</CardContent>

View File

@@ -12,7 +12,7 @@ import {
import { absoluteUrl } from "@/lib/utils"
import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles"
import "@/app/legacy-themes.css"
import "@/styles/legacy-themes.css"
import { ComponentPreview } from "./component-preview"

View File

@@ -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 "@/app/globals.css"
import "@/styles/globals.css"
export const metadata: Metadata = {
title: {

View File

@@ -2,6 +2,11 @@
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"
@@ -16,11 +21,6 @@ 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,

View File

@@ -1,8 +1,6 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import {
Select,
SelectContent,
@@ -10,7 +8,9 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "@/styles/base-nova/ui/select"
} from "@/examples/base/ui/select"
import { cn } from "@/lib/utils"
export type Language = "en" | "ar" | "he"

View File

@@ -7,6 +7,12 @@ 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"
@@ -29,33 +35,40 @@ export function ModeSwitcher({
}, [resolvedTheme, setTheme])
return (
<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>
<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>
)
}

View File

@@ -1,5 +1,5 @@
import { cn } from "@/lib/utils"
import { Button } from "@/styles/base-nova/ui/button"
import { Button } from "@/registry/new-york-v4/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 variant="outline" size="sm" className="mt-2 w-fit">
<Button size="sm" className="mt-2 w-fit">
Deploy Now
</Button>
<a

View File

@@ -1,4 +1,5 @@
import Link from "next/link"
import { Button } from "@/examples/radix/ui/button"
import { PlusSignIcon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
@@ -13,7 +14,6 @@ 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"

View File

@@ -1,9 +1,6 @@
"use client"
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 { Label } from "@/examples/base/ui/label"
import {
Select,
SelectContent,
@@ -12,7 +9,11 @@ import {
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/styles/base-nova/ui/select"
} from "@/examples/base/ui/select"
import { THEMES } from "@/lib/themes"
import { cn } from "@/lib/utils"
import { useThemeConfig } from "@/components/active-theme"
import { CopyCodeButton } from "./theme-customizer"

View File

@@ -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 theme tokens for your components. **This cannot be changed after initialization.**
This is used to generate the default color palette for your components. **This cannot be changed after initialization.**
```json title="components.json"
{
"tailwind": {
"baseColor": "neutral" | "stone" | "zinc" | "mauve" | "olive" | "mist" | "taupe"
"baseColor": "gray" | "neutral" | "slate" | "stone" | "zinc" | "mauve" | "olive" | "mist" | "taupe"
}
}
```
### tailwind.cssVariables
We use and recommend CSS variables for theming.
You can choose between using CSS variables or Tailwind CSS utility classes for theming.
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.
To use utility classes for theming set `tailwind.cssVariables` to `false`. For CSS variables, set `tailwind.cssVariables` to `true`.
```json title="components.json"
{

View File

@@ -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.
- [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/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/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.

View File

@@ -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 buttons behavior, you have to override styles or wrap the component. With shadcn/ui, you simply edit the button code directly._
<Accordion type="single" collapsible>
<Accordion collapsible>
<AccordionItem value="faq-1" className="border-none">
<AccordionTrigger>
How do I pull upstream updates in an Open Code approach?

View File

@@ -3,7 +3,7 @@ title: Your project is ready!
description: You've created a new project with shadcn/ui.
---
Here are a few things you can do to get started building with shadcn/ui.
Here's a few things you can do to get started building with shadcn/ui.
## Add Components

View File

@@ -121,7 +121,7 @@ The process for adding components is the same as above. Select a flag to resolve
## Upgrade Status
To make it easy for you to track the progress of the upgrade, here is a table with the React 19 support status for the shadcn/ui dependencies.
To make it easy for you track the progress of the upgrade, I've created a table below with React 19 support status for the shadcn/ui dependencies.
- ✅ - Works with React 19 using npm, pnpm, and bun.
- 🚧 - Works with React 19 using pnpm and bun. Requires flag for npm. PR is in progress.

View File

@@ -193,7 +193,7 @@ Here's how you do it:
}
```
This change makes it much simpler to access your theme variables in both utility classes and outside of CSS, e.g. using color values in JavaScript.
This change makes it much simpler to access your theme variables in both utility classes and outside of CSS for eg. using color values in JavaScript.
### 3. Update colors for charts
@@ -281,7 +281,7 @@ function AccordionItem({
We've deprecated `tailwindcss-animate` in favor of `tw-animate-css`.
New projects will have `tw-animate-css` installed by default.
New project will have `tw-animate-css` installed by default.
For existing projects, follow the steps below to migrate.

View File

@@ -1,48 +1,46 @@
---
title: Theming
description: Using CSS variables and theme tokens.
description: Using CSS Variables and color utilities for theming.
---
<Callout>
You can choose between using CSS variables (recommended) or utility classes for theming.
Want to build your theme visually? Use [shadcn/create](/create) to preview colors, radius, fonts, and icons, then generate a preset for your project.
</Callout>
We use and recommend CSS variables for theming.
This gives you semantic theme tokens like `background`, `foreground`, and `primary` that components use by default. Override those tokens in your CSS to change the look of your app without rewriting component classes.
## CSS Variables
```tsx /bg-background/ /text-foreground/
<div className="bg-background text-foreground" />
```
To use CSS variables for theming, set `tailwind.cssVariables` to `true` in your `components.json` file. This is the default.
To use CSS variables for theming set `tailwind.cssVariables` to `true` in your `components.json` file.
```json {8} title="components.json" showLineNumbers
{
"style": "base-nova",
"style": "new-york",
"rsc": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true
}
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
```
Tailwind maps these tokens into utilities like `bg-background`, `text-foreground`, `border-border`, and `ring-ring`.
## Convention
Dark mode works by overriding the same tokens inside a `.dark` selector. See the [dark mode docs](/docs/dark-mode/next) for adding a theme provider and toggling the `.dark` class.
## Token Convention
We use semantic background and foreground pairs. The base token controls the surface color and the `-foreground` token controls the text and icon color that sits on that surface.
We use a simple `background` and `foreground` convention for colors. The `background` variable is used for the background color of the component and the `foreground` variable is used for the text color.
<Callout className="mt-4">
The background suffix is omitted for the surface token. For example, `primary` pairs with `primary-foreground`.
The `background` suffix is omitted when the variable is used for the background color of the component.
</Callout>
@@ -59,148 +57,11 @@ The `background` color of the following component will be `var(--primary)` and t
<div className="bg-primary text-primary-foreground">Hello</div>
```
## Theme Tokens
## List of variables
These tokens live in your CSS file under `:root` and `.dark`.
| Token | What it controls | Used by |
| ------------------------------------------------ | ------------------------------------------------------ | ---------------------------------------------------------------------------- |
| `background` / `foreground` | The default app background and text color. | The page shell, page sections, and default text. |
| `card` / `card-foreground` | Elevated surfaces and the content inside them. | `Card`, dashboard panels, settings panels. |
| `popover` / `popover-foreground` | Floating surfaces and the content inside them. | `Popover`, `DropdownMenu`, `ContextMenu`, and other overlays. |
| `primary` / `primary-foreground` | High-emphasis actions and brand surfaces. | Default `Button`, selected states, badges, and active accents. |
| `secondary` / `secondary-foreground` | Lower-emphasis filled actions and supporting surfaces. | Secondary buttons, secondary badges, and supporting UI. |
| `muted` / `muted-foreground` | Subtle surfaces and lower-emphasis content. | Descriptions, placeholders, empty states, helper text, and subdued surfaces. |
| `accent` / `accent-foreground` | Interactive hover, focus, and active surfaces. | Ghost buttons, menu highlight states, hovered rows, and selected items. |
| `destructive` | Destructive actions and error emphasis. | Destructive buttons, invalid states, and destructive menu items. |
| `border` | Default borders and separators. | Cards, menus, tables, separators, and layout dividers. |
| `input` | Form control borders and input surface treatment. | `Input`, `Textarea`, `Select`, and outline-style controls. |
| `ring` | Focus rings and outlines. | Buttons, inputs, checkboxes, menus, and other focusable controls. |
| `chart-1` ... `chart-5` | The default chart palette. | Charts and chart-driven dashboard blocks. |
| `sidebar` / `sidebar-foreground` | The base sidebar surface and default sidebar text. | The `Sidebar` container and its default content. |
| `sidebar-primary` / `sidebar-primary-foreground` | High-emphasis actions inside the sidebar. | Active items, icon tiles, badges, and sidebar CTAs. |
| `sidebar-accent` / `sidebar-accent-foreground` | Hover and selected states inside the sidebar. | Sidebar menu hover states, open items, and interactive rows. |
| `sidebar-border` | Sidebar-specific borders and separators. | Sidebar headers, groups, and internal dividers. |
| `sidebar-ring` | Sidebar-specific focus rings. | Focused controls inside the sidebar. |
| `radius` | The base corner radius scale. | Cards, inputs, buttons, popovers, and the derived `radius-*` tokens. |
<Callout className="mt-4">
The chart tokens are covered in more detail in the [Chart theming docs](/docs/components/chart#theming).
</Callout>
## Radius Scale
`--radius` is the base radius token for your theme.
We derive a small radius scale from it so components can use consistent corner sizes while still sharing a single source of truth.
Here's the list of variables available for customization:
```css title="app/globals.css" showLineNumbers
@theme inline {
--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);
}
```
This means:
- `radius-lg` is the base value.
- Smaller radii scale down from `--radius`.
- Larger radii scale up from `--radius`.
- Changing `--radius` updates the entire radius scale.
## Adding New Tokens
To add a new token, define it under `:root` and `.dark`, then expose it to Tailwind with `@theme inline`.
```css title="app/globals.css" showLineNumbers
:root {
--warning: oklch(0.84 0.16 84);
--warning-foreground: oklch(0.28 0.07 46);
}
.dark {
--warning: oklch(0.41 0.11 46);
--warning-foreground: oklch(0.99 0.02 95);
}
@theme inline {
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
}
```
You can now use `bg-warning` and `text-warning-foreground` in your components.
```tsx /bg-warning/ /text-warning-foreground/
<div className="bg-warning text-warning-foreground" />
```
## Base Colors
`tailwind.baseColor` controls the default token values generated for your project when you run `init` or use a preset.
The available base colors are: **Neutral**, **Stone**, **Zinc**, **Mauve**, **Olive**, **Mist**, and **Taupe**.
## Default Theme CSS
The following is the full default `neutral` theme scaffold. Copy it into your global CSS file and adjust the tokens as needed.
<CodeCollapsibleWrapper>
```css showLineNumbers title="app/globals.css"
@import "tailwindcss";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--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-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
@@ -218,6 +79,7 @@ The following is the full default `neutral` theme scaffold. Copy it into your gl
--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.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
@@ -241,7 +103,7 @@ The following is the full default `neutral` theme scaffold. Copy it into your gl
--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: oklch(0.269 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
@@ -249,9 +111,10 @@ The following is the full default `neutral` theme scaffold. Copy it into your gl
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 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.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
@@ -267,38 +130,43 @@ The following is the full default `neutral` theme scaffold. Copy it into your gl
--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.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
--sidebar-ring: oklch(0.439 0 0);
}
```
</CodeCollapsibleWrapper>
## Adding new colors
## Without CSS Variables
To add new colors, you need to add them to your CSS file under the `:root` and `dark` pseudo-classes. Then, use the `@theme inline` directive to make the colors available as CSS variables.
If you do not want to use CSS variables, the CLI can generate components with inline Tailwind color utilities instead.
```css title="app/globals.css" showLineNumbers
:root {
--warning: oklch(0.84 0.16 84);
--warning-foreground: oklch(0.28 0.07 46);
}
```bash
npx shadcn@latest init --no-css-variables
.dark {
--warning: oklch(0.41 0.11 46);
--warning-foreground: oklch(0.99 0.02 95);
}
@theme inline {
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
}
```
This sets `tailwind.cssVariables` to `false` in your `components.json` file.
You can now use the `warning` utility class in your components.
```tsx /bg-zinc-950/ /text-zinc-50/ /dark:bg-white/ /dark:text-zinc-950/
<div className="bg-zinc-950 text-zinc-50 dark:bg-white dark:text-zinc-950" />
```tsx /bg-warning/ /text-warning-foreground/
<div className="bg-warning text-warning-foreground" />
```
<Callout className="mt-4">
## Other color formats
This is an installation-time choice. To switch an existing project, delete and re-install your components.
See the [Tailwind CSS documentation](https://tailwindcss.com/docs/colors) for more information on using colors in Tailwind CSS.
</Callout>
## Base Colors
The available base colors are: **Neutral**, **Stone**, **Zinc**, **Mauve**, **Olive**, **Mist**, and **Taupe**.
You can set the base color when initializing your project using a preset or by updating the `baseColor` field in your `components.json` file.

View File

@@ -6,7 +6,7 @@ date: 2024-03-22
One of the most requested features since launch has been layouts: admin dashboards with sidebar, marketing page sections, cards and more.
**Today, we're launching [Blocks](/blocks).**
**Today, we're launching [**Blocks**](/blocks)**.
<a href="/blocks">
<Image

View File

@@ -22,4 +22,4 @@ A fully featured input OTP component. It has support for numeric and alphanumeri
[Read the docs](/docs/components/input-otp)
If you have a [v0](https://v0.dev) account, the new components are available for generation.
If you have a [v0](https://v0.dev), the new components are available for generation.

View File

@@ -8,10 +8,10 @@ The new CLI is now available. It's a complete rewrite with a lot of new features
This is a major step towards distributing code that you and your LLMs can access and use.
1. First up, the CLI now has support for all major React frameworks out of the box. Next.js, Remix, Vite and Laravel. And when you init into a new app, we update your existing Tailwind files instead of overriding.
2. A component now ships its own dependencies. Take the accordion for example, it can define its Tailwind keyframes. When you add it to your project, we'll update your tailwind.config.ts file accordingly.
1. First up, the cli now has support for all major React framework out of the box. Next.js, Remix, Vite and Laravel. And when you init into a new app, we update your existing Tailwind files instead of overriding.
2. A component now ship its own dependencies. Take the accordion for example, it can define its Tailwind keyframes. When you add it to your project, we'll update your tailwind.config.ts file accordingly.
3. You can also install remote components using url. `npx shadcn add https://acme.com/registry/navbar.json`.
4. We have also improved the init command. It does framework detection and can even init a brand new Next.js app in one command. `npx shadcn init`.
4. We have also improve the init command. It does framework detection and can even init a brand new Next.js app in one command. `npx shadcn init`.
5. We have created a new schema that you can use to ship your own component registry. And since it has support for urls, you can even use it to distribute private components.
6. And a few more updates like better error handling and monorepo support.
@@ -42,4 +42,4 @@ To update an existing project to use the new CLI, update your `components.json`
}
```
If you're using a different import alias prefix, e.g. `~`, replace `@` with your prefix.
If you're using a different import alias prefix eg `~`, replace `@` with your prefix.

View File

@@ -1,9 +1,9 @@
---
title: April 2025 - Cross-framework Route Support
description: The shadcn CLI can now auto-detect your framework and adapt routes for you.
title: March 2025 - Cross-framework Route Support
description: The shadcn CLI can now auto-detect your framework and adapts routes for you.
date: 2025-04-09
---
The shadcn CLI can now auto-detect your framework and adapt routes for you.
The shadcn CLI can now auto-detect your framework and adapts routes for you.
Works with all frameworks including Laravel, Vite and React Router.

View File

@@ -10,7 +10,7 @@ We're working on zero-config MCP support for shadcn/ui registry. One command `np
src="/images/mcp.jpeg"
width="1432"
height="1050"
alt="MCP Server"
alt="Lift Mode"
className="mt-6 w-full overflow-hidden rounded-lg border"
/>

View File

@@ -1,5 +1,5 @@
---
title: April 2025 - shadcn 2.5.0
title: March 2025 - shadcn 2.5.0
description: Resolve anywhere - registries can now place files anywhere in an app.
date: 2025-04-26
---

Some files were not shown because too many files have changed in this diff Show More