Skip to content

Commit

Permalink
feat: add global fontface support (#2738)
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo authored Jul 19, 2024
1 parent a87ec32 commit e952f82
Show file tree
Hide file tree
Showing 20 changed files with 312 additions and 86 deletions.
61 changes: 61 additions & 0 deletions .changeset/lazy-cups-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
'@pandacss/generator': minor
'@pandacss/types': minor
'@pandacss/core': minor
---

Add support for defining global font face in config and preset

```ts
// pandacss.config.js
export default defineConfig({
globalFontface: {
Roboto: {
src: 'url(/fonts/roboto.woff2) format("woff2")',
fontWeight: '400',
fontStyle: 'normal',
},
},
})
```

You can also add multiple font `src` for the same weight

```ts
// pandacss.config.js

export default defineConfig({
globalFontface: {
Roboto: {
// multiple src
src: ['url(/fonts/roboto.woff2) format("woff2")', 'url(/fonts/roboto.woff) format("woff")'],
fontWeight: '400',
fontStyle: 'normal',
},
},
})
```

You can also define multiple font weights

```ts
// pandacss.config.js

export default defineConfig({
globalFontface: {
// multiple font weights
Roboto: [
{
src: 'url(/fonts/roboto.woff2) format("woff2")',
fontWeight: '400',
fontStyle: 'normal',
},
{
src: 'url(/fonts/roboto-bold.woff2) format("woff2")',
fontWeight: '700',
fontStyle: 'normal',
},
],
},
})
```
5 changes: 5 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
CompositionStyles,
Config,
CssKeyframes,
GlobalFontface,
GlobalStyleObject,
HooksApiInterface,
LayerStyles,
Expand Down Expand Up @@ -72,6 +73,10 @@ export function defineGlobalStyles(definition: GlobalStyleObject) {
return definition
}

export function defineGlobalFontface(definition: GlobalFontface) {
return definition
}

export function defineUtility(utility: PropertyConfig) {
return utility
}
Expand Down
84 changes: 84 additions & 0 deletions packages/core/__tests__/global-fontface.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { GlobalFontface } from '../src/global-fontface'

describe('global fontface', () => {
test('single src', () => {
const fontface = new GlobalFontface({
globalFontface: {
Inter: {
src: 'url(./inter.woff2)',
fontWeight: 400,
fontStyle: 'normal',
},
},
})

expect(fontface.toString()).toMatchInlineSnapshot(`
"@font-face {
font-family: Inter;
src: url(./inter.woff2);
font-weight: 400;
font-style: normal;
}"
`)
})

test('multiple src', () => {
const fontface = new GlobalFontface({
globalFontface: {
Inter: {
src: ['url(./inter.woff2)', 'url(./inter.woff)'],
fontWeight: 400,
fontStyle: 'normal',
},
},
})

expect(fontface.toString()).toMatchInlineSnapshot(`
"@font-face {
font-family: Inter;
src: url(./inter.woff2),url(./inter.woff);
font-weight: 400;
font-style: normal;
}"
`)
})

test('multiple font src', () => {
const fontface = new GlobalFontface({
globalFontface: {
Inter: [
{
src: ['url(./inter.woff2)', 'url(./inter.woff)'],
fontWeight: 400,
fontStyle: 'normal',
},
{
src: ['url(./inter-bold.woff2)', 'url(./inter-bold.woff)'],
fontWeight: 700,
fontStyle: 'normal',
},
],
},
})

expect(fontface.toString()).toMatchInlineSnapshot(`
"@font-face {
font-family: Inter;
src: url(./inter.woff2),url(./inter.woff);
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: Inter;
src: url(./inter-bold.woff2),url(./inter-bold.woff);
font-weight: 700;
font-style: normal;
}"
`)
})
})
19 changes: 19 additions & 0 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { StyleEncoder } from './style-encoder'
import { Stylesheet } from './stylesheet'
import type { ParserOptions, StylesheetContext } from './types'
import { Utility } from './utility'
import { GlobalFontface } from './global-fontface'

const defaults = (config: UserConfig): UserConfig => ({
cssVarRoot: ':where(:root, :host)',
Expand Down Expand Up @@ -67,7 +68,9 @@ export class Context {
imports: ImportMap
paths: PathEngine
file: FileEngine

globalVars: GlobalVars
globalFontface: GlobalFontface

encoder: StyleEncoder
decoder: StyleDecoder
Expand Down Expand Up @@ -139,6 +142,9 @@ export class Context {

// Relies on this.encoder, this.decoder
this.setupCompositions(theme)
this.registerAnimationName(theme)
this.registerFontFamily(config.globalFontface)

this.recipes.save(this.baseSheetContext)

this.staticCss = new StaticCss({
Expand Down Expand Up @@ -179,6 +185,10 @@ export class Context {
cssVarRoot: this.config.cssVarRoot!,
})

this.globalFontface = new GlobalFontface({
globalFontface: this.config.globalFontface,
})

this.messages = getMessages({
jsx: this.jsx,
config: this.config,
Expand Down Expand Up @@ -297,6 +307,14 @@ export class Context {
}
}

private registerAnimationName = (theme: Theme): void => {
this.utility.addPropertyType('animationName', Object.keys(theme.keyframes ?? {}))
}

private registerFontFamily = (fontFaces: Record<string, any> | undefined): void => {
this.utility.addPropertyType('fontFamily', Object.keys(fontFaces ?? {}))
}

setupProperties = (): void => {
this.properties = new Set(['css', ...this.utility.keys(), ...this.conditions.keys()])
this.isValidProperty = memo((key: string) => this.properties.has(key) || isCssProperty(key))
Expand All @@ -317,6 +335,7 @@ export class Context {
cssVarRoot: this.config.cssVarRoot!,
helpers: patternFns,
globalVars: this.globalVars,
globalFontface: this.globalFontface,
} satisfies Omit<StylesheetContext, 'layers'>
}

Expand Down
46 changes: 46 additions & 0 deletions packages/core/src/global-fontface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { FontfaceRule, GlobalFontface as GlobalFontfaceDefinition } from '@pandacss/types'
import { stringify } from './stringify'

interface GlobalFontfaceOptions {
globalFontface?: GlobalFontfaceDefinition
}

export class GlobalFontface {
names: string[]

constructor(private options: GlobalFontfaceOptions) {
const { globalFontface = {} } = options
this.names = Object.keys(globalFontface)
}

isEmpty() {
return this.names.length === 0
}

toString() {
const { globalFontface = {} } = this.options
return stringifyGlobalFontface(globalFontface)
}
}

const stringifyGlobalFontface = (globalFontface: GlobalFontfaceDefinition) => {
if (!globalFontface) return ''

const lines: string[] = []

Object.entries(globalFontface).forEach(([key, value]) => {
const _value = Array.isArray(value) ? value : [value]
_value.forEach((v) => {
lines.push(stringifyFontface(key, v))
})
})

return lines.join('\n\n')
}

function stringifyFontface(fontFamily: string, config: FontfaceRule) {
return `@font-face {
font-family: ${fontFamily};
${stringify(config)}
}`
}
1 change: 1 addition & 0 deletions packages/core/src/stylesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class Stylesheet {

let css = stringify(result)
css += this.context.globalVars.toString()
css += this.context.globalFontface.toString()

if (this.context.hooks['cssgen:done']) {
css = this.context.hooks['cssgen:done']({ artifact: 'global', content: css }) ?? css
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export interface TransformResult {
}

export interface StylesheetContext
extends Pick<Context, 'utility' | 'conditions' | 'encoder' | 'decoder' | 'isValidProperty' | 'hooks' | 'globalVars'> {
extends Pick<
Context,
'utility' | 'conditions' | 'encoder' | 'decoder' | 'isValidProperty' | 'hooks' | 'globalVars' | 'globalFontface'
> {
layers: Layers
helpers: PatternHelpers
hash?: boolean
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,11 @@ export class Utility {
}
}

addPropertyType = (property: string, type: string[]) => {
const set = this.types.get(property) ?? new Set()
this.types.set(property, new Set([...set, ...type]))
}

/**
* Returns the Typescript type for the define properties
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/fixture/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const textStyles = {
},
}

export const fixturePreset: Omit<PresetCore, 'globalCss' | 'staticCss' | 'globalVars'> = {
export const fixturePreset: Omit<PresetCore, 'globalCss' | 'staticCss' | 'globalVars' | 'globalFontface'> = {
...presetBase,
conditions,
theme: {
Expand Down
1 change: 1 addition & 0 deletions packages/generator/__tests__/generate-prop-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ describe('generate property types', () => {
export interface UtilityValues {
textStyle: "headline.h1" | "headline.h2";
animationName: "spin" | "ping" | "pulse" | "bounce";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"content": "import type { ConditionalValue, Nested } from './conditions'\nimport type { PropertiesFallback } from './csstype'\nimport type { SystemProperties, CssVarProperties } from './style-props'\n\ntype String = string & {}\ntype Number = number & {}\n\nexport type Pretty<T> = { [K in keyof T]: T[K] } & {}\n\nexport type DistributiveOmit<T, K extends keyof any> = T extends unknown ? Omit<T, K> : never\n\nexport type DistributiveUnion<T, U> = {\n [K in keyof T]: K extends keyof U ? U[K] | T[K] : T[K]\n} & DistributiveOmit<U, keyof T>\n\nexport type Assign<T, U> = {\n [K in keyof T]: K extends keyof U ? U[K] : T[K]\n} & U\n\n/* -----------------------------------------------------------------------------\n * Native css properties\n * -----------------------------------------------------------------------------*/\n\nexport type CssProperty = keyof PropertiesFallback\n\nexport interface CssProperties extends PropertiesFallback<String | Number>, CssVarProperties {}\n\nexport interface CssKeyframes {\n [name: string]: {\n [time: string]: CssProperties\n }\n}\n\n/* -----------------------------------------------------------------------------\n * Conditional css properties\n * -----------------------------------------------------------------------------*/\n\ninterface GenericProperties {\n [key: string]: ConditionalValue<String | Number | boolean>\n}\n\n/* -----------------------------------------------------------------------------\n * Native css props\n * -----------------------------------------------------------------------------*/\n\nexport type NestedCssProperties = Nested<CssProperties>\n\nexport type SystemStyleObject = Nested<SystemProperties & CssVarProperties>\n\nexport interface GlobalStyleObject {\n [selector: string]: SystemStyleObject\n}\nexport interface ExtendableGlobalStyleObject {\n [selector: string]: SystemStyleObject | undefined\n extend?: GlobalStyleObject | undefined\n}\n\ntype FilterStyleObject<P extends string> = {\n [K in P]?: K extends keyof SystemStyleObject ? SystemStyleObject[K] : unknown\n}\n\nexport type CompositionStyleObject<Property extends string> = Nested<FilterStyleObject<Property> & CssVarProperties>\n\n/* -----------------------------------------------------------------------------\n * Jsx style props\n * -----------------------------------------------------------------------------*/\ninterface WithCss {\n css?: SystemStyleObject | SystemStyleObject[]\n}\n\nexport type JsxStyleProps = SystemStyleObject & WithCss\n\nexport interface PatchedHTMLProps {\n htmlWidth?: string | number\n htmlHeight?: string | number\n htmlTranslate?: 'yes' | 'no' | undefined\n htmlContent?: string\n}\n\nexport type OmittedHTMLProps = 'color' | 'translate' | 'transition' | 'width' | 'height' | 'content'\n\ntype WithHTMLProps<T> = DistributiveOmit<T, OmittedHTMLProps> & PatchedHTMLProps\n\nexport type JsxHTMLProps<T extends Record<string, any>, P extends Record<string, any> = {}> = Assign<\n WithHTMLProps<T>,\n P\n>\n"
"content": "import type { ConditionalValue, Nested } from './conditions'\nimport type { AtRule, PropertiesFallback } from './csstype'\nimport type { SystemProperties, CssVarProperties } from './style-props'\n\ntype String = string & {}\ntype Number = number & {}\n\nexport type Pretty<T> = { [K in keyof T]: T[K] } & {}\n\nexport type DistributiveOmit<T, K extends keyof any> = T extends unknown ? Omit<T, K> : never\n\nexport type DistributiveUnion<T, U> = {\n [K in keyof T]: K extends keyof U ? U[K] | T[K] : T[K]\n} & DistributiveOmit<U, keyof T>\n\nexport type Assign<T, U> = {\n [K in keyof T]: K extends keyof U ? U[K] : T[K]\n} & U\n\n/* -----------------------------------------------------------------------------\n * Native css properties\n * -----------------------------------------------------------------------------*/\n\nexport type CssProperty = keyof PropertiesFallback\n\nexport interface CssProperties extends PropertiesFallback<String | Number>, CssVarProperties {}\n\nexport interface CssKeyframes {\n [name: string]: {\n [time: string]: CssProperties\n }\n}\n\n/* -----------------------------------------------------------------------------\n * Conditional css properties\n * -----------------------------------------------------------------------------*/\n\ninterface GenericProperties {\n [key: string]: ConditionalValue<String | Number | boolean>\n}\n\n/* -----------------------------------------------------------------------------\n * Native css props\n * -----------------------------------------------------------------------------*/\n\nexport type NestedCssProperties = Nested<CssProperties>\n\nexport type SystemStyleObject = Nested<SystemProperties & CssVarProperties>\n\nexport interface GlobalStyleObject {\n [selector: string]: SystemStyleObject\n}\nexport interface ExtendableGlobalStyleObject {\n [selector: string]: SystemStyleObject | undefined\n extend?: GlobalStyleObject | undefined\n}\n\n/* -----------------------------------------------------------------------------\n * Composition (text styles, layer styles)\n * -----------------------------------------------------------------------------*/\n\ntype FilterStyleObject<P extends string> = {\n [K in P]?: K extends keyof SystemStyleObject ? SystemStyleObject[K] : unknown\n}\n\nexport type CompositionStyleObject<Property extends string> = Nested<FilterStyleObject<Property> & CssVarProperties>\n\n/* -----------------------------------------------------------------------------\n * Font face\n * -----------------------------------------------------------------------------*/\n\nexport type GlobalFontfaceRule = Omit<AtRule.FontFaceFallback, 'src'> & Required<Pick<AtRule.FontFaceFallback, 'src'>>\n\nexport type FontfaceRule = Omit<GlobalFontfaceRule, 'fontFamily'>\n\nexport interface GlobalFontface {\n [name: string]: FontfaceRule | FontfaceRule[]\n}\n\nexport interface ExtendableGlobalFontface {\n [name: string]: FontfaceRule | FontfaceRule[] | GlobalFontface | undefined\n extend?: GlobalFontface | undefined\n}\n\n/* -----------------------------------------------------------------------------\n * Jsx style props\n * -----------------------------------------------------------------------------*/\ninterface WithCss {\n css?: SystemStyleObject | SystemStyleObject[]\n}\n\nexport type JsxStyleProps = SystemStyleObject & WithCss\n\nexport interface PatchedHTMLProps {\n htmlWidth?: string | number\n htmlHeight?: string | number\n htmlTranslate?: 'yes' | 'no' | undefined\n htmlContent?: string\n}\n\nexport type OmittedHTMLProps = 'color' | 'translate' | 'transition' | 'width' | 'height' | 'content'\n\ntype WithHTMLProps<T> = DistributiveOmit<T, OmittedHTMLProps> & PatchedHTMLProps\n\nexport type JsxHTMLProps<T extends Record<string, any>, P extends Record<string, any> = {}> = Assign<\n WithHTMLProps<T>,\n P\n>\n"
}
23 changes: 22 additions & 1 deletion packages/studio/styled-system/types/system-types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import type { ConditionalValue, Nested } from './conditions';
import type { PropertiesFallback } from './csstype';
import type { AtRule, PropertiesFallback } from './csstype';
import type { SystemProperties, CssVarProperties } from './style-props';

type String = string & {}
Expand Down Expand Up @@ -56,12 +56,33 @@ export interface ExtendableGlobalStyleObject {
extend?: GlobalStyleObject | undefined
}

/* -----------------------------------------------------------------------------
* Composition (text styles, layer styles)
* -----------------------------------------------------------------------------*/

type FilterStyleObject<P extends string> = {
[K in P]?: K extends keyof SystemStyleObject ? SystemStyleObject[K] : unknown
}

export type CompositionStyleObject<Property extends string> = Nested<FilterStyleObject<Property> & CssVarProperties>

/* -----------------------------------------------------------------------------
* Font face
* -----------------------------------------------------------------------------*/

export type GlobalFontfaceRule = Omit<AtRule.FontFaceFallback, 'src'> & Required<Pick<AtRule.FontFaceFallback, 'src'>>

export type FontfaceRule = Omit<GlobalFontfaceRule, 'fontFamily'>

export interface GlobalFontface {
[name: string]: FontfaceRule | FontfaceRule[]
}

export interface ExtendableGlobalFontface {
[name: string]: FontfaceRule | FontfaceRule[] | GlobalFontface | undefined
extend?: GlobalFontface | undefined
}

/* -----------------------------------------------------------------------------
* Jsx style props
* -----------------------------------------------------------------------------*/
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/analyze-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ interface FileSizes {
export interface ReportSnapshot {
schemaVersion: string
details: ReportDetails
config: Omit<Config, 'globalCss'>
config: Omit<Config, 'globalCss' | 'globalFontface'>

propByIndex: Map<PropertyReportItem['index'], PropertyReportItem>
componentByIndex: Map<ComponentReportItem['componentIndex'], ComponentReportItem>
Expand Down
Loading

0 comments on commit e952f82

Please sign in to comment.