Compare commits

..

1 Commits

Author SHA1 Message Date
shadcn
86479e0a30 fix: init e2e tests 2026-03-10 17:21:19 +04:00
4261 changed files with 238856 additions and 16380 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).

78
.github/workflows/deprecated.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: Deprecated
on:
pull_request_target:
types: [opened, synchronize]
permissions:
issues: write
contents: read
pull-requests: write
jobs:
deprecated:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
with:
files: |
apps/www/**
files_ignore: |
apps/www/public/r/**
base_sha: ${{ github.event.pull_request.base.sha }}
sha: ${{ github.event.pull_request.head.sha }}
- name: Comment on PR if www files changed
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
const wwwFiles = changedFiles.filter(file =>
file.startsWith('apps/www/') &&
!file.startsWith('apps/www/public/r/') &&
file !== 'apps/www/package.json'
);
if (wwwFiles.length > 0) {
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
Consider applying the change to \`apps/v4\` if relevant.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
// Add deprecated label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['deprecated']
});
} else {
// Remove deprecated label if no www files are changed
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'deprecated'
});
} catch (error) {
// Label doesn't exist, which is fine
console.log('Deprecated label not found, skipping removal');
}
}

View File

@@ -16,7 +16,7 @@ jobs:
check-registry-sync:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
name: check-registry-sync
name: Check registry sync
permissions:
contents: read
pull-requests: write
@@ -66,44 +66,6 @@ jobs:
with:
node-version: 20
- name: Block reserved registry namespaces
env:
RESERVED_NAMESPACES: "@shadcn,@ui,@blocks,@components,@block,@component,@util,@utils,@registry,@lib,@hook,@hooks,@theme,@themes,@chart,@charts"
run: |
node <<'EOF'
const fs = require("node:fs")
const files = [
"apps/v4/public/r/registries.json",
"apps/v4/registry/directory.json",
]
const reservedNamespaces = new Set(
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
)
function readNames(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
(entry) => entry.name
)
}
const violations = files.flatMap((filePath) => {
return readNames(filePath)
.filter((name) => reservedNamespaces.has(name))
.map((name) => `${filePath}: ${name}`)
})
if (violations.length > 0) {
console.error("Reserved registry namespaces are not allowed:")
for (const violation of violations) {
console.error(`- ${violation}`)
}
process.exit(1)
}
EOF
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install

View File

@@ -5,4 +5,3 @@ build
.contentlayer
**/fixtures
deprecated
apps/v4/registry/styles/**/*.css

View File

@@ -5,4 +5,3 @@ build
.contentlayer
registry/__index__.tsx
content/docs/components/calendar.mdx
registry/styles/**/*.css

View File

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

@@ -82,11 +82,6 @@ export function MenuAccentPicker({
key={accent.value}
value={accent.value}
closeOnClick={isMobile}
disabled={
accent.value === "bold" &&
(params.menuColor === "default-translucent" ||
params.menuColor === "inverted-translucent")
}
>
{accent.label}
</PickerRadioItem>

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

@@ -7,7 +7,9 @@ import {
CardFooter,
CardHeader,
} from "@/examples/base/ui/card"
import { FieldGroup, FieldSeparator } from "@/examples/base/ui/field"
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"
@@ -16,19 +18,19 @@ 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

@@ -6,6 +6,7 @@ import {
buildRegistryTheme,
DEFAULT_CONFIG,
type DesignSystemConfig,
type RadiusValue,
} from "@/registry/config"
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
import { FONTS } from "@/app/(create)/lib/fonts"
@@ -14,46 +15,6 @@ import {
type DesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
type RegistryThemeCssVars = NonNullable<
ReturnType<typeof buildRegistryTheme>["cssVars"]
>
function removeManagedBodyClasses(body: Element) {
for (const className of Array.from(body.classList)) {
if (
MANAGED_BODY_CLASS_PREFIXES.some((prefix) => className.startsWith(prefix))
) {
body.classList.remove(className)
}
}
}
function buildCssRule(selector: string, cssVars?: Record<string, string>) {
const declarations = Object.entries(cssVars ?? {})
.filter(([, value]) => Boolean(value))
.map(([key, value]) => ` --${key}: ${value};`)
.join("\n")
if (!declarations) {
return `${selector} {}\n`
}
return `${selector} {\n${declarations}\n}\n`
}
function buildThemeCssText(cssVars: RegistryThemeCssVars) {
return [
buildCssRule(":root", {
...(cssVars.theme ?? {}),
...(cssVars.light ?? {}),
}),
buildCssRule(".dark", cssVars.dark),
].join("\n")
}
export function DesignSystemProvider({
children,
}: {
@@ -63,65 +24,19 @@ export function DesignSystemProvider({
shallow: true, // No need to go through the server…
history: "replace", // …or push updates into the iframe history.
})
const [liveSearchParams, setLiveSearchParams] = React.useState(searchParams)
const [isReady, setIsReady] = React.useState(false)
const {
style,
theme,
font,
fontHeading,
baseColor,
chartColor,
menuAccent,
menuColor,
radius,
} = searchParams
const { style, theme, font, baseColor, menuAccent, menuColor, radius } =
liveSearchParams
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)
document.getElementById(THEME_STYLE_ELEMENT_ID)?.remove()
if (initialFontSansRef.current) {
document.documentElement.style.setProperty(
"--font-sans",
initialFontSansRef.current
)
} else {
document.documentElement.style.removeProperty("--font-sans")
}
if (initialFontHeadingRef.current) {
document.documentElement.style.setProperty(
"--font-heading",
initialFontHeadingRef.current
)
} else {
document.documentElement.style.removeProperty("--font-heading")
}
}
}, [])
setLiveSearchParams(searchParams)
}, [searchParams])
const handleDesignSystemMessage = React.useCallback(
(nextParams: DesignSystemSearchParams) => {
setLiveSearchParams(nextParams)
setSearchParams(nextParams)
},
[setSearchParams]
@@ -131,7 +46,11 @@ export function DesignSystemProvider({
React.useEffect(() => {
if (style === "lyra" && radius !== "none") {
setSearchParams({ radius: "none" })
setLiveSearchParams((prev) => ({
...prev,
radius: "none",
}))
setSearchParams({ radius: "none" as RadiusValue })
}
}, [style, radius, setSearchParams])
@@ -144,36 +63,27 @@ export function DesignSystemProvider({
const body = document.body
// Iterate over a snapshot so removals do not affect traversal.
removeManagedBodyClasses(body)
Array.from(body.classList).forEach((className) => {
if (
className.startsWith("style-") ||
className.startsWith("base-color-")
) {
body.classList.remove(className)
}
})
body.classList.add(`style-${style}`, `base-color-${baseColor}`)
// Update font.
// Always set --font-sans for the preview so the selected font is visible.
// The font type (sans/serif/mono) is metadata for the CLI updater.
const selectedFont = FONTS.find((f) => f.value === font)
if (selectedFont) {
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])
const registryTheme = React.useMemo(() => {
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
@@ -184,13 +94,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(() => {
@@ -198,83 +107,69 @@ export function DesignSystemProvider({
return
}
const styleId = "design-system-theme-vars"
let styleElement = document.getElementById(
THEME_STYLE_ELEMENT_ID
styleId
) as HTMLStyleElement | null
if (!styleElement) {
styleElement = document.createElement("style")
styleElement.id = THEME_STYLE_ELEMENT_ID
styleElement.id = styleId
document.head.appendChild(styleElement)
}
styleElement.textContent = buildThemeCssText(registryTheme.cssVars)
const {
light: lightVars,
dark: darkVars,
theme: themeVars,
} = registryTheme.cssVars
let cssText = ":root {\n"
// Add theme vars (shared across light/dark).
if (themeVars) {
Object.entries(themeVars).forEach(([key, value]) => {
if (value) {
cssText += ` --${key}: ${value};\n`
}
})
}
// Add light mode vars.
if (lightVars) {
Object.entries(lightVars).forEach(([key, value]) => {
if (value) {
cssText += ` --${key}: ${value};\n`
}
})
}
cssText += "}\n\n"
cssText += ".dark {\n"
if (darkVars) {
Object.entries(darkVars).forEach(([key, value]) => {
if (value) {
cssText += ` --${key}: ${value};\n`
}
})
}
cssText += "}\n"
styleElement.textContent = cssText
}, [registryTheme])
// Handle menu color inversion by adding/removing dark class to elements with cn-menu-target.
// useLayoutEffect to apply classes synchronously before paint, avoiding flash.
React.useLayoutEffect(() => {
React.useEffect(() => {
if (!menuColor) {
return
}
const isInvertedMenu =
menuColor === "inverted" || menuColor === "inverted-translucent"
const isTranslucentMenu =
menuColor === "default-translucent" ||
menuColor === "inverted-translucent"
let frameId = 0
const updateMenuElements = () => {
const allElements = document.querySelectorAll<HTMLElement>(
".cn-menu-target, [data-menu-translucent]"
)
if (allElements.length === 0) {
return
}
// Disable transitions while toggling classes.
allElements.forEach((element) => {
element.style.transition = "none"
})
allElements.forEach((element) => {
if (element.classList.contains("cn-menu-target")) {
if (isInvertedMenu) {
element.classList.add("dark")
} else {
element.classList.remove("dark")
}
const menuElements = document.querySelectorAll(".cn-menu-target")
menuElements.forEach((element) => {
if (menuColor === "inverted") {
element.classList.add("dark")
} else {
element.classList.remove("dark")
}
// When translucent is enabled, move from data-attr to class so styles apply.
// When disabled, move back to a data-attr so the element stays queryable
// for future toggles without losing its identity as a menu element.
if (isTranslucentMenu) {
element.classList.add("cn-menu-translucent")
element.removeAttribute("data-menu-translucent")
} else if (element.classList.contains("cn-menu-translucent")) {
element.classList.remove("cn-menu-translucent")
element.setAttribute("data-menu-translucent", "")
}
})
// Force a reflow, then re-enable transitions.
void document.body.offsetHeight
allElements.forEach((element) => {
element.style.transition = ""
})
}
const scheduleMenuUpdate = () => {
if (frameId) {
return
}
frameId = window.requestAnimationFrame(() => {
frameId = 0
updateMenuElements()
})
}
@@ -283,7 +178,7 @@ export function DesignSystemProvider({
// Watch for new menu elements being added to the DOM.
const observer = new MutationObserver(() => {
scheduleMenuUpdate()
updateMenuElements()
})
observer.observe(document.body, {
@@ -293,9 +188,6 @@ export function DesignSystemProvider({
return () => {
observer.disconnect()
if (frameId) {
window.cancelAnimationFrame(frameId)
}
}
}, [menuColor])

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,8 +1,6 @@
"use client"
import * as React from "react"
import { Menu02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useTheme } from "next-themes"
import { useMounted } from "@/hooks/use-mounted"
@@ -12,37 +10,99 @@ import {
Picker,
PickerContent,
PickerGroup,
PickerLabel,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerTrigger,
} from "@/app/(create)/components/picker"
import {
isTranslucentMenuColor,
useDesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
type ColorChoice = "default" | "inverted"
type SurfaceChoice = "solid" | "translucent"
function getMenuColorValue(
color: ColorChoice,
translucent: boolean
): MenuColorValue {
if (color === "default") {
return translucent ? "default-translucent" : "default"
}
return translucent ? "inverted-translucent" : "inverted"
}
const MENU_OPTIONS: { value: MenuColorValue; label: string }[] = [
{ value: "default", label: "Default / Solid" },
{ value: "default-translucent", label: "Default / Translucent" },
{ value: "inverted", label: "Inverted / Solid" },
{ value: "inverted-translucent", label: "Inverted / Translucent" },
]
const MENU_OPTIONS = [
{
value: "default" as const,
label: "Default",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
role="img"
stroke="currentColor"
className="size-4 text-foreground"
>
<path
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M8.5 11.5L14.5001 11.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.5 15H13.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 8H15.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
{
value: "inverted" as const,
label: "Inverted",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
viewBox="0 0 24 24"
fill="none"
role="img"
className="size-4 fill-background"
>
<path
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M8.5 11.5L14.5001 11.5"
stroke="var(--foreground)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.5 15H13.5"
stroke="var(--foreground)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 8H15.5"
stroke="var(--foreground)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
},
] as const
export function MenuColorPicker({
isMobile,
@@ -51,69 +111,25 @@ export function MenuColorPicker({
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const { resolvedTheme } = useTheme()
const mounted = useMounted()
const lastSolidMenuAccentRef = React.useRef(params.menuAccent)
const isDark = mounted && resolvedTheme === "dark"
const [params, setParams] = useDesignSystemSearchParams()
const currentMenu = MENU_OPTIONS.find(
(menu) => menu.value === params.menuColor
)
const colorChoice: ColorChoice =
params.menuColor === "inverted" ||
params.menuColor === "inverted-translucent"
? "inverted"
: "default"
const surfaceChoice: SurfaceChoice =
params.menuColor === "default-translucent" ||
params.menuColor === "inverted-translucent"
? "translucent"
: "solid"
React.useEffect(() => {
if (surfaceChoice === "solid") {
lastSolidMenuAccentRef.current = params.menuAccent
}
}, [params.menuAccent, surfaceChoice])
const setColor = (color: ColorChoice) => {
const nextMenuColor = getMenuColorValue(
color,
surfaceChoice === "translucent"
)
setParams({
menuColor: nextMenuColor,
...(isTranslucentMenuColor(nextMenuColor) && { menuAccent: "subtle" }),
})
}
const setSurface = (choice: SurfaceChoice) => {
const isTranslucent = choice === "translucent"
const nextMenuColor = getMenuColorValue(colorChoice, isTranslucent)
setParams({
menuColor: nextMenuColor,
menuAccent: isTranslucent ? "subtle" : lastSolidMenuAccentRef.current,
})
}
return (
<div className="group/picker relative">
<Picker>
<PickerTrigger>
<PickerTrigger disabled={mounted && resolvedTheme === "dark"}>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Menu</div>
<div className="line-clamp-1 text-sm font-medium text-foreground">
<div className="text-xs text-muted-foreground">Menu Color</div>
<div className="text-sm font-medium text-foreground">
{currentMenu?.label}
</div>
</div>
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
<HugeiconsIcon
icon={Menu02Icon}
strokeWidth={2}
className="size-4"
/>
{currentMenu?.icon}
</div>
</PickerTrigger>
<PickerContent
@@ -121,43 +137,24 @@ export function MenuColorPicker({
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerGroup>
<PickerLabel>Color</PickerLabel>
<PickerRadioGroup
value={colorChoice}
onValueChange={(value) => {
setColor(value as ColorChoice)
}}
>
<PickerRadioItem value="default" closeOnClick={isMobile}>
Default
</PickerRadioItem>
<PickerRadioItem
value="inverted"
closeOnClick={isMobile}
disabled={isDark}
>
Inverted
</PickerRadioItem>
</PickerRadioGroup>
</PickerGroup>
<PickerSeparator />
<PickerGroup>
<PickerLabel>Appearance</PickerLabel>
<PickerRadioGroup
value={surfaceChoice}
onValueChange={(value) => {
setSurface(value as SurfaceChoice)
}}
>
<PickerRadioItem value="solid" closeOnClick={isMobile}>
Solid
</PickerRadioItem>
<PickerRadioItem value="translucent" closeOnClick={isMobile}>
Translucent
</PickerRadioItem>
</PickerRadioGroup>
</PickerGroup>
<PickerRadioGroup
value={currentMenu?.value}
onValueChange={(value) => {
setParams({ menuColor: value as MenuColorValue })
}}
>
<PickerGroup>
{MENU_OPTIONS.map((menu) => (
<PickerRadioItem
key={menu.value}
value={menu.value}
closeOnClick={isMobile}
>
{menu.label}
</PickerRadioItem>
))}
</PickerGroup>
</PickerRadioGroup>
</PickerContent>
</Picker>
<LockButton

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

@@ -35,7 +35,6 @@ 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 { 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 border-0 bg-neutral-800 p-6 text-foreground shadow-xl ring-1 ring-neutral-950/80 backdrop-blur-xl [--border:var(--color-neutral-700)]! 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 bg-neutral-950/20 ring-1 ring-neutral-950/80 dark:bg-neutral-900/50 dark:ring-neutral-700/50"
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 border-neutral-700/50 bg-neutral-900/50 px-3 py-3 text-neutral-100">
<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,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"
@@ -19,10 +18,7 @@ import {
RANDOMIZE_BIASES,
type RandomizeContext,
} from "@/app/(create)/lib/randomize-biases"
import {
isTranslucentMenuColor,
useDesignSystemSearchParams,
} from "@/app/(create)/lib/search-params"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
function randomItem<T>(array: readonly T[]): T {
return array[Math.floor(Math.random() * array.length)]
@@ -63,77 +59,32 @@ export function useRandom() {
const selectedTheme = locks.has("theme")
? paramsRef.current.theme
: randomItem(availableThemes).name
context.theme = selectedTheme
const availableChartColors = applyBias(
getThemesForBaseColor(baseColor),
context,
RANDOMIZE_BIASES.chartColors
)
const selectedChartColor = locks.has("chartColor")
? paramsRef.current.chartColor
: randomItem(availableChartColors).name
context.chartColor = selectedChartColor
const selectedFont = locks.has("font")
? paramsRef.current.font
: randomItem(availableFonts).value
context.font = selectedFont
// Pick heading font: ~70% inherit, ~30% distinct with cross-category contrast.
let selectedFontHeading: FontHeadingValue
if (locks.has("fontHeading")) {
selectedFontHeading = paramsRef.current.fontHeading
} else if (Math.random() < 0.7) {
selectedFontHeading = "inherit"
} else {
const bodyType = availableFonts.find(
(f) => f.value === selectedFont
)?.type
const contrastFonts = availableFonts.filter(
(f) => f.type !== bodyType && f.value !== selectedFont
)
selectedFontHeading = (
contrastFonts.length > 0
? randomItem(contrastFonts)
: randomItem(availableFonts)
).value as FontHeadingValue
}
const selectedRadius = locks.has("radius")
? paramsRef.current.radius
: randomItem(availableRadii).name
const selectedIconLibrary = locks.has("iconLibrary")
? paramsRef.current.iconLibrary
: randomItem(Object.values(iconLibraries)).name
const lockedMenuAccent = locks.has("menuAccent")
const selectedMenuAccent = locks.has("menuAccent")
? paramsRef.current.menuAccent
: undefined
const availableMenuColors =
!locks.has("menuColor") && lockedMenuAccent === "bold"
? MENU_COLORS.filter((menuColor) => {
return !isTranslucentMenuColor(menuColor.value)
})
: MENU_COLORS
: randomItem(MENU_ACCENTS).value
const selectedMenuColor = locks.has("menuColor")
? paramsRef.current.menuColor
: randomItem(availableMenuColors).value
const selectedMenuAccent =
locks.has("menuAccent") || isTranslucentMenuColor(selectedMenuColor)
? paramsRef.current.menuAccent === "bold" &&
isTranslucentMenuColor(selectedMenuColor)
? "subtle"
: paramsRef.current.menuAccent
: randomItem(MENU_ACCENTS).value
: randomItem(MENU_COLORS).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,

View File

@@ -24,10 +24,8 @@ 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,

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,9 @@
import { NextResponse, type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import { isPresetCode } from "shadcn/preset"
import { registryItemSchema } from "shadcn/schema"
import { buildRegistryBase } from "@/registry/config"
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
import { getPresetCode } from "@/app/(create)/lib/preset-code"
export async function GET(request: NextRequest) {
try {
@@ -16,12 +14,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: result.error }, { status: 400 })
}
const rawPreset = searchParams.get("preset")
const presetCode =
rawPreset && isPresetCode(rawPreset)
? rawPreset
: getPresetCode(result.data)
const registryBase = buildRegistryBase(result.data)
const parseResult = registryItemSchema.safeParse(registryBase)
@@ -35,12 +27,10 @@ export async function GET(request: NextRequest) {
)
}
if (searchParams.get("track") === "1") {
track("create_app", {
...result.data,
preset: presetCode,
})
}
track("create_app", {
...result.data,
preset: searchParams.get("preset") ?? "",
})
return NextResponse.json(parseResult.data)
} catch (error) {

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 = [
@@ -142,111 +118,26 @@ export type DesignSystemSearchParams = inferParserType<
typeof designSystemSearchParams
>
export function isTranslucentMenuColor(
menuColor?: MenuColorValue | null
): menuColor is "default-translucent" | "inverted-translucent" {
return (
menuColor === "default-translucent" || menuColor === "inverted-translucent"
)
}
function normalizePartialDesignSystemParams(
params: Partial<DesignSystemSearchParams>
): Partial<DesignSystemSearchParams> {
if (
params.menuAccent === "bold" &&
isTranslucentMenuColor(params.menuColor ?? undefined)
) {
return {
...params,
menuAccent: "subtle",
}
}
return params
}
function normalizeDesignSystemParams(
params: DesignSystemSearchParams
): DesignSystemSearchParams {
let result = {
...params,
fontHeading: normalizeFontHeading(params.font, params.fontHeading),
}
// Validate theme and chartColor against baseColor.
if (result.baseColor) {
const available = getThemesForBaseColor(result.baseColor)
const themeValid = available.some((t) => t.name === result.theme)
const chartColorValid = available.some((t) => t.name === result.chartColor)
if (!themeValid || !chartColorValid) {
const fallback = (available[0]?.name ?? result.baseColor) as ThemeName
result = {
...result,
...(!themeValid && { theme: fallback }),
...(!chartColorValid && { chartColor: fallback as ChartColorName }),
}
}
}
if (
result.menuAccent === "bold" &&
isTranslucentMenuColor(result.menuColor)
) {
return {
...result,
menuAccent: "subtle",
}
}
return result
}
// If preset param exists, decode it and overlay on raw params.
// V1 presets don't encode chartColor — fall back to the colored
// theme that base-color themes originally borrowed charts from.
type SearchParamsLike = Pick<URLSearchParams, "get" | "has">
function resolvePresetParams(
rawParams: DesignSystemSearchParams,
searchParams: SearchParamsLike
) {
if (rawParams.preset && isPresetCode(rawParams.preset)) {
const decoded = decodePreset(rawParams.preset)
if (decoded) {
const presetOverrides = resolvePresetOverrides(searchParams, decoded)
return normalizeDesignSystemParams({
...decoded,
...presetOverrides,
base: rawParams.base,
item: rawParams.item,
preset: rawParams.preset,
template: rawParams.template,
rtl: rawParams.rtl,
size: rawParams.size,
custom: rawParams.custom,
})
}
}
return normalizeDesignSystemParams(rawParams)
}
// Wraps nuqs useQueryStates with transparent preset encoding/decoding.
// - Reads: if ?preset=CODE is in the URL, decodes it and returns individual values.
// - Writes: when design system params are set, encodes them into a preset code.
export function useDesignSystemSearchParams(options: Options = {}) {
const searchParams = useSearchParams()
const [rawParams, rawSetParams] = useQueryStates(designSystemSearchParams, {
shallow: false,
history: "push",
...options,
})
const params = React.useMemo(
() => resolvePresetParams(rawParams, searchParams),
[rawParams, searchParams]
)
// If preset param exists, decode it and overlay on raw params.
const params = React.useMemo(() => {
if (rawParams.preset && isPresetCode(rawParams.preset)) {
const decoded = decodePreset(rawParams.preset)
if (decoded) {
return { ...rawParams, ...decoded }
}
}
return rawParams
}, [rawParams])
// Use ref so setParams callback stays stable across renders.
const paramsRef = React.useRef(params)
@@ -265,9 +156,8 @@ export function useDesignSystemSearchParams(options: Options = {}) {
) => Partial<DesignSystemSearchParams>),
setOptions?: Options
) => {
const resolvedUpdates = normalizePartialDesignSystemParams(
const resolvedUpdates =
typeof updates === "function" ? updates(paramsRef.current) : updates
)
const hasDesignSystemUpdate = DESIGN_SYSTEM_KEYS.some(
(key) => key in resolvedUpdates
@@ -279,14 +169,22 @@ export function useDesignSystemSearchParams(options: Options = {}) {
}
// Merge current decoded values with updates.
const merged = normalizeDesignSystemParams({
...paramsRef.current,
...resolvedUpdates,
})
const merged = { ...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

@@ -28,7 +28,7 @@ export async function StarsCount() {
const formattedCount =
json.stargazers_count >= 1000
? `${Math.round(json.stargazers_count / 1000)}k`
: json.stargazers_count?.toLocaleString()
: json.stargazers_count.toLocaleString()
return (
<span className="w-fit text-xs text-muted-foreground tabular-nums">

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

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

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

View File

@@ -13,7 +13,7 @@ npx shadcn@latest migrate radix
It will automatically update all imports in your `ui` components and install `radix-ui` as a dependency.
```diff showLineNumbers title="components/ui/alert-dialog.tsx"
- import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
- import * as AlertDialogPrimitive from "@radix-ui/react-dialog"
+ import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
```

View File

@@ -91,7 +91,7 @@ Need to keep your components private? We've got you covered. Configure authentic
Your private components stay private. Perfect for enterprise teams with proprietary UI libraries.
We support all major authentication methods: basic auth, bearer token, API key query params and custom headers.
We support all major authentication methods: basic auth, bearer token, api key query params and custom headers.
See the [authentication docs](/docs/registry/authentication) for more details.
@@ -125,7 +125,7 @@ Preview components before installing them. Search across multiple registries. Se
src="/images/mcp.jpeg"
width="1432"
height="1050"
alt="MCP Server"
alt="Lift Mode"
className="mt-6 w-full overflow-hidden rounded-lg border"
/>
@@ -175,7 +175,7 @@ Missing environment variables? The CLI tells you exactly what's needed:
Registry "@private" requires the following environment variables:
• REGISTRY_TOKEN
Set the required environment variables in your .env or .env.local file.
Set the required environment variables to your .env or .env.local file.
```
Registry authors can provide custom error messages in their responses to help users and AI agents understand and fix issues quickly.

View File

@@ -6,7 +6,7 @@ date: 2025-09-02
We've created an index of open source registries that you can install items from.
You can search, view and add items from the registry index without configuring the `components.json` file.
You can search, view and add items from the registry index without configuring the `.components.json` file.
They'll be automatically added to your `components.json` file for you.

View File

@@ -173,7 +173,7 @@ You can also add buttons to the input group.
className="[&_.preview]:h-[300px] [&_pre]:h-[300px]!"
/>
Or text, labels, tooltips, ...
Or text, labels, tooltips,...
<ComponentPreview
name="input-group-text"
@@ -266,7 +266,7 @@ between vertical and horizontal layouts based on container width. Done.
className="[&_.preview]:h-[600px] [&_pre]:h-[600px]!"
/>
Wait, here's more. Wrap your fields in `FieldLabel` to create a selectable field group. Really easy. And it looks great.
Wait here's more. Wrap your fields in `FieldLabel` to create a selectable field group. Really easy. And it looks great.
<ComponentPreview
name="field-choice-card"

View File

@@ -14,7 +14,7 @@ Today, we're changing that: **npx shadcn create**.
Customize Everything. Pick your component library, icons, base color, theme, fonts and create your own version of shadcn/ui.
We're starting with **5 new visual styles**, designed to help your UI actually feel like _your_ UI.
We're starting with **5 new visual styles,** designed to help your UI actually feel like _your_ UI.
- **Vega** The classic shadcn/ui look.
- **Nova** Reduced padding and margins for compact layouts.

View File

@@ -6,7 +6,7 @@ date: 2026-03-06
We're releasing version 4 of shadcn/cli. More capable, easier to use. Built for you and your coding agents. Here's everything new.
## shadcn/skills
### shadcn/skills
shadcn/skills gives coding agents the context they need to work with your components and registry correctly. It covers both Radix and Base UI primitives, updated APIs, component patterns and registry workflows. The skill also knows how to use the shadcn CLI, when to invoke it and which flags to pass. Agents make fewer mistakes and produce code that actually matches your design system.
@@ -20,7 +20,7 @@ You can ask your agent things like:
npx skills add shadcn/ui
```
## Introducing --preset
### Introducing --preset
A preset packs your entire design system config into a short code. Colors, theme, icon library, fonts, radius. One string, everything included.
@@ -32,26 +32,26 @@ npx shadcn@latest init --preset a1Dg5eFl
Use it to scaffold projects from custom config, share with your team or publish in your registry. Drop it in prompts so your agent knows where to start. Use it across Claude, Codex, v0, Replit. Take your preset with you.
## Switching presets
### Switching presets
When you're working on a new app, it can take a few tries to find something you like so we've made switching presets really easy. Run `init --preset` in your app, and the CLI will take care of reconfiguring everything, including your components.
When you're working on a new app, it can take a few tries to find something you like so we've made switching presets really easy. Run init --preset in your app, and the cli will take care of reconfiguring everything including your components.
```bash
npx shadcn@latest init --preset ad3qkJ7
```
## Skills + Presets
### Skills + Presets
Your agent knows how to use presets. Scaffold a project, switch design systems, try something new.
- "create a new next app using --preset adtk27v"
- "let's try --preset adtk27v"
## New shadcn/create
### New shadcn/create
To help you build custom presets, we rebuilt [shadcn/create](/create). It now includes a library of UI components you can use to preview your presets. See how your colors, fonts and radius apply to real components before you start building.
## New --dry-run, --diff, and --view flags
### New --dry-run, --diff, and --view flags
Inspect what a registry will add to your project before anything gets written. Review the payload yourself or pipe it to your coding agent for a second look.
@@ -61,7 +61,7 @@ npx shadcn@latest add button --diff
npx shadcn@latest add button --view
```
## Updating primitives
### Updating primitives
You can use the `--diff` flag to check for registry updates. Or ask your agent: "check for updates from @shadcn and merge with my local changes".
@@ -69,7 +69,7 @@ You can use the `--diff` flag to check for registry updates. Or ask your agent:
npx shadcn@latest add button --diff
```
## shadcn init --template
### shadcn init --template
`shadcn init` now scaffolds full project templates for Next.js, Vite, Laravel, React Router, Astro and TanStack Start. Dark mode included for Next.js and Vite.
@@ -91,7 +91,7 @@ Use `--monorepo` to set up a monorepo.
npx shadcn@latest init -t next --monorepo
```
## shadcn init --base
### shadcn init --base
Pick your primitives. Use `--base` to start a project with Radix or Base UI.
@@ -99,7 +99,7 @@ Pick your primitives. Use `--base` to start a project with Radix or Base UI.
npx shadcn@latest init --base radix
```
## shadcn info
### shadcn info
The `info` command now shows the full picture: framework, version, CSS vars, which components are installed, and where to find docs and examples for every component. Great for giving coding agents the context they need to work with your project.
@@ -107,7 +107,7 @@ The `info` command now shows the full picture: framework, version, CSS vars, whi
npx shadcn@latest info
```
## shadcn docs
### shadcn docs
Get docs, code and examples for any UI component right from the CLI. Gives your coding agent the context to use your primitives correctly.
@@ -120,7 +120,7 @@ combobox
- api https://base-ui.com/react/components/combobox
```
## registry:base and registry:font
### registry:base and registry:font
Registries can now distribute an entire design system as a single payload using `registry:base`. Components, dependencies, CSS vars, fonts, config. One install, everything set up.

View File

@@ -161,7 +161,7 @@ To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl)
### size
Use the `size` prop on the `AlertDialogContent` component to control the size of the alert dialog. It accepts the following values:
Use the `size` props on the `AlertDialogContent` component to control the size of the alert dialog. It accepts the following values:
| Prop | Type | Default |
| ------ | ------------------- | ----------- |

View File

@@ -135,7 +135,7 @@ To create a button group, use the `ButtonGroup` component. See the [Button Group
<ComponentPreview styleName="base-nova" name="button-group-demo" />
### As Link
### Link
You can use the `buttonVariants` helper to make a link look like a button.

View File

@@ -182,7 +182,7 @@ You can pass options to the carousel using the `opts` prop. See the [Embla Carou
## API
Use a state and the `setApi` prop to get an instance of the carousel API.
Use a state and the `setApi` props to get an instance of the carousel API.
<ComponentPreview
styleName="base-nova"
@@ -319,4 +319,4 @@ The `direction` option accepts `"ltr"` or `"rtl"` and should match the `dir` pro
## API Reference
See the [Embla Carousel docs](https://www.embla-carousel.com/api/) for more information on props and plugins.
See the [Embla Carousel docs](https://www.embla-carousel.com/api/plugins/) for more information on props and plugins.

View File

@@ -384,7 +384,7 @@ Charts have built-in support for theming. You can use css variables (recommended
--chart-2: oklch(0.6 0.118 184.704);
}
.dark {
.dark: {
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
}

View File

@@ -222,7 +222,7 @@ Use `ComboboxGroup` and `ComboboxSeparator` to group items.
### Custom Items
You can render a custom component inside `ComboboxItem`.
You can render custom component inside `ComboboxItem`.
<ComponentPreview styleName="base-nova" name="combobox-custom" />
@@ -240,7 +240,7 @@ Use the `disabled` prop to disable the combobox.
### Auto Highlight
Use the `autoHighlight` prop to automatically highlight the first item on filter.
Use the `autoHighlight` prop automatically highlight the first item on filter.
<ComponentPreview styleName="base-nova" name="combobox-auto-highlight" />

View File

@@ -316,13 +316,13 @@ You can use the same approach to format other cells and headers.
## Row Actions
Let's add row actions to our table. We'll use a `<DropdownMenu />` component for this.
Let's add row actions to our table. We'll use a `<Dropdown />` component for this.
<Steps className="mb-0 pt-2">
### Update columns definition
Update our columns definition to add a new `actions` column. The `actions` cell returns a `<DropdownMenu />` component.
Update our columns definition to add a new `actions` column. The `actions` cell returns a `<Dropdown />` component.
```tsx showLineNumbers title="app/payments/columns.tsx" {4,6-14,18-45}
"use client"
@@ -472,7 +472,7 @@ Let's make the email column sortable.
### Update `<DataTable>`
```tsx showLineNumbers title="app/payments/data-table.tsx" {3,6,10,18,25-29}
```tsx showLineNumbers title="app/payments/data-table.tsx" showLineNumbers {3,6,10,18,25-29}
"use client"
import * as React from "react"

View File

@@ -82,8 +82,8 @@ import {
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuSeparator />
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>

View File

@@ -1,6 +1,6 @@
---
title: Input OTP
description: Accessible one-time password component with copy-paste functionality.
description: Accessible one-time password component with copy paste functionality.
base: base
component: true
links:

View File

@@ -59,7 +59,7 @@ npm install @base-ui/react
## Usage
```tsx showLineNumbers
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
import { ScrollArea } from "@/components/ui/scroll-area"
```
```tsx showLineNumbers

View File

@@ -31,7 +31,7 @@ Sonner is built and maintained by [emilkowalski](https://twitter.com/emilkowalsk
npx shadcn@latest add sonner
```
<Step>Add the Toaster component.</Step>
<Step>Add the Toaster component</Step>
```tsx title="app/layout.tsx" {1,9} showLineNumbers
import { Toaster } from "@/components/ui/sonner"
@@ -71,7 +71,7 @@ npm install sonner next-themes
styleName="base-nova"
/>
<Step>Add the Toaster component.</Step>
<Step>Add the Toaster component</Step>
```tsx showLineNumbers title="app/layout.tsx" {1,8}
import { Toaster } from "@/components/ui/sonner"

View File

@@ -88,13 +88,13 @@ Use the `size-*` utility class to change the size of the spinner.
### Button
Add a spinner to a button to indicate a loading state. Place the `<Spinner />` before the label with `data-icon="inline-start"` for a start position, or after the label with `data-icon="inline-end"` for an end position.
Add a spinner to a button to indicate a loading state. Remember to use the `data-icon="inline-start"` prop to add the spinner to the start of the button and the `data-icon="inline-end"` prop to add the spinner to the end of the button.
<ComponentPreview styleName="base-nova" name="spinner-button" />
### Badge
Add a spinner to a badge to indicate a loading state. Place the `<Spinner />` before the label with `data-icon="inline-start"` for a start position, or after the label with `data-icon="inline-end"` for an end position.
Add a spinner to a badge to indicate a loading state. Remember to use the `data-icon="inline-start"` prop to add the spinner to the start of the badge and the `data-icon="inline-end"` prop to add the spinner to the end of the badge.
<ComponentPreview styleName="base-nova" name="spinner-badge" />

View File

@@ -1,6 +1,6 @@
---
title: Typography
description: Styles for headings, paragraphs, lists, etc.
description: Styles for headings, paragraphs, lists...etc
base: base
component: true
---

View File

@@ -161,7 +161,7 @@ To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl)
### size
Use the `size` prop on the `AlertDialogContent` component to control the size of the alert dialog. It accepts the following values:
Use the `size` props on the `AlertDialogContent` component to control the size of the alert dialog. It accepts the following values:
| Prop | Type | Default |
| ------ | ------------------- | ----------- |

View File

@@ -135,7 +135,7 @@ To create a button group, use the `ButtonGroup` component. See the [Button Group
<ComponentPreview styleName="radix-nova" name="button-group-demo" />
### As Child
### Link
You can use the `asChild` prop on `<Button />` to make another component look like a button. Here's an example of a link that looks like a button.

View File

@@ -319,4 +319,4 @@ The `direction` option accepts `"ltr"` or `"rtl"` and should match the `dir` pro
## API Reference
See the [Embla Carousel docs](https://www.embla-carousel.com/api/) for more information on props and plugins.
See the [Embla Carousel docs](https://www.embla-carousel.com/api/plugins/) for more information on props and plugins.

View File

@@ -384,7 +384,7 @@ Charts have built-in support for theming. You can use css variables (recommended
--chart-2: oklch(0.6 0.118 184.704);
}
.dark {
.dark: {
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
}

View File

@@ -222,7 +222,7 @@ Use `ComboboxGroup` and `ComboboxSeparator` to group items.
### Custom Items
You can render a custom component inside `ComboboxItem`.
You can render custom component inside `ComboboxItem`.
<ComponentPreview styleName="base-nova" name="combobox-custom" />
@@ -240,7 +240,7 @@ Use the `disabled` prop to disable the combobox.
### Auto Highlight
Use the `autoHighlight` prop to automatically highlight the first item on filter.
Use the `autoHighlight` prop automatically highlight the first item on filter.
<ComponentPreview styleName="base-nova" name="combobox-auto-highlight" />

View File

@@ -316,13 +316,13 @@ You can use the same approach to format other cells and headers.
## Row Actions
Let's add row actions to our table. We'll use a `<DropdownMenu />` component for this.
Let's add row actions to our table. We'll use a `<Dropdown />` component for this.
<Steps className="mb-0 pt-2">
### Update columns definition
Update our columns definition to add a new `actions` column. The `actions` cell returns a `<DropdownMenu />` component.
Update our columns definition to add a new `actions` column. The `actions` cell returns a `<Dropdown />` component.
```tsx showLineNumbers title="app/payments/columns.tsx" {4,6-14,18-45}
"use client"
@@ -472,7 +472,7 @@ Let's make the email column sortable.
### Update `<DataTable>`
```tsx showLineNumbers title="app/payments/data-table.tsx" {3,6,10,18,25-28}
```tsx showLineNumbers title="app/payments/data-table.tsx" showLineNumbers {3,6,10,18,25-28}
"use client"
import * as React from "react"

View File

@@ -83,8 +83,8 @@ import {
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
</DropdownMenuGroup>

View File

@@ -1,6 +1,6 @@
---
title: Input OTP
description: Accessible one-time password component with copy-paste functionality.
description: Accessible one-time password component with copy paste functionality.
base: radix
component: true
links:

View File

@@ -59,7 +59,7 @@ npm install radix-ui
## Usage
```tsx showLineNumbers
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
import { ScrollArea } from "@/components/ui/scroll-area"
```
```tsx showLineNumbers

View File

@@ -88,13 +88,13 @@ Use the `size-*` utility class to change the size of the spinner.
### Button
Add a spinner to a button to indicate a loading state. Place the `<Spinner />` before the label with `data-icon="inline-start"` for a start position, or after the label with `data-icon="inline-end"` for an end position.
Add a spinner to a button to indicate a loading state. Remember to use the `data-icon="inline-start"` prop to add the spinner to the start of the button and the `data-icon="inline-end"` prop to add the spinner to the end of the button.
<ComponentPreview styleName="radix-nova" name="spinner-button" />
### Badge
Add a spinner to a badge to indicate a loading state. Place the `<Spinner />` before the label with `data-icon="inline-start"` for a start position, or after the label with `data-icon="inline-end"` for an end position.
Add a spinner to a badge to indicate a loading state. Remember to use the `data-icon="inline-start"` prop to add the spinner to the start of the badge and the `data-icon="inline-end"` prop to add the spinner to the end of the badge.
<ComponentPreview styleName="radix-nova" name="spinner-badge" />

View File

@@ -1,6 +1,6 @@
---
title: Typography
description: Styles for headings, paragraphs, lists, etc.
description: Styles for headings, paragraphs, lists...etc
base: radix
component: true
---

View File

@@ -1,6 +1,6 @@
---
title: Next.js
description: Adding dark mode to your Next.js app.
description: Adding dark mode to your next app.
---
<Steps>

View File

@@ -1,6 +1,6 @@
---
title: Remix
description: Adding dark mode to your Remix app.
description: Adding dark mode to your remix app.
---
<Steps>
@@ -103,7 +103,7 @@ export function App() {
## Add an action route
Create a file in `/routes/action.set-theme.ts`. Ensure that you pass the filename to the ThemeProvider component. This route is used to store the preferred theme in the session storage when the user changes it.
Create a file in `/routes/action.set-theme.ts`. Ensure that you pass the filename to the ThemeProvider component. This route it's used to store the preferred theme in the session storage when the user changes it.
```tsx title="app/routes/action.set-theme.ts" showLineNumbers
import { createThemeAction } from "remix-themes"

View File

@@ -1,6 +1,6 @@
---
title: Vite
description: Adding dark mode to your Vite app.
description: Adding dark mode to your vite app.
---
## Create a theme provider

View File

@@ -28,7 +28,7 @@ This form leverages Next.js and React's built-in capabilities for form handling.
- Uses Next.js `<Form />` component for navigation and progressive enhancement.
- `<Field />` components for building accessible forms.
- `useActionState` for managing form state and errors.
- Handles loading states with the pending prop.
- Handles loading states with pending prop.
- Server Actions for handling form submissions.
- Server-side validation using Zod.

View File

@@ -66,7 +66,7 @@ Here's a basic example of a form using the `<Controller />` component from React
### Create a form schema
We'll start by defining the shape of our form using a Zod schema.
We'll start by defining the shape of our form using a Zod schema
<Callout icon={<InfoIcon />}>
**Note:** This example uses `zod v3` for schema validation, but you can
@@ -89,7 +89,7 @@ const formSchema = z.object({
})
```
### Set up the form
### Setup the form
Next, we'll use the `useForm` hook from React Hook Form to create our form instance. We'll also add the Zod resolver to validate the form data.

View File

@@ -106,7 +106,7 @@ const formSchema = z.object({
})
```
### Set up the form
### Setup the form
Use the `useForm` hook from TanStack Form to create your form instance with Zod validation.

View File

@@ -1,6 +1,6 @@
---
title: Astro
description: Install and configure shadcn/ui for Astro.
description: Install and configure shadcn/ui for Astro
---
<Callout className="mb-6 border-emerald-600 bg-emerald-100 dark:border-emerald-400 dark:bg-emerald-900">
@@ -46,7 +46,7 @@ import { Button } from "@/components/ui/button"
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Astro + Tailwind CSS</title>
<title>Astro + TailwindCSS</title>
</head>
<body>

View File

@@ -1,6 +1,6 @@
---
title: Gatsby
description: Install and configure shadcn/ui for Gatsby.
description: Install and configure Gatsby.
---
<Callout className="mb-6 border-blue-600 bg-blue-50 dark:border-blue-900 dark:bg-blue-950 [&_code]:bg-blue-100 dark:[&_code]:bg-blue-900">
@@ -78,7 +78,7 @@ export const onCreateWebpackConfig = ({ actions }) => {
### Run the CLI
Run the `shadcn` init command to set up your project:
Run the `shadcn` init command to setup your project:
```bash
npx shadcn@latest init

View File

@@ -1,6 +1,6 @@
---
title: Laravel
description: Install and configure shadcn/ui for Laravel.
description: Install and configure shadcn/ui for Laravel
---
<Callout className="mb-6 border-emerald-600 bg-emerald-100 dark:border-emerald-400 dark:bg-emerald-900">
@@ -13,7 +13,7 @@ description: Install and configure shadcn/ui for Laravel.
### Create Project
Start by creating a new Laravel project with Inertia and React using the Laravel installer:
Start by creating a new Laravel project with Inertia and React using the laravel installer:
```bash
laravel new my-app --react

View File

@@ -13,7 +13,7 @@ description: Install and configure shadcn/ui for Next.js.
### Create Project
Run the `init` command to create a new Next.js project or to set up an existing one:
Run the `init` command to create a new Next.js project or to setup an existing one:
```bash
npx shadcn@latest init -t next

View File

@@ -21,7 +21,7 @@ npx create-remix@latest my-app
### Run the CLI
Run the `shadcn` init command to set up your project:
Run the `shadcn` init command to setup your project:
```bash
npx shadcn@latest init

View File

@@ -16,13 +16,13 @@ description: Install and configure shadcn/ui for TanStack Start.
Run the following command to create a new TanStack Start project with shadcn/ui:
```bash
npx shadcn@latest init -t start
npx shadcn@latest init -t tanstack
```
**For a monorepo project, use `--monorepo` flag:**
```bash
npx shadcn@latest init -t start --monorepo
npx shadcn@latest init -t tanstack --monorepo
```
### Add Components

View File

@@ -13,7 +13,7 @@ description: Install and configure shadcn/ui for Vite.
### Create Project
Run the `init` command to create a new Vite project or to set up an existing one:
Run the `init` command to create a new Vite project or to setup an existing one:
```bash
npx shadcn@latest init -t vite
@@ -38,7 +38,7 @@ The command above will add the `Button` component to your project. You can then
```tsx {1,6} showLineNumbers title="src/App.tsx"
import { Button } from "@/components/ui/button"
export default function App() {
export default function Home() {
return (
<div>
<Button>Click me</Button>

View File

@@ -44,7 +44,7 @@ The following registry item is a custom style that extends shadcn/ui. On `npx sh
The following registry item is a custom style that doesn't extend shadcn/ui. See the `extends: none` field.
It can be used to create a new style from scratch, i.e. custom components, css vars, dependencies, etc.
It can be used to create a new style from scratch i.e custom components, css vars, dependencies, etc.
On `npx shadcn add`, the following will:
@@ -69,21 +69,21 @@ On `npx shadcn add`, the following will:
],
"cssVars": {
"theme": {
"font-sans": "Inter, sans-serif"
},
"font-sans": "Inter, sans-serif",
}
"light": {
"main": "#88aaee",
"bg": "#dfe5f2",
"border": "#000",
"text": "#000",
"ring": "#000"
"ring": "#000",
},
"dark": {
"main": "#88aaee",
"bg": "#272933",
"border": "#000",
"text": "#e6e6e6",
"ring": "#fff"
"ring": "#fff",
}
}
}
@@ -147,7 +147,7 @@ The following style will init using shadcn/ui defaults and then add a custom `br
### Custom block
This block installs the `login-01` block from the shadcn/ui registry.
This blocks installs the `login-01` block from the shadcn/ui registry.
```json title="login-01.json" showLineNumbers
{
@@ -174,7 +174,7 @@ This block installs the `login-01` block from the shadcn/ui registry.
### Install a block and override primitives
You can install a block from the shadcn/ui registry and override the primitives using your custom ones.
You can install a block fromt the shadcn/ui registry and override the primitives using your custom ones.
On `npx shadcn add`, the following will:
@@ -325,8 +325,7 @@ A `registry:font` item installs a Google Font. The `font` field is required and
"provider": "google",
"import": "Inter",
"variable": "--font-sans",
"subsets": ["latin"],
"dependency": "@fontsource-variable/inter"
"subsets": ["latin"]
}
}
```
@@ -344,8 +343,7 @@ A `registry:font` item installs a Google Font. The `font` field is required and
"import": "JetBrains_Mono",
"variable": "--font-mono",
"weight": ["400", "500", "600", "700"],
"subsets": ["latin"],
"dependency": "@fontsource-variable/jetbrains-mono"
"subsets": ["latin"]
}
}
```
@@ -362,8 +360,7 @@ A `registry:font` item installs a Google Font. The `font` field is required and
"provider": "google",
"import": "Lora",
"variable": "--font-serif",
"subsets": ["latin"],
"dependency": "@fontsource-variable/lora"
"subsets": ["latin"]
}
}
```
@@ -383,8 +380,7 @@ Use the `selector` field to apply a font to specific CSS selectors instead of gl
"import": "Playfair_Display",
"variable": "--font-heading",
"subsets": ["latin"],
"selector": "h1, h2, h3, h4, h5, h6",
"dependency": "@fontsource-variable/playfair-display"
"selector": "h1, h2, h3, h4, h5, h6"
}
}
```
@@ -399,24 +395,24 @@ A `registry:base` item is a complete design system base. It defines the full set
The `config` field accepts the following properties (all optional):
| Property | Type | Description |
| -------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `style` | `string` | The style name for the base. |
| `iconLibrary` | `string` | The icon library to use (e.g. `lucide`). |
| `rsc` | `boolean` | Whether to enable React Server Components. Defaults to `false`. |
| `tsx` | `boolean` | Whether to use TypeScript. Defaults to `true`. |
| `rtl` | `boolean` | Whether to enable right-to-left support. Defaults to `false`. |
| `menuColor` | `"default" \| "inverted" \| "default-translucent" \| "inverted-translucent"` | The menu color scheme. Defaults to `"default"`. |
| `menuAccent` | `"subtle" \| "bold"` | The menu accent style. Defaults to `"subtle"`. |
| `tailwind.baseColor` | `string` | The base color name (e.g. `neutral`, `slate`, `zinc`). |
| `tailwind.css` | `string` | Path to the Tailwind CSS file. |
| `tailwind.prefix` | `string` | A prefix to add to all Tailwind classes. |
| `aliases.components` | `string` | Import alias for components. |
| `aliases.utils` | `string` | Import alias for utilities. |
| `aliases.ui` | `string` | Import alias for UI components. |
| `aliases.lib` | `string` | Import alias for lib. |
| `aliases.hooks` | `string` | Import alias for hooks. |
| `registries` | `Record<string, string \| object>` | Custom registry URLs. Keys must start with `@`. |
| Property | Type | Description |
| -------------------- | ---------------------------------- | --------------------------------------------------------------- |
| `style` | `string` | The style name for the base. |
| `iconLibrary` | `string` | The icon library to use (e.g. `lucide`). |
| `rsc` | `boolean` | Whether to enable React Server Components. Defaults to `false`. |
| `tsx` | `boolean` | Whether to use TypeScript. Defaults to `true`. |
| `rtl` | `boolean` | Whether to enable right-to-left support. Defaults to `false`. |
| `menuColor` | `"default" \| "inverted"` | The menu color scheme. Defaults to `"default"`. |
| `menuAccent` | `"subtle" \| "bold"` | The menu accent style. Defaults to `"subtle"`. |
| `tailwind.baseColor` | `string` | The base color name (e.g. `neutral`, `slate`, `zinc`). |
| `tailwind.css` | `string` | Path to the Tailwind CSS file. |
| `tailwind.prefix` | `string` | A prefix to add to all Tailwind classes. |
| `aliases.components` | `string` | Import alias for components. |
| `aliases.utils` | `string` | Import alias for utilities. |
| `aliases.ui` | `string` | Import alias for UI components. |
| `aliases.lib` | `string` | Import alias for lib. |
| `aliases.hooks` | `string` | Import alias for hooks. |
| `registries` | `Record<string, string \| object>` | Custom registry URLs. Keys must start with `@`. |
```json title="custom-base.json" showLineNumbers
{
@@ -877,7 +873,7 @@ Note: you need to define both `@keyframes` in css and `theme` in cssVars to use
You can add environment variables using the `envVars` field.
```json title="example-item.json" showLineNumbers {5-9}
{
{»
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "custom-item",
"type": "registry:item",
@@ -921,7 +917,7 @@ Here's an example of a registry item that installs custom Cursor rules for _pyth
}
```
Here's another example for installing a custom ESLint config:
Here's another example for installation custom ESLint config:
```json title=".eslintrc.json" showLineNumbers {9}
{
@@ -944,7 +940,7 @@ You can also have a universal item that installs multiple files:
```json title="my-custom-starter-template.json" showLineNumbers {9}
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "my-custom-starter-template",
"name": "my-custom-start-template",
"type": "registry:item",
"dependencies": ["better-auth"],
"files": [

View File

@@ -36,7 +36,7 @@ Here's an example of a complex component that installs a page, two components, a
},
{
"path": "registry/new-york/hello-world/lib/format-date.ts",
"type": "registry:lib"
"type": "registry:utils"
},
{
"path": "registry/new-york/hello-world/hello.config.ts",

View File

@@ -18,7 +18,7 @@ See [Build your Open in v0 button](https://v0.dev/chat/button) for more informat
Here's a simple example of how to add a `Open in v0` button to your site.
```tsx showLineNumbers
```jsx showLineNumbers
import { Button } from "@/components/ui/button"
export function OpenInV0Button({ url }: { url: string }) {

View File

@@ -56,6 +56,7 @@ Here's an example of a valid registry:
}
]
}
}
]
}
```

View File

@@ -339,34 +339,6 @@ Environment variables are added to the `.env.local` or `.env` file. Existing var
</Callout>
### font
The `font` property is required for `registry:font` items. It configures the font family, provider, import name, CSS variable, and the npm package to install for non-Next.js projects.
```json title="registry-item.json" showLineNumbers
{
"font": {
"family": "'Inter Variable', sans-serif",
"provider": "google",
"import": "Inter",
"variable": "--font-sans",
"subsets": ["latin"],
"dependency": "@fontsource-variable/inter"
}
}
```
| Property | Type | Required | Description |
| ------------ | ---------- | -------- | ----------------------------------------------------------------------------------------- |
| `family` | `string` | Yes | The CSS font-family value. |
| `provider` | `string` | Yes | The font provider. Currently only `google` is supported. |
| `import` | `string` | Yes | The import name for the font from `next/font/google`. |
| `variable` | `string` | Yes | The CSS variable name for the font (e.g., `--font-sans`, `--font-mono`). |
| `weight` | `string[]` | No | Array of font weights to include. |
| `subsets` | `string[]` | No | Array of font subsets to include. |
| `selector` | `string` | No | CSS selector to apply the font to. Defaults to `html`. |
| `dependency` | `string` | No | The npm package to install for non-Next.js projects (e.g., `@fontsource-variable/inter`). |
### docs
Use `docs` to show custom documentation or message when installing your registry item via the CLI.

View File

@@ -116,7 +116,7 @@ function AlertDialogTitle({
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn(
"cn-font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
"text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
className
)}
{...props}

View File

@@ -38,7 +38,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="alert-title"
className={cn(
"cn-font-heading font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
className
)}
{...props}

View File

@@ -5,7 +5,7 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {

View File

@@ -37,7 +37,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-title"
className={cn(
"cn-font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}

View File

@@ -110,7 +110,7 @@ function ComboboxContent({
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"cn-menu-target cn-menu-translucent group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-start-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"cn-menu-target group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-start-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}

View File

@@ -52,7 +52,7 @@ function ContextMenuContent({
<ContextMenuPrimitive.Popup
data-slot="context-menu-content"
className={cn(
"cn-menu-target cn-menu-translucent z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-start-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"cn-menu-target z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-start-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
@@ -147,7 +147,7 @@ function ContextMenuSubContent({
return (
<ContextMenuContent
data-slot="context-menu-sub-content"
className="cn-menu-target cn-menu-translucent shadow-lg"
className="shadow-lg"
side="inline-end"
{...props}
/>

View File

@@ -119,10 +119,7 @@ function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"cn-font-heading text-base leading-none font-medium",
className
)}
className={cn("text-base leading-none font-medium", className)}
{...props}
/>
)

View File

@@ -97,10 +97,7 @@ function DrawerTitle({
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn(
"cn-font-heading text-base font-medium text-foreground",
className
)}
className={cn("text-base font-medium text-foreground", className)}
{...props}
/>
)

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