feat: add serif fonts

This commit is contained in:
shadcn
2026-02-27 15:58:51 +04:00
parent a50f6795cc
commit 408d15f73f
19 changed files with 256 additions and 38 deletions

View File

@@ -30,7 +30,7 @@ export function CopyPreset() {
}, [presetCode])
return (
<Button variant="ghost" onClick={handleCopy} className="group/button">
<Button variant="ghost" size="sm" onClick={handleCopy} className="group/button">
<HugeiconsIcon
icon={hasCopied ? Tick02Icon : Copy01Icon}
strokeWidth={2}

View File

@@ -34,7 +34,7 @@ export function Customizer() {
return (
<div
className="flex flex-col gap-4 rounded-2xl border p-4 md:h-[calc(100svh---spacing(12))] md:w-64"
className="flex flex-col gap-4 rounded-2xl border p-4 md:h-[calc(100svh---spacing(8))] md:w-64"
ref={anchorRef}
>
<div className="flex items-center gap-2">

View File

@@ -46,6 +46,8 @@ export function DesignSystemProvider({
body.classList.add(`style-${style}`, `base-color-${baseColor}`)
// Update font.
// Always set --font-sans for the preview so the selected font is visible.
// The font type (sans/serif/mono) is metadata for the CLI updater.
const selectedFont = FONTS.find((f) => f.value === font)
if (selectedFont) {
const fontFamily = selectedFont.font.style.fontFamily

View File

@@ -17,7 +17,7 @@ export function HistoryButtons() {
<div className="hidden items-center gap-1 sm:flex">
<Button
variant="ghost"
size="icon"
size="icon-sm"
title="Undo"
disabled={!canGoBack}
onClick={goBack}
@@ -27,7 +27,7 @@ export function HistoryButtons() {
</Button>
<Button
variant="ghost"
size="icon"
size="icon-sm"
title="Redo"
disabled={!canGoForward}
onClick={goForward}

View File

@@ -39,7 +39,7 @@ export function MainMenu() {
<ButtonGroup>
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="secondary" id="menu-button" />}
render={<Button variant="secondary" size="sm" id="menu-button" />}
>
Menu
<ChevronDownIcon data-icon="inline-end" />

View File

@@ -30,8 +30,8 @@ export function ModeSwitcher({
render={
<Button
variant={variant}
size="icon"
className={cn("group/toggle extend-touch-target size-8", className)}
size="icon-sm"
className={cn("group/toggle extend-touch-target", className)}
onClick={toggleTheme}
id="mode-switcher-button"
/>

View File

@@ -117,7 +117,7 @@ export function ProjectForm() {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button />}>Create Project</DialogTrigger>
<DialogTrigger render={<Button size="sm" />}>Create Project</DialogTrigger>
<DialogContent className="min-w-0 sm:max-w-md">
<DialogHeader>
<DialogTitle>Create Project</DialogTitle>

View File

@@ -28,6 +28,7 @@ export function RandomButton() {
</Button>
<Button
variant="outline"
size="sm"
onClick={randomize}
className="hidden w-full sm:flex"
>

View File

@@ -42,7 +42,7 @@ export function ResetButton() {
/>
<AlertDialogTrigger
render={
<Button variant="outline" className="hidden w-full sm:flex">
<Button variant="outline" size="sm" className="hidden w-full sm:flex">
Reset
</Button>
}

View File

@@ -37,7 +37,7 @@ export function ShareButton() {
}, [shareUrl])
return (
<Button variant="outline" onClick={handleCopy}>
<Button variant="outline" size="sm" onClick={handleCopy}>
{hasCopied ? (
<HugeiconsIcon
icon={Tick02Icon}

View File

@@ -46,6 +46,7 @@ export function V0Button({ className }: { className?: string }) {
<Button
nativeButton={false}
role="link"
size="sm"
variant={isMobile ? "default" : "outline"}
className={cn(
"w-24 gap-1 data-[variant=default]:h-[31px] lg:w-8 xl:w-24",

View File

@@ -44,7 +44,6 @@ async function getAllItems() {
const entries = await Promise.all(
BASES.map(async (base) => {
const items = await getItemsForBase(base.name as BaseName)
// Single pass: filter nulls, strip to {name, title, type}, skip numeric suffixes. (js-combine-iterations)
const filtered: Pick<
NonNullable<(typeof items)[number]>,
"name" | "title" | "type"
@@ -76,7 +75,7 @@ export default async function CreatePage() {
<SidebarProvider className="flex h-auto min-h-min flex-1 flex-col items-start overflow-hidden px-0">
<div
data-slot="designer"
className="flex w-full flex-1 flex-col gap-2 p-6 pt-1 pb-4 sm:gap-2 sm:pt-6 md:flex-row md:pb-6 lg:gap-6"
className="flex w-full flex-1 flex-col gap-2 p-4 sm:gap-2 md:flex-row lg:gap-4"
>
<Customizer />
<div className="flex flex-1 flex-col overflow-hidden rounded-2xl border">

View File

@@ -5,9 +5,12 @@ import {
Geist_Mono,
Inter,
JetBrains_Mono,
Lora,
Merriweather,
Noto_Sans,
Nunito_Sans,
Outfit,
Playfair_Display,
Public_Sans,
Raleway,
Roboto,
@@ -73,6 +76,21 @@ const outfit = Outfit({
variable: "--font-outfit",
})
const lora = Lora({
subsets: ["latin"],
variable: "--font-lora",
})
const merriweather = Merriweather({
subsets: ["latin"],
variable: "--font-merriweather",
})
const playfairDisplay = Playfair_Display({
subsets: ["latin"],
variable: "--font-playfair-display",
})
export const FONTS = [
{
name: "Geist",
@@ -146,6 +164,24 @@ export const FONTS = [
font: jetbrainsMono,
type: "mono",
},
{
name: "Lora",
value: "lora",
font: lora,
type: "serif",
},
{
name: "Merriweather",
value: "merriweather",
font: merriweather,
type: "serif",
},
{
name: "Playfair Display",
value: "playfair-display",
font: playfairDisplay,
type: "serif",
},
] as const
export type Font = (typeof FONTS)[number]

View File

@@ -5,7 +5,6 @@ export const BASE_COLORS = THEMES.filter((theme) =>
"neutral",
"stone",
"zinc",
"gray",
"mauve",
"olive",
"mist",

View File

@@ -143,4 +143,40 @@ export const fonts = [
import: "Geist_Mono",
},
},
{
name: "font-lora",
title: "Lora",
type: "registry:font",
font: {
family: "'Lora Variable', serif",
provider: "google",
variable: "--font-serif",
subsets: ["latin"],
import: "Lora",
},
},
{
name: "font-merriweather",
title: "Merriweather",
type: "registry:font",
font: {
family: "'Merriweather Variable', serif",
provider: "google",
variable: "--font-serif",
subsets: ["latin"],
import: "Merriweather",
},
},
{
name: "font-playfair-display",
title: "Playfair Display",
type: "registry:font",
font: {
family: "'Playfair Display Variable', serif",
provider: "google",
variable: "--font-serif",
subsets: ["latin"],
import: "Playfair_Display",
},
},
] satisfies RegistryItem[]

View File

@@ -114,15 +114,14 @@ export default function Page() {
configWithDefaults(packagesUiWithRegistries)
)
if (tree?.fonts?.length) {
const [fontSans] = tree.fonts
// Add font CSS variables to packages/ui CSS (same as massageTreeForFonts for Next.js).
const themeCssVars: Record<string, string> = {}
for (const font of tree.fonts) {
themeCssVars[font.font.variable] = `var(${font.font.variable})`
}
// Add font CSS variable to packages/ui CSS (same as massageTreeForFonts for Next.js).
await updateCssVars(
{
theme: {
[fontSans.font.variable]: `var(${fontSans.font.variable})`,
},
},
{ theme: themeCssVars },
resolvedPackagesUiConfig,
{
silent: options.silent,

View File

@@ -60,6 +60,9 @@ export const PRESET_FONTS = [
"jetbrains-mono",
"geist",
"geist-mono",
"lora",
"merriweather",
"playfair-display",
] as const
export const PRESET_RADII = [
"default",

View File

@@ -827,6 +827,141 @@ export default function RootLayout({
expect(thirdRun).toBe(firstRun)
})
it("should add a single serif font to empty layout", async () => {
const input = `
import type { Metadata } from "next"
import "./globals.css"
export const metadata: Metadata = {
title: "My App",
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
`
const fonts = [
{
name: "font-lora",
type: "registry:font" as const,
font: {
family: "'Lora Variable', serif",
provider: "google" as const,
import: "Lora",
variable: "--font-serif",
subsets: ["latin"],
},
},
]
const result = await transformLayoutFonts(input, fonts, mockConfig)
expect(result).toMatchInlineSnapshot(`
"
import type { Metadata } from "next"
import "./globals.css"
import { Lora } from "next/font/google";
const lora = Lora({subsets:['latin'],variable:'--font-serif'});
export const metadata: Metadata = {
title: "My App",
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={lora.variable}>
<body>{children}</body>
</html>
)
}
"
`)
})
it("should add serif and sans fonts together", async () => {
const input = `
import type { Metadata } from "next"
import "./globals.css"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
`
const fonts = [
{
name: "font-inter",
type: "registry:font" as const,
font: {
family: "'Inter Variable', sans-serif",
provider: "google" as const,
import: "Inter",
variable: "--font-sans",
subsets: ["latin"],
},
},
{
name: "font-lora",
type: "registry:font" as const,
font: {
family: "'Lora Variable', serif",
provider: "google" as const,
import: "Lora",
variable: "--font-serif",
subsets: ["latin"],
},
},
]
const result = await transformLayoutFonts(input, fonts, mockConfig)
expect(result).toMatchInlineSnapshot(`
"
import type { Metadata } from "next"
import "./globals.css"
import { Inter, Lora } from "next/font/google";
import { cn } from "@/lib/utils";
const lora = Lora({subsets:['latin'],variable:'--font-serif'});
const inter = Inter({subsets:['latin'],variable:'--font-sans'});
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={cn(inter.variable, lora.variable)}>
<body>{children}</body>
</html>
)
}
"
`)
})
it("should be idempotent with multiple fonts", async () => {
const input = `
export default function RootLayout({

View File

@@ -27,32 +27,39 @@ export async function massageTreeForFonts(
return tree
}
const [fontSans] = tree.fonts
tree.cssVars ??= {}
tree.cssVars.theme ??= {}
if (
const isNext =
projectInfo.framework.name === "next-app" ||
projectInfo.framework.name === "next-pages"
) {
tree.cssVars.theme[
fontSans.font.variable
] = `var(${fontSans.font.variable})`
return tree
for (const font of tree.fonts) {
if (isNext) {
tree.cssVars.theme[font.font.variable] = `var(${font.font.variable})`
} else {
// Other frameworks will use fontsource for now.
const fontName = font.name.replace("font-", "")
const fontSourceDependency = `@fontsource-variable/${fontName}`
tree.dependencies ??= []
tree.dependencies.push(fontSourceDependency)
tree.css ??= {}
tree.css[`@import "${fontSourceDependency}"`] = {}
tree.cssVars.theme[font.font.variable] = font.font.family
}
}
// Other frameworks will use fontsource for now.
const fontName = fontSans.name.replace("font-", "")
const fontSourceDependency = `@fontsource-variable/${fontName}`
tree.dependencies ??= []
tree.dependencies.push(fontSourceDependency)
tree.css ??= {}
tree.css[`@import "${fontSourceDependency}"`] = {}
tree.css["@layer base"] ??= {}
tree.css["@layer base"].body = {
"@apply font-sans": {},
// For non-Next frameworks, apply font utility classes to body.
if (!isNext && tree.fonts.length > 0) {
const fontClasses = tree.fonts
.map((f) => f.font.variable.replace("--", ""))
.join(" ")
tree.css ??= {}
tree.css["@layer base"] ??= {}
tree.css["@layer base"].body = {
[`@apply ${fontClasses}`]: {},
}
}
tree.cssVars.theme[fontSans.font.variable] = fontSans.font.family
return tree
}