diff --git a/apps/v4/content/docs/registry/examples.mdx b/apps/v4/content/docs/registry/examples.mdx index fc434564ff..bbd58686ef 100644 --- a/apps/v4/content/docs/registry/examples.mdx +++ b/apps/v4/content/docs/registry/examples.mdx @@ -365,6 +365,28 @@ A `registry:font` item installs a Google Font. The `font` field is required and } ``` +### Font with custom selector + +Use the `selector` field to apply a font to specific CSS selectors instead of globally on `html`. This is useful for heading fonts or other targeted font applications. + +```json title="font-playfair-display.json" showLineNumbers +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "font-playfair-display", + "type": "registry:font", + "font": { + "family": "'Playfair Display Variable', serif", + "provider": "google", + "import": "Playfair_Display", + "variable": "--font-heading", + "subsets": ["latin"], + "selector": "h1, h2, h3, h4, h5, h6" + } +} +``` + +When `selector` is set, the font utility class (e.g. `font-heading`) is applied via CSS `@apply` on the specified selector within `@layer base`, instead of being added to the `` element. The CSS variable is still injected on `` so it's available globally. + ## registry:base ### Custom base diff --git a/apps/v4/public/r/templates/astro-monorepo.tar.gz b/apps/v4/public/r/templates/astro-monorepo.tar.gz index dc44ccd911..fd68348bf1 100644 Binary files a/apps/v4/public/r/templates/astro-monorepo.tar.gz and b/apps/v4/public/r/templates/astro-monorepo.tar.gz differ diff --git a/apps/v4/public/r/templates/next-monorepo.tar.gz b/apps/v4/public/r/templates/next-monorepo.tar.gz index 14f4a5203f..7f63743f2d 100644 Binary files a/apps/v4/public/r/templates/next-monorepo.tar.gz and b/apps/v4/public/r/templates/next-monorepo.tar.gz differ diff --git a/apps/v4/public/r/templates/react-router-monorepo.tar.gz b/apps/v4/public/r/templates/react-router-monorepo.tar.gz index 51dfd96a6b..67304d048c 100644 Binary files a/apps/v4/public/r/templates/react-router-monorepo.tar.gz and b/apps/v4/public/r/templates/react-router-monorepo.tar.gz differ diff --git a/apps/v4/public/r/templates/start-monorepo.tar.gz b/apps/v4/public/r/templates/start-monorepo.tar.gz index f564b0ab5d..03508e50e5 100644 Binary files a/apps/v4/public/r/templates/start-monorepo.tar.gz and b/apps/v4/public/r/templates/start-monorepo.tar.gz differ diff --git a/apps/v4/public/r/templates/vite-monorepo.tar.gz b/apps/v4/public/r/templates/vite-monorepo.tar.gz index 2416cd2d52..a9a07acd85 100644 Binary files a/apps/v4/public/r/templates/vite-monorepo.tar.gz and b/apps/v4/public/r/templates/vite-monorepo.tar.gz differ diff --git a/apps/v4/public/schema/registry-item.json b/apps/v4/public/schema/registry-item.json index da2aab51a7..772b1212ff 100644 --- a/apps/v4/public/schema/registry-item.json +++ b/apps/v4/public/schema/registry-item.json @@ -246,6 +246,10 @@ "type": "string" }, "description": "Array of font subsets to include (e.g., ['latin', 'latin-ext'])." + }, + "selector": { + "type": "string", + "description": "CSS selector to apply the font utility class to (e.g., 'h1, h2, h3'). Defaults to 'html'." } }, "required": ["family", "provider", "import", "variable"] diff --git a/packages/shadcn/src/commands/build.ts b/packages/shadcn/src/commands/build.ts index 6116d92426..5515f94689 100644 --- a/packages/shadcn/src/commands/build.ts +++ b/packages/shadcn/src/commands/build.ts @@ -57,7 +57,8 @@ export const build = new Command() buildSpinner.start(`Building ${registryItem.name}...`) // Add the schema to the registry item. - registryItem["$schema"] = `${SHADCN_URL}/schema/registry-item.json` + registryItem["$schema"] = + "https://ui.shadcn.com/schema/registry-item.json" // Loop through each file in the files array. for (const file of registryItem.files ?? []) { diff --git a/packages/shadcn/src/commands/info.ts b/packages/shadcn/src/commands/info.ts index 849e2561bf..ab5afddd31 100644 --- a/packages/shadcn/src/commands/info.ts +++ b/packages/shadcn/src/commands/info.ts @@ -148,7 +148,7 @@ function collectInfo( components: `${SHADCN_URL}/docs/components/${base}/[component].md`, ui: `${GITHUB_RAW_BASE}/${base}/ui/[component].tsx`, examples: `${GITHUB_RAW_BASE}/${base}/examples/[component]-example.tsx`, - schema: `${SHADCN_URL}/schema.json`, + schema: "https://ui.shadcn.com/schema.json", }, } } diff --git a/packages/shadcn/src/commands/init.ts b/packages/shadcn/src/commands/init.ts index 3a0e9da9bd..d29db83e4d 100644 --- a/packages/shadcn/src/commands/init.ts +++ b/packages/shadcn/src/commands/init.ts @@ -815,7 +815,7 @@ async function promptForConfig(defaultConfig: Config | null = null) { } return rawConfigSchema.parse({ - $schema: `${SHADCN_URL}/schema.json`, + $schema: "https://ui.shadcn.com/schema.json", style: options.style, tailwind: { config: options.tailwindConfig, diff --git a/packages/shadcn/src/commands/registry/build.ts b/packages/shadcn/src/commands/registry/build.ts index c3ab9d0eb6..942640684d 100644 --- a/packages/shadcn/src/commands/registry/build.ts +++ b/packages/shadcn/src/commands/registry/build.ts @@ -121,7 +121,8 @@ async function buildRegistry(opts: z.infer) { buildSpinner.start(`Building ${registryItem.name}...`) // Add the schema to the registry item. - registryItem["$schema"] = `${SHADCN_URL}/schema/registry-item.json` + registryItem["$schema"] = + "https://ui.shadcn.com/schema/registry-item.json" for (const file of registryItem.files) { const absPath = path.resolve(resolvePaths.cwd, file.path) diff --git a/packages/shadcn/src/registry/schema.ts b/packages/shadcn/src/registry/schema.ts index c6e51da81a..5ed646627d 100644 --- a/packages/shadcn/src/registry/schema.ts +++ b/packages/shadcn/src/registry/schema.ts @@ -142,6 +142,7 @@ export const registryItemFontSchema = z.object({ variable: z.string(), weight: z.array(z.string()).optional(), subsets: z.array(z.string()).optional(), + selector: z.string().optional(), }) // Common fields shared by all registry items. diff --git a/packages/shadcn/src/utils/get-project-info.ts b/packages/shadcn/src/utils/get-project-info.ts index f4c4df30c3..affc185ef3 100644 --- a/packages/shadcn/src/utils/get-project-info.ts +++ b/packages/shadcn/src/utils/get-project-info.ts @@ -385,7 +385,7 @@ export async function getProjectConfig( } const config: z.infer = { - $schema: `${SHADCN_URL}/schema.json`, + $schema: "https://ui.shadcn.com/schema.json", rsc: projectInfo.isRSC, tsx: projectInfo.isTsx, style: "new-york", diff --git a/packages/shadcn/src/utils/updaters/update-fonts.test.ts b/packages/shadcn/src/utils/updaters/update-fonts.test.ts index bbcb16ee59..98a6f59c74 100644 --- a/packages/shadcn/src/utils/updaters/update-fonts.test.ts +++ b/packages/shadcn/src/utils/updaters/update-fonts.test.ts @@ -1163,6 +1163,59 @@ export default function RootLayout({ expect(firstRun).toContain("inter.variable") expect(firstRun).toContain("merriweather.variable") }) + + it("should add .variable but not utility class for custom selector font", async () => { + const input = ` +import type { Metadata } from "next" +import "./globals.css" + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} +` + 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-playfair-display", + type: "registry:font" as const, + font: { + family: "'Playfair Display Variable', serif", + provider: "google" as const, + import: "Playfair_Display", + variable: "--font-heading", + subsets: ["latin"], + selector: "h1, h2, h3, h4, h5, h6", + }, + }, + ] + + const result = await transformLayoutFonts(input, fonts, mockConfig) + + // .variable should be on for both fonts. + expect(result).toContain("inter.variable") + expect(result).toContain("playfairDisplay.variable") + // Only font-sans utility class should be on , not font-heading. + expect(result).toContain('"font-sans"') + expect(result).not.toContain('"font-heading"') + }) }) vi.mock("@/src/utils/get-project-info", () => ({ @@ -1174,7 +1227,7 @@ vi.mock("@/src/utils/get-project-info", () => ({ })) describe("massageTreeForFonts", () => { - it("should add font @apply to body when no existing css", async () => { + it("should add font @apply to html when no existing css", async () => { const tree = { fonts: [ { @@ -1195,12 +1248,12 @@ describe("massageTreeForFonts", () => { resolvedPaths: { cwd: "/test" }, } as any) - expect(result.css!["@layer base"].body).toEqual({ + expect(result.css!["@layer base"].html).toEqual({ "@apply font-sans": {}, }) }) - it("should preserve existing body css rules when adding font classes", async () => { + it("should preserve existing html css rules when adding font classes", async () => { const tree = { fonts: [ { @@ -1220,7 +1273,7 @@ describe("massageTreeForFonts", () => { }, css: { "@layer base": { - body: { + html: { "@apply bg-background text-foreground": {}, }, }, @@ -1231,7 +1284,7 @@ describe("massageTreeForFonts", () => { resolvedPaths: { cwd: "/test" }, } as any) - expect(result.css!["@layer base"].body).toEqual({ + expect(result.css!["@layer base"].html).toEqual({ "@apply bg-background text-foreground font-sans": {}, }) }) @@ -1264,7 +1317,7 @@ describe("massageTreeForFonts", () => { ], css: { "@layer base": { - body: { + html: { "@apply bg-background text-foreground": {}, }, }, @@ -1275,8 +1328,77 @@ describe("massageTreeForFonts", () => { resolvedPaths: { cwd: "/test" }, } as any) - expect(result.css!["@layer base"].body).toEqual({ + expect(result.css!["@layer base"].html).toEqual({ "@apply bg-background text-foreground font-sans font-serif": {}, }) }) + + it("should apply font to custom selector", async () => { + const tree = { + fonts: [ + { + name: "font-playfair-display", + type: "registry:font" as const, + font: { + family: "'Playfair Display Variable', serif", + provider: "google" as const, + import: "Playfair_Display", + variable: "--font-heading", + subsets: ["latin"], + selector: "h1, h2, h3, h4, h5, h6", + }, + }, + ], + } as any + + const result = await massageTreeForFonts(tree, { + resolvedPaths: { cwd: "/test" }, + } as any) + + expect(result.css!["@layer base"]["h1, h2, h3, h4, h5, h6"]).toEqual({ + "@apply font-heading": {}, + }) + expect(result.css!["@layer base"].html).toBeUndefined() + }) + + it("should handle mixed selectors (default html + custom)", async () => { + const tree = { + 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-playfair-display", + type: "registry:font" as const, + font: { + family: "'Playfair Display Variable', serif", + provider: "google" as const, + import: "Playfair_Display", + variable: "--font-heading", + subsets: ["latin"], + selector: "h1, h2, h3, h4, h5, h6", + }, + }, + ], + } as any + + const result = await massageTreeForFonts(tree, { + resolvedPaths: { cwd: "/test" }, + } as any) + + expect(result.css!["@layer base"].html).toEqual({ + "@apply font-sans": {}, + }) + expect(result.css!["@layer base"]["h1, h2, h3, h4, h5, h6"]).toEqual({ + "@apply font-heading": {}, + }) + }) }) diff --git a/packages/shadcn/src/utils/updaters/update-fonts.ts b/packages/shadcn/src/utils/updaters/update-fonts.ts index 5ce4243af0..1ec26cfe9d 100644 --- a/packages/shadcn/src/utils/updaters/update-fonts.ts +++ b/packages/shadcn/src/utils/updaters/update-fonts.ts @@ -53,23 +53,37 @@ export async function massageTreeForFonts( } } - // Apply font utility classes to body. + // Apply font utility classes grouped by selector. if (tree.fonts.length > 0) { - const fontClasses = tree.fonts - .map((f) => f.font.variable.replace("--", "")) - .join(" ") + // Group fonts by their CSS selector (default to "html"). + const groups = new Map() + for (const font of tree.fonts) { + const selector = font.font.selector ?? "html" + const cls = font.font.variable.replace("--", "") + if (!groups.has(selector)) { + groups.set(selector, []) + } + groups.get(selector)!.push(cls) + } + tree.css ??= {} tree.css["@layer base"] ??= {} - tree.css["@layer base"].body ??= {} - // Find existing @apply key and merge, or create new. - const existingApplyKey = Object.keys(tree.css["@layer base"].body).find( - (key) => key.startsWith("@apply ") - ) - if (existingApplyKey) { - delete tree.css["@layer base"].body[existingApplyKey] - tree.css["@layer base"].body[`${existingApplyKey} ${fontClasses}`] = {} - } else { - tree.css["@layer base"].body[`@apply ${fontClasses}`] = {} + + for (const [selector, classes] of groups) { + const fontClasses = classes.join(" ") + tree.css["@layer base"][selector] ??= {} + // Find existing @apply key and merge, or create new. + const existingApplyKey = Object.keys( + tree.css["@layer base"][selector] + ).find((key) => key.startsWith("@apply ")) + if (existingApplyKey) { + delete tree.css["@layer base"][selector][existingApplyKey] + tree.css["@layer base"][selector][ + `${existingApplyKey} ${fontClasses}` + ] = {} + } else { + tree.css["@layer base"][selector][`@apply ${fontClasses}`] = {} + } } } @@ -206,7 +220,10 @@ export async function transformLayoutFonts( ) if (existingVarDecl) { fontVariableNames.push(existingVarDecl.getName()) - fontUtilityClasses.push(font.font.variable.replace("--", "")) + // Only add utility class to if font has no custom selector. + if (!font.font.selector) { + fontUtilityClasses.push(font.font.variable.replace("--", "")) + } } continue } @@ -259,7 +276,10 @@ export async function transformLayoutFonts( } fontVariableNames.push(varName) - fontUtilityClasses.push(font.font.variable.replace("--", "")) + // Only add utility class to if font has no custom selector. + if (!font.font.selector) { + fontUtilityClasses.push(font.font.variable.replace("--", "")) + } } // Only keep one font-family class (font-sans, font-serif, font-mono) on . diff --git a/templates/astro-monorepo/apps/web/src/pages/index.astro b/templates/astro-monorepo/apps/web/src/pages/index.astro index a14300aacc..25e624863c 100644 --- a/templates/astro-monorepo/apps/web/src/pages/index.astro +++ b/templates/astro-monorepo/apps/web/src/pages/index.astro @@ -1,6 +1,6 @@ --- import "@workspace/ui/globals.css" -import { Button } from "@workspace/ui/components/ui/button" +import { Button } from "@workspace/ui/components/button" --- diff --git a/templates/next-monorepo/apps/web/app/page.tsx b/templates/next-monorepo/apps/web/app/page.tsx index 8231c83fb9..88765bf926 100644 --- a/templates/next-monorepo/apps/web/app/page.tsx +++ b/templates/next-monorepo/apps/web/app/page.tsx @@ -1,4 +1,4 @@ -import { Button } from "@workspace/ui/components/ui/button" +import { Button } from "@workspace/ui/components/button" export default function Page() { return ( @@ -10,7 +10,7 @@ export default function Page() {

We've already added the button component for you.

-
+
(Press d to toggle dark mode)
diff --git a/templates/react-router-monorepo/apps/web/app/routes/home.tsx b/templates/react-router-monorepo/apps/web/app/routes/home.tsx index c4b7fe6d30..c93be941da 100644 --- a/templates/react-router-monorepo/apps/web/app/routes/home.tsx +++ b/templates/react-router-monorepo/apps/web/app/routes/home.tsx @@ -1,4 +1,4 @@ -import { Button } from "@workspace/ui/components/ui/button" +import { Button } from "@workspace/ui/components/button" export default function Home() { return ( diff --git a/templates/start-monorepo/apps/web/src/routes/index.tsx b/templates/start-monorepo/apps/web/src/routes/index.tsx index 806cdde062..4a2709124e 100644 --- a/templates/start-monorepo/apps/web/src/routes/index.tsx +++ b/templates/start-monorepo/apps/web/src/routes/index.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from "@tanstack/react-router" -import { Button } from "@workspace/ui/components/ui/button" +import { Button } from "@workspace/ui/components/button" export const Route = createFileRoute("/")({ component: App }) diff --git a/templates/vite-monorepo/apps/web/src/App.tsx b/templates/vite-monorepo/apps/web/src/App.tsx index 9e8103760b..55a08a929b 100644 --- a/templates/vite-monorepo/apps/web/src/App.tsx +++ b/templates/vite-monorepo/apps/web/src/App.tsx @@ -1,4 +1,4 @@ -import { Button } from "@workspace/ui/components/ui/button" +import { Button } from "@workspace/ui/components/button" export function App() { return ( @@ -10,7 +10,7 @@ export function App() {

We've already added the button component for you.

-
+
(Press d to toggle dark mode)