diff --git a/package.json b/package.json index f251a0fa..bd0366bd 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,8 @@ "@babel/plugin-transform-react-jsx": "^7.24.7", "@babel/types": "^7.24.7", "@biomejs/biome": "^1.8.3", + "@capsizecss/core": "^4.1.2", + "@capsizecss/metrics": "^3.3.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@tokens-studio/sd-transforms": "^1.0.0", diff --git a/src/configs/getWebConfig.ts b/src/configs/getWebConfig.ts index 245c0255..e3338d5e 100644 --- a/src/configs/getWebConfig.ts +++ b/src/configs/getWebConfig.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { camelCase } from "lodash-es"; +import StyleDictionary from "style-dictionary"; import type { File, PlatformConfig } from "style-dictionary/types"; import type { Theme } from "../@types"; import { isCoreColor } from "../filters/isCoreColor"; @@ -25,9 +26,22 @@ import { type Tier, cssFileName, } from "../utils/cssFileName"; +import { fontFaces, fontFamilyOverrides } from "../utils/fontFallbacks"; const basePxFontSize = 16; +StyleDictionary.registerFormat({ + name: "css/fontFallbacks", + format: () => fontFaces, +}); + +StyleDictionary.registerTransform({ + name: "css/fontFallbacks", + type: "value", + filter: (token) => token.type === "fontFamilies", + transform: (token) => fontFamilyOverrides[token.value] ?? token.value, +}); + export default function ( target: "js" | "css" | "ts", theme: Theme, @@ -42,6 +56,7 @@ export default function ( "attribute/cti", "css/pxToRem", "css/percentageToUnitless", + "css/fontFallbacks", target === "css" ? "name/kebab" : "camelCaseDecimal", ]; @@ -115,6 +130,11 @@ function getFilesFormat(theme: Theme, target: "css" | "js" | "ts"): File[] { }, }); + const fontFaces = { + destination: `${COMPOUND_TOKENS_NAMESPACE}-font-fallbacks.css`, + format: "css/fontFallbacks", + }; + return [ common("base"), common("semantic"), @@ -126,5 +146,6 @@ function getFilesFormat(theme: Theme, target: "css" | "js" | "ts"): File[] { // This file is to be imported with a media query import themed("base", true), themed("semantic", true), + fontFaces, ]; } diff --git a/src/utils/fontFallbacks.ts b/src/utils/fontFallbacks.ts new file mode 100644 index 00000000..ada01e73 --- /dev/null +++ b/src/utils/fontFallbacks.ts @@ -0,0 +1,221 @@ +import { createFontStack } from "@capsizecss/core"; + +// The goal of this is to generate @font-face rules to fallback to local fonts, +// but with font metrics adjusted to match the ones of the font we actually use. +// This way, we can display temporarily a local sans-serif font, laid out very +// close to what Inter will look like once we actually loaded it. +// +// Out of this file, we get two exports: +// - fontFamilyOverrides: a mapping from font we have in tokens to a font-family +// which has all the overrides +// - fontFaces: the @font-face rules to inject + +// Inter: the font we actually use +import inter500 from "@capsizecss/metrics/inter/500"; +import inter500Italic from "@capsizecss/metrics/inter/500italic"; +import inter600 from "@capsizecss/metrics/inter/600"; +import inter600Italic from "@capsizecss/metrics/inter/600italic"; +import inter400Italic from "@capsizecss/metrics/inter/italic"; +import inter400 from "@capsizecss/metrics/inter/regular"; + +// Roboto +import roboto500 from "@capsizecss/metrics/roboto/500"; +import roboto500Italic from "@capsizecss/metrics/roboto/500italic"; +import roboto700 from "@capsizecss/metrics/roboto/700"; +import roboto700Italic from "@capsizecss/metrics/roboto/700italic"; +import roboto400Italic from "@capsizecss/metrics/roboto/italic"; +import roboto400 from "@capsizecss/metrics/roboto/regular"; + +// Segoe UI +import segoeUI600 from "@capsizecss/metrics/segoeUI/600"; +import segoeUI600Italic from "@capsizecss/metrics/segoeUI/600italic"; +import segoeUI700 from "@capsizecss/metrics/segoeUI/700"; +import segoeUI700Italic from "@capsizecss/metrics/segoeUI/700italic"; +import segoeUI400Italic from "@capsizecss/metrics/segoeUI/italic"; +import segoeUI400 from "@capsizecss/metrics/segoeUI/regular"; + +// Helvetica Neue +import helveticaNeue500 from "@capsizecss/metrics/helveticaNeue/500"; +import helveticaNeue500Italic from "@capsizecss/metrics/helveticaNeue/500italic"; +import helveticaNeue700 from "@capsizecss/metrics/helveticaNeue/700"; +import helveticaNeue700Italic from "@capsizecss/metrics/helveticaNeue/700italic"; +import helveticaNeue400Italic from "@capsizecss/metrics/helveticaNeue/italic"; +import helveticaNeue400 from "@capsizecss/metrics/helveticaNeue/regular"; + +// Ubuntu +import ubuntu500 from "@capsizecss/metrics/ubuntu/500"; +import ubuntu500Italic from "@capsizecss/metrics/ubuntu/500italic"; +import ubuntu700 from "@capsizecss/metrics/ubuntu/700"; +import ubuntu700Italic from "@capsizecss/metrics/ubuntu/700italic"; +import ubuntu400Italic from "@capsizecss/metrics/ubuntu/italic"; +import ubuntu400 from "@capsizecss/metrics/ubuntu/regular"; + +// Fira Sans +import firaSans500 from "@capsizecss/metrics/firaSans/500"; +import firaSans500Italic from "@capsizecss/metrics/firaSans/500italic"; +import firaSans600 from "@capsizecss/metrics/firaSans/600"; +import firaSans600Italic from "@capsizecss/metrics/firaSans/600italic"; +import firaSans400Italic from "@capsizecss/metrics/firaSans/italic"; +import firaSans400 from "@capsizecss/metrics/firaSans/regular"; + +// Noto Sans +import notoSans500 from "@capsizecss/metrics/notoSans/500"; +import notoSans500Italic from "@capsizecss/metrics/notoSans/500italic"; +import notoSans600 from "@capsizecss/metrics/notoSans/600"; +import notoSans600Italic from "@capsizecss/metrics/notoSans/600italic"; +import notoSans400Italic from "@capsizecss/metrics/notoSans/italic"; +import notoSans400 from "@capsizecss/metrics/notoSans/regular"; + +// The ultimate sans-serif fallback: Arial +import arial700 from "@capsizecss/metrics/arial/700"; +import arial700Italic from "@capsizecss/metrics/arial/700italic"; +import arial400Italic from "@capsizecss/metrics/arial/italic"; +import arial400 from "@capsizecss/metrics/arial/regular"; + +const { fontFamily: interFontFamily, fontFaces: inter400Stack } = + createFontStack( + [ + inter400, + helveticaNeue400, + segoeUI400, + roboto400, + ubuntu400, + firaSans400, + notoSans400, + arial400, + ], + { + fontFaceProperties: { + fontStyle: "normal", + fontWeight: 400, + fontDisplay: "swap", + }, + }, + ); + +const { fontFaces: inter400ItalicStack } = createFontStack( + [ + inter400Italic, + helveticaNeue400Italic, + segoeUI400Italic, + roboto400Italic, + ubuntu400Italic, + firaSans400Italic, + notoSans400Italic, + arial400Italic, + ], + { + fontFaceProperties: { + fontStyle: "italic", + fontWeight: 400, + fontDisplay: "swap", + }, + }, +); + +const { fontFaces: inter500Stack } = createFontStack( + [ + inter500, + helveticaNeue500, + segoeUI600, + roboto500, + ubuntu500, + firaSans500, + notoSans500, + ], + { + fontFaceProperties: { + fontStyle: "normal", + fontWeight: 500, + fontDisplay: "swap", + }, + }, +); + +const { fontFaces: inter500ItalicStack } = createFontStack( + [ + inter500Italic, + helveticaNeue500Italic, + segoeUI600Italic, + roboto500Italic, + ubuntu500Italic, + firaSans500Italic, + notoSans500Italic, + ], + { + fontFaceProperties: { + fontStyle: "italic", + fontWeight: 500, + fontDisplay: "swap", + }, + }, +); + +const { fontFaces: inter600Stack } = createFontStack( + [ + inter600, + helveticaNeue700, + segoeUI700, + roboto700, + ubuntu700, + firaSans600, + notoSans600, + arial700, + ], + { + fontFaceProperties: { + fontStyle: "normal", + fontWeight: 600, + fontDisplay: "swap", + }, + }, +); + +const { fontFaces: inter600ItalicStack } = createFontStack( + [ + inter600Italic, + helveticaNeue700Italic, + segoeUI700Italic, + roboto700Italic, + ubuntu700Italic, + firaSans600Italic, + notoSans600Italic, + arial700Italic, + ], + { + fontFaceProperties: { + fontStyle: "italic", + fontWeight: 600, + fontDisplay: "swap", + }, + }, +); + +// The font token -> font-family mapping used to override +export const fontFamilyOverrides: Record = { + Inter: `${interFontFamily}, sans-serif`, + + // We don't bother to compute accurate fallbacks for the monospace font for + // now, as we don't use it as much, but we make sure we have at least a + // cross-browser fallback for it + Inconsolata: "Inconsolata, ui-monospace, monospace", +}; + +// The CSS @font-face rules to inject +export const fontFaces = `/* Fallback for Inter regular */ +${inter400Stack} + +${inter400ItalicStack} + + +/* Fallback for Inter medium */ +${inter500Stack} + +${inter500ItalicStack} + + +/* Fallback for Inter semibold */ +${inter600Stack} + +${inter600ItalicStack} +`; diff --git a/src/utils/generateCssIndex.ts b/src/utils/generateCssIndex.ts index 7ebafd0e..13ccffa5 100644 --- a/src/utils/generateCssIndex.ts +++ b/src/utils/generateCssIndex.ts @@ -17,7 +17,11 @@ limitations under the License. import path from "node:path"; import fs from "fs-extra"; import type { Theme } from "../@types"; -import { type Tier, cssFileName } from "./cssFileName"; +import { + COMPOUND_TOKENS_NAMESPACE, + type Tier, + cssFileName, +} from "./cssFileName"; const header = `/* Establish a layer order that allows semantic tokens to be customized, but not base tokens. * The layers are prefixed by 'cpd-' because Tailwind will interpret '@layer base' directives. @@ -29,6 +33,7 @@ const tiers: Tier[] = ["base", "semantic"]; export function generateCssIndex(): void { const imports = [ + `@import url("./${COMPOUND_TOKENS_NAMESPACE}-font-fallbacks.css");`, ...(function* () { for (const theme of themes) { for (const tier of tiers) { diff --git a/yarn.lock b/yarn.lock index 2d3d7ae7..6ed07b67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -315,6 +315,18 @@ dependencies: postcss-calc-ast-parser "^0.1.4" +"@capsizecss/core@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@capsizecss/core/-/core-4.1.2.tgz#a278e19953ed4922653d2c2a7614915fd8fc5f24" + integrity sha512-5tMjLsVsaEEwJ816y3eTfhhTIyUWNFt58x6YcHni0eV5tta8MGDOAIe+CV5ICb5pguXgDpNGLprqhPqBWtkFSg== + dependencies: + csstype "^3.1.1" + +"@capsizecss/metrics@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@capsizecss/metrics/-/metrics-3.3.0.tgz#bbbe3df4cd5acedbb605757ae0ba440cf7ea9d17" + integrity sha512-WAQtKgyz7fZDEMuERSLPmWXuV53O/HDJZLof8BMWEX1GTWYiiNdqGA6j56+GCSSeVyzYDxkBnm5taIh0YyW6fQ== + "@esbuild/aix-ppc64@0.23.1": version "0.23.1" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353" @@ -1031,6 +1043,11 @@ csso@^4.2.0: dependencies: css-tree "^1.1.2" +csstype@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"