mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-03 01:18:38 +00:00
feat: add serif fonts
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -28,6 +28,7 @@ export function RandomButton() {
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={randomize}
|
||||
className="hidden w-full sm:flex"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -5,7 +5,6 @@ export const BASE_COLORS = THEMES.filter((theme) =>
|
||||
"neutral",
|
||||
"stone",
|
||||
"zinc",
|
||||
"gray",
|
||||
"mauve",
|
||||
"olive",
|
||||
"mist",
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -60,6 +60,9 @@ export const PRESET_FONTS = [
|
||||
"jetbrains-mono",
|
||||
"geist",
|
||||
"geist-mono",
|
||||
"lora",
|
||||
"merriweather",
|
||||
"playfair-display",
|
||||
] as const
|
||||
export const PRESET_RADII = [
|
||||
"default",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user