Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/v4/content/docs/04.theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,34 @@ This means:
- Larger radii scale up from `--radius`.
- Changing `--radius` updates the entire radius scale.

## Fonts

Your font comes from the preset you initialize with, or from the one you apply later:

```bash
npx shadcn-vue@latest apply <preset>
```

The CLI installs the font as a package and imports it from your CSS file, so the
files are served from your own app instead of a font CDN:

```css title="assets/css/tailwind.css"
@import "@fontsource-variable/inter";
```

```css
@theme inline {
--font-sans: 'Inter Variable', sans-serif;
--font-heading: var(--font-sans);
}
```

Fonts are only written when you init or apply a preset — adding a component never
touches them. To use a font the CLI doesn't ship, set `--font-sans` (or
`--font-heading`) to your own family and load it however you like: `@nuxt/fonts`,
`unplugin-fonts`, or your own `@font-face` rules all work, and the CLI leaves them
alone.

## Adding New Tokens

To add a new token, define it under `:root` and `.dark`, then expose it to Tailwind with `@theme inline`.
Expand Down
1 change: 0 additions & 1 deletion apps/v4/content/docs/06.cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ Options:
--base <base> the component library base to use. (reka)
--style <style> the visual style to use. (vega, nova, maia, lyra, mira, luma, sera)
--icon-library <icon-library> the icon library to use. (lucide, tabler, hugeicons, phosphor, remixicon)
--font <font> the font to use. (inter, figtree, jetbrains-mono, geist, geist-mono)
-b, --base-color <base-color> the base color to use. (neutral, gray, zinc, stone, slate)
-n, --name <name> the name for the new project.
-d, --defaults use default configuration. (default: false)
Expand Down
3 changes: 0 additions & 3 deletions apps/v4/public/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
"base": {
"type": "string"
},
"font": {
"type": "string"
},
"typescript": {
"type": "boolean",
"default": true
Expand Down
58 changes: 33 additions & 25 deletions apps/v4/registry/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { iconLibraries } from "shadcn-vue/icons"
import { z } from "zod"
import { BASE_COLORS } from "@/registry/base-colors"
import { BASES } from "@/registry/bases"
import { fonts } from "@/registry/fonts"
import { bodyFonts, fonts, headingFonts } from "@/registry/fonts"
import { STYLES } from "@/registry/styles"
import { THEMES } from "@/registry/themes"

Expand All @@ -18,7 +18,7 @@ export { type Base, BASES }
export { type Style, STYLES }
export { type Theme, THEMES }
export { BASE_COLORS, type BaseColor }
export { fonts }
export { bodyFonts, fonts, headingFonts }
export { iconLibraries, type IconLibrary, type IconLibraryName }

export type BaseName = Base["name"]
Expand All @@ -30,7 +30,7 @@ export const POINTER_CURSOR_SELECTOR
= "button:not(:disabled), [role=\"button\"]:not(:disabled)"

// Derive font values from registry fonts (e.g., "font-inter" -> "inter").
const fontValues = fonts.map(f => f.name.replace("font-", "")) as [
const fontValues = bodyFonts.map(f => f.name.replace("font-", "")) as [
string,
...string[],
]
Expand Down Expand Up @@ -403,29 +403,42 @@ export function buildRegistryBase(
...iconLibraryItem.packages,
]

// Fonts are applied CLI-side via getFontImport(config.font) from the
// local FONTS constant — shadcn-vue's registry does not publish font-*
// items, so we intentionally do not add them as registryDependencies.
const registryDependencies = ["utils"]

// Resolve font metadata from the web registry so the emitted
// registry:base item carries both the @theme CSS variable and a body rule
// that actually applies the font — the CLI's addFontImportPlugin only
// handles the Google Fonts @import url(...) line.
const fontItem = fonts.find(f => f.name === `font-${config.font}`)
const fontItem = bodyFonts.find(f => f.name === `font-${config.font}`)
const fontHeadingItem
= normalizedFontHeading !== "inherit"
? fonts.find(f => f.name === `font-${normalizedFontHeading}`)
? headingFonts.find(f => f.name === `font-heading-${normalizedFontHeading}`)
: undefined

// Fonts are registry items: the CLI installs the fontsource package each one
// names and imports it from the project's CSS file. Nothing is fetched from
// a font CDN, and no font URL is written into the project.
const registryDependencies = [
"utils",
...(fontItem ? [fontItem.name] : []),
...(fontHeadingItem ? [fontHeadingItem.name] : []),
]

const themeVars: Record<string, string> = {
...(registryTheme.cssVars?.theme as Record<string, string> | undefined),
}
const bodyRules: Record<string, Record<string, unknown>> = {
"@apply bg-background text-foreground": {},
}

// The font item carries this same value, but CLI versions before font items
// existed only read the base — without it they'd fall back to the default
// sans stack. Both sides emit the identical family, so keeping it costs a
// duplicate declaration and nothing else.
if (fontItem) {
themeVars[fontItem.font.variable] = fontItem.font.family
}
if (fontHeadingItem) {
themeVars["--font-heading"] = fontHeadingItem.font.family
}

// The base emits the body rule that applies the font, since that's a
// property of the base rather than of the font.
if (fontItem) {
// Map the font's target variable to a Tailwind utility class.
// shadcn-vue fonts all target --font-sans today (jetbrains-mono included),
// but we handle --font-mono / --font-serif for future-proofing.
Expand All @@ -438,18 +451,12 @@ export function buildRegistryBase(
bodyRules[`@apply ${applyClass}`] = {}
}

// Emit --font-heading so the Tailwind v4 `font-heading` utility is wired
// up. When fontHeading is "inherit" (default) we alias it to the body
// font's CSS variable; otherwise we resolve the heading font's family
// from the web registry and emit it literally. The heading font's Google
// Fonts @import is pulled in CLI-side by addFontImportPlugin (see
// add-components.ts) via `config.fontHeading`.
// Emit --font-heading so the Tailwind v4 `font-heading` utility is wired up.
// When fontHeading is "inherit" (default) we alias it to the body font's CSS
// variable; a real heading font brings its own value through its item.
if (normalizedFontHeading === "inherit") {
themeVars["--font-heading"] = `var(${fontItem?.font.variable ?? "--font-sans"})`
}
else if (fontHeadingItem) {
themeVars["--font-heading"] = fontHeadingItem.font.family
}

return {
name: `${config.base}-${config.style}`,
Expand All @@ -458,9 +465,10 @@ export function buildRegistryBase(
config: {
style: `${config.base}-${config.style}`,
iconLibrary: iconLibraryItem.name,
// Deprecated: current CLIs drop these on parse — fonts travel as
// registry:font items now. Kept because CLI versions that still write
// the font's @import themselves read them to know which font to write.
font: config.font,
// Only persist fontHeading when it's a real override, so projects
// with the default ("inherit") don't gain a new components.json field.
...(normalizedFontHeading !== "inherit"
&& { fontHeading: normalizedFontHeading }),
rtl: config.rtl ?? false,
Expand Down
Loading
Loading