From 013d6d25d6b64481fd7b71aae0879e73c27416a6 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Wed, 18 Mar 2026 13:29:44 +0100 Subject: [PATCH 1/4] feat: more configurable typography --- designsystemet.config.json | 78 +++- packages/cli/bin/config.ts | 14 +- packages/cli/bin/designsystemet.ts | 20 +- packages/cli/src/config.ts | 109 ++++- packages/cli/src/index.ts | 2 +- packages/cli/src/scripts/createJsonSchema.ts | 4 +- packages/cli/src/tokens/create.ts | 45 +- .../src/tokens/create/generators/$metadata.ts | 24 +- .../src/tokens/create/generators/$themes.ts | 75 ++-- .../generators/primitives/typography.ts | 396 +++++++++--------- .../create/generators/semantic/style.ts | 233 +---------- packages/cli/src/tokens/types.ts | 15 + 12 files changed, 497 insertions(+), 518 deletions(-) diff --git a/designsystemet.config.json b/designsystemet.config.json index 0f399be298..36d327a9a8 100644 --- a/designsystemet.config.json +++ b/designsystemet.config.json @@ -16,7 +16,83 @@ }, "borderRadius": 4, "typography": { - "fontFamily": "Inter" + "fonts": { + "primary": { + "fontFamily": "Inter", + "fontWeight": { + "regular": "Regular", + "medium": "Medium", + "semibold": "Semi bold" + } + }, + "secondary": { + "fontFamily": "Playfair Display", + "fontWeight": { + "regular": "Regular", + "medium": "Medium", + "semibold": "SemiBold" + }, + "size": { + "small": { "base": 13 }, + "medium": { "base": 15 }, + "large": { "base": 18 } + } + } + }, + "components": { + "heading": { + "font": "secondary" + }, + "body": { + "font": "primary" + } + } + } + }, + "test-different-fonts": { + "colors": { + "main": { + "accent": "#0062BA" + }, + "support": { + "brand1": "#0D7A5F", + "brand2": "#5B3FA0" + }, + "neutral": "#24272B" + }, + "borderRadius": 4, + "typography": { + "fonts": { + "main": { + "fontFamily": "Playpen Sans", + "fontWeight": { + "regular": "Regular", + "medium": "Medium", + "semibold": "SemiBold" + }, + "size": { + "small": { "base": 14 }, + "medium": { "base": 16 }, + "large": { "base": 18 } + } + }, + "headings": { + "fontFamily": "Karantina", + "fontWeight": { + "regular": "Light", + "medium": "Regular", + "semibold": "Bold" + } + } + }, + "components": { + "heading": { + "font": "headings" + }, + "body": { + "font": "main" + } + } } } } diff --git a/packages/cli/bin/config.ts b/packages/cli/bin/config.ts index a95fd713a2..c0cc79f8d3 100644 --- a/packages/cli/bin/config.ts +++ b/packages/cli/bin/config.ts @@ -4,7 +4,7 @@ import * as R from 'ramda'; import { type BuildConfigSchema, type CreateConfigSchema, - commonConfig, + configFileBuildSchema, configFileCreateSchema, parseConfig, validateConfig, @@ -27,6 +27,8 @@ export async function readConfigFile(configFilePath: string, allowFileNotFound = if (configFile) { console.log(`Found config file: ${pc.green(configFilePath)}`); + } else { + console.log(pc.yellow('No config file found, using default settings')); } return configFile; @@ -79,8 +81,8 @@ export async function parseCreateConfig( const unvalidatedConfig = noUndefined({ outDir: configParsed?.outDir ?? getCliOption(cmd, 'outDir'), - clean: configParsed?.clean ?? getCliOption(cmd, 'clean'), - themes: configParsed?.themes + clean: configParsed?.clean ?? (getCliOption(cmd, 'clean') as boolean), + themes: (configParsed?.themes ? R.map((jsonThemeValues) => { // For each theme specified in the JSON config, we resolve the option values in the following order: // - default value @@ -96,8 +98,8 @@ export async function parseCreateConfig( // and default theme options from the CLI. { [theme]: getThemeOptions(getCliOption), - }, - }); + }) as CreateConfigSchema['themes'], + } satisfies CreateConfigSchema); return validateConfig(configFileCreateSchema, unvalidatedConfig, configFilePath); } @@ -108,5 +110,5 @@ export async function parseBuildConfig( ): Promise { const configParsed: BuildConfigSchema = parseConfig(configFile, configFilePath); - return validateConfig(commonConfig, configParsed, configFilePath); + return validateConfig(configFileBuildSchema, configParsed, configFilePath); } diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index e340c02a1d..c301277814 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -5,6 +5,7 @@ import pc from 'picocolors'; import * as R from 'ramda'; import { convertToHex } from '../src/colors/index.js'; import type { CssColor } from '../src/colors/types.js'; +import { type CreateConfigSchema, parseConfig } from '../src/config.js'; import migrations from '../src/migrations/index.js'; import { buildTokens } from '../src/tokens/build.js'; import { createTokenFiles } from '../src/tokens/create/files.js'; @@ -12,7 +13,7 @@ import { cliOptions, createTokens } from '../src/tokens/create.js'; import { generateConfigFromTokens } from '../src/tokens/generate-config.js'; import type { OutputFile, Theme } from '../src/tokens/types.js'; import { dsfs } from '../src/utils/filesystem.js'; -import { parseCreateConfig, readConfigFile } from './config.js'; +import { parseBuildConfig, parseCreateConfig, readConfigFile } from './config.js'; export const figletAscii = ` _____ _ _ _ @@ -54,6 +55,17 @@ function makeTokenCommands() { console.log(figletAscii); const { verbose, clean, dry, experimentalTailwind, tokens } = opts; + const { configFile, configFilePath } = await getConfigFile(opts.config); + const config = await parseBuildConfig(configFile, { configFilePath }); + + // Hacky: Find any font size overrides from the create config. + // This only works because these settings can't be passed as CLI options + const typographySizeOverrides = Object.values(parseConfig(configFile, configFilePath).themes) + .flatMap((x) => x.typography) + .flatMap((x) => Object.values(x?.fonts ?? [])) + .flatMap((x) => Object.values(x.size ?? [])) + .flatMap((x) => Object.values(x.overrides ?? [])); + // TODO - add outdir eqivalent to config option when parsing config, so that it can be set in the config file as well. buildDir? dsfs.init({ dry, outdir: opts.outDir, verbose }); @@ -70,6 +82,12 @@ function makeTokenCommands() { tailwind: experimentalTailwind, }); + if (typographySizeOverrides.length > 0) { + // If typography sizes have been overridden with explicit values, we can't use modular formulae + config.build = config.build ?? {}; + config.build.typographySizeValues = 'static'; + } + console.log(`\nšŸ’¾ Writing build to ${pc.green(outDir)}`); await dsfs.writeFiles(files, outDir, true); diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 05ceaf28eb..ba1444ba11 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -161,6 +161,55 @@ const focusOverrideSchema = z }) .describe('Overrides for the focus colors'); +const fontSizeSteps = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13'] as const; +const fontSizeStepOverrideSchema = z + .number() + .describe('Number in pixels to use as a font size for this step in the scale'); +const fontSizeOverrides = z.partialRecord(z.enum(fontSizeSteps), fontSizeStepOverrideSchema); + +const fontSizeMode = z.object({ + base: z + .number() + .optional() + .describe('The font size (in px) to use as the basis for the scale. Is used as font-size.4.'), + ratio: z + .number() + .optional() + .describe( + 'The ratio used to calculate each step in the scale. Must be larger than 1 (a ratio of 1 would make all font sizes the same, while a number between 0 and 1 effectively inverts the scale). Larger numbers result in a larger font-size.10 and a smaller font-size.1.', + ), + overrides: fontSizeOverrides + .optional() + .describe( + 'Override one or more steps in this scale to specific pixel values. Will force build.typographySizeValues to be "static".', + ), +}); + +const typographySizeSchema = z + .partialRecord(z.enum(['small', 'medium', 'large']), fontSizeMode) + .describe('Sizing configuration for the individual size modes'); + +const typographyComponentsSchema = z.object({ + heading: z + .object({ + font: z + .string() + .describe( + 'Define which font to use for heading styles. Must be one of the under theme..typography.fonts', + ), + }) + .optional(), + body: z + .object({ + font: z + .string() + .describe( + 'Define which font to use for body text styles. Must be one of the under theme..typography.fonts', + ), + }) + .optional(), +}); + const overridesSchema = z .object({ colors: semanticColorOverrideSchema.optional(), @@ -171,6 +220,17 @@ const overridesSchema = z .describe('Overrides for generated design tokens. Currently only supports colors defined in your theme') .optional(); +const typographyFontSchema = z.object({ + fontFamily: z.string().describe('Sets the font-family for this font'), + fontWeight: z + .record( + z.enum(['regular', 'medium', 'semibold']), + z.string().describe('The name of the weight as displayed in Figma'), + ) + .describe('Sets the font-weights for this font'), + size: typographySizeSchema.optional().describe('Configure sizing for this font'), +}); + const themeSchema = z .object({ colors: z @@ -182,7 +242,14 @@ const themeSchema = z .meta({ description: 'Defines the colors for this theme' }), typography: z .object({ - fontFamily: z.string().meta({ description: 'Sets the font-family for this theme' }), + fontFamily: z.string().describe('DEPRECATED! Use fonts..fontFamily instead.').optional(), + fonts: z + .record(z.string(), typographyFontSchema) + .describe('Define fonts that can be used in the theme') + .optional(), + components: typographyComponentsSchema + .describe('Define which fonts to use for each typography style') + .optional(), }) .describe('Defines the typography for a given theme') .optional(), @@ -191,26 +258,42 @@ const themeSchema = z }) .meta({ description: 'An object defining a theme. The property name holding the object becomes the theme name.' }); -export const commonConfig = z.object({ +const commonConfig = z.object({ clean: z.boolean().meta({ description: 'Delete the output directory before building or creating tokens' }).optional(), }); -const _configFileCreateSchema = z - .object({ - outDir: z.string().meta({ description: 'Path to the output directory for the created design tokens' }), - themes: z.record(z.string(), themeSchema).meta({ - description: - 'An object with one or more themes. Each property defines a theme, and the property name is used as the theme name.', - }), - }) - .required(); +const _configFileCreateSchema = z.object({ + outDir: z.string().meta({ description: 'Path to the output directory for the created design tokens' }), + themes: z.record(z.string(), themeSchema).meta({ + description: + 'An object with one or more themes. Each property defines a theme, and the property name is used as the theme name.', + }), +}); +const _configFileBuildSchema = z.object({ + build: z + .object({ + typographySizeValues: z + .enum(['modular', 'static']) + .optional() + .describe( + 'Changes how CSS values are generated. "modular" is the default, and will output css formulae which can be changed using --ds-font-scale-base and --ds-font-scale-ratio in code. "static" will output static values for each size mode. If you have overridden any steps in the font size scales with specific values, "static" will always be used.', + ), + }) + .optional() + .describe('Options that only affect build'), +}); + +export const configFileCreateSchema = _configFileCreateSchema.extend(commonConfig.shape); +export const configFileBuildSchema = _configFileBuildSchema.extend(commonConfig.shape); /** * This defines the structure of the final configuration file */ -export const configFileCreateSchema = _configFileCreateSchema.extend(commonConfig.shape); +export const configFileSchema = configFileCreateSchema.extend(configFileBuildSchema.shape); export type CommonConfigSchema = z.infer; -export type BuildConfigSchema = z.infer; +export type BuildConfigSchema = z.infer; export type CreateConfigSchema = z.infer; export type ConfigSchemaTheme = z.infer; export type ColorOverrideSchema = z.infer; +export type TypographySizeSchema = z.infer; +export type TypographyFontSchema = z.infer; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 18ca4efe5c..5faebdb7f5 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,6 +1,6 @@ export * from './colors/index.js'; export { type CreateConfigSchema as ConfigSchema, - configFileCreateSchema as configSchema, + configFileSchema as configSchema, } from './config.js'; export * from './tokens/index.js'; diff --git a/packages/cli/src/scripts/createJsonSchema.ts b/packages/cli/src/scripts/createJsonSchema.ts index 82aac35595..5c5b2aa218 100644 --- a/packages/cli/src/scripts/createJsonSchema.ts +++ b/packages/cli/src/scripts/createJsonSchema.ts @@ -1,13 +1,13 @@ import { writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { z } from 'zod'; -import { configFileCreateSchema } from '../config.js'; +import { configFileSchema } from '../config.js'; const schema = z .object({ $schema: z.string().optional(), }) - .extend(configFileCreateSchema.shape); + .extend(configFileSchema.shape); writeFile( resolve(import.meta.dirname, '../../dist/config.schema.json'), diff --git a/packages/cli/src/tokens/create.ts b/packages/cli/src/tokens/create.ts index bdae78787d..ca93b0d4e4 100644 --- a/packages/cli/src/tokens/create.ts +++ b/packages/cli/src/tokens/create.ts @@ -2,7 +2,11 @@ import type { ColorScheme } from '../colors/types.js'; import { generateColorScheme } from './create/generators/primitives/color-scheme.js'; import { generateGlobals } from './create/generators/primitives/globals.js'; import { generateSize, generateSizeGlobal } from './create/generators/primitives/size.js'; -import { generateFontSizes, generateTypography } from './create/generators/primitives/typography.js'; +import { + generateFont, + generateFontSizeGlobal, + generateFontSizeMode, +} from './create/generators/primitives/typography.js'; import { generateSemanticColors } from './create/generators/semantic/color.js'; import { generateColorModes } from './create/generators/semantic/color-modes.js'; import { generateSemanticStyle } from './create/generators/semantic/style.js'; @@ -31,16 +35,41 @@ export const createTokens = async (theme: Theme) => { const colorSchemes: ColorScheme[] = ['light', 'dark']; const sizeModes: SizeModes[] = ['small', 'medium', 'large']; + // TODO handle default font definition somewhere else + const fontDefinitions = typography.fonts ?? { + primary: { + // Use deprecated typography.fontFamily as fallback if no typography.fonts is defined + fontFamily: typography.fontFamily ?? 'Inter', + fontWeight: { + regular: 'Regular', + medium: 'Medium', + semibold: 'Semi bold', + }, + }, + }; + + const fontNames = Object.keys(fontDefinitions); + const tokenSets: TokenSets = new Map([ ['primitives/globals', generateGlobals()], - ...sizeModes.map((size): [string, TokenSet] => [`primitives/modes/size/${size}`, generateSize(size)]), ['primitives/modes/size/global', generateSizeGlobal()], - ...sizeModes.map((size): [string, TokenSet] => [ - `primitives/modes/typography/size/${size}`, - generateFontSizes(size), + ...sizeModes.map((size): [string, TokenSet] => [`primitives/modes/size/${size}`, generateSize(size)]), + ...fontNames.map((font): [string, TokenSet] => [ + `primitives/modes/size/global/${name}/font-${font}`, + generateFontSizeGlobal(name, font), + ]), + ...fontNames.flatMap((font): [string, TokenSet][] => + sizeModes.map((size) => [ + `primitives/modes/size/${size}/${name}/font-${font}`, + generateFontSizeMode(size, name, font, typography.fonts?.[font]?.size), + ]), + ), + ...fontNames.map((font): [string, TokenSet] => [ + `primitives/fonts/${name}/${font}`, + generateFont(name, font, fontDefinitions[font]), ]), - [`primitives/modes/typography/primary/${name}`, generateTypography(name, typography)], - [`primitives/modes/typography/secondary/${name}`, generateTypography(name, typography)], + // [`primitives/modes/typography/primary/${name}`, generateTypography(name, typography)], + // [`primitives/modes/typography/secondary/${name}`, generateTypography(name, typography)], ...colorSchemes.flatMap((scheme): [string, TokenSet][] => [ [`primitives/modes/color-scheme/${scheme}/${name}`, generateColorScheme(name, scheme, colors, overrides)], ]), @@ -53,5 +82,5 @@ export const createTokens = async (theme: Theme) => { [`semantic/style`, generateSemanticStyle()], ]); - return { tokenSets }; + return { tokenSets, themeDimensions: { colorSchemes, fontNames, sizeModes } }; }; diff --git a/packages/cli/src/tokens/create/generators/$metadata.ts b/packages/cli/src/tokens/create/generators/$metadata.ts index ece7b3236f..8f64d56e51 100644 --- a/packages/cli/src/tokens/create/generators/$metadata.ts +++ b/packages/cli/src/tokens/create/generators/$metadata.ts @@ -1,25 +1,29 @@ -import type { ColorScheme } from '../../../colors/types.js'; -import type { Colors, SizeModes } from '../../types.js'; +import type { Colors, TokenSetDimensionsForAllThemes } from '../../types.js'; type Metadata = { tokenSetOrder: string[]; }; export function generate$Metadata( - schemes: ColorScheme[], + dimensions: TokenSetDimensionsForAllThemes, themes: string[], colors: Colors, - sizeModes: SizeModes[], ): Metadata { + const { colorSchemes, sizeModes, fontNamesPerTheme } = dimensions; + const sizesAndGlobal = ['global', ...sizeModes]; return { tokenSetOrder: [ 'primitives/globals', - ...sizeModes.map((size) => `primitives/modes/size/${size}`), - 'primitives/modes/size/global', - ...sizeModes.map((size) => `primitives/modes/typography/size/${size}`), - ...themes.map((theme) => `primitives/modes/typography/primary/${theme}`), - ...themes.map((theme) => `primitives/modes/typography/secondary/${theme}`), - ...schemes.flatMap((scheme) => [...themes.map((theme) => `primitives/modes/color-scheme/${scheme}/${theme}`)]), + ...sizesAndGlobal.map((size) => `primitives/modes/size/${size}`), + ...sizesAndGlobal.flatMap((size) => + themes.flatMap((theme) => + fontNamesPerTheme[theme].map((font) => `primitives/modes/size/${size}/${theme}/font-${font}`), + ), + ), + ...themes.flatMap((theme) => fontNamesPerTheme[theme].map((font) => `primitives/fonts/${theme}/${font}`)), + ...colorSchemes.flatMap((scheme) => [ + ...themes.map((theme) => `primitives/modes/color-scheme/${scheme}/${theme}`), + ]), ...themes.map((theme) => `themes/${theme}`), 'semantic/color', ...Object.entries(colors.main).map(([color]) => `semantic/modes/main-color/${color}`), diff --git a/packages/cli/src/tokens/create/generators/$themes.ts b/packages/cli/src/tokens/create/generators/$themes.ts index c86c7aa5e3..85d8794d7e 100644 --- a/packages/cli/src/tokens/create/generators/$themes.ts +++ b/packages/cli/src/tokens/create/generators/$themes.ts @@ -1,7 +1,9 @@ import { type ThemeObject, TokenSetStatus } from '@tokens-studio/types'; import type { ColorScheme } from '../../../colors/types.js'; -import type { Colors, SizeModes } from '../../types.js'; +import type { Colors, SizeModes, TokenSetDimensionsForAllThemes } from '../../types.js'; + +type FontsPerTheme = TokenSetDimensionsForAllThemes['fontNamesPerTheme']; const capitalize = (word: string) => word.charAt(0).toUpperCase() + word.slice(1); @@ -36,13 +38,13 @@ type ThemeObject_ = ThemeObject & { */ export async function generate$Themes( - colorSchemes: ColorSchemes, + tokenSetDimensions: TokenSetDimensionsForAllThemes, themes: string[], colors: Colors, - sizeModes: SizeModes[], ): Promise { + const { colorSchemes, fontNamesPerTheme, sizeModes } = tokenSetDimensions; return [ - ...generateSizeGroup(sizeModes), + ...generateSizeGroup(themes, fontNamesPerTheme, sizeModes), ...(await generateThemesGroup(themes)), ...generateTypographyGroup(themes), ...generateColorSchemesGroup(colorSchemes, themes), @@ -52,48 +54,47 @@ export async function generate$Themes( ]; } -function generateSizeGroup(_sizes: SizeModes[]): ThemeObject_[] { - return [ - { +function generateSizeGroup(themes: string[], fonts: FontsPerTheme, sizeModes: SizeModes[]): ThemeObject_[] { + const defaultSize = 'medium'; + const sizesWithDefaultFirst = [ + ...sizeModes.filter((x) => x === defaultSize), + ...sizeModes.filter((x) => x !== defaultSize), + ]; + const existingFigmaIds = { + small: { id: '8b2c8cc86611a34b135cb22948666779361fd729', - name: 'medium', - $figmaStyleReferences: {}, - selectedTokenSets: { - 'primitives/modes/size/medium': TokenSetStatus.SOURCE, - 'primitives/modes/size/global': TokenSetStatus.ENABLED, - 'primitives/modes/typography/size/medium': TokenSetStatus.ENABLED, - }, + $figmaCollectionId: 'VariableCollectionId:36248:20757', + $figmaModeId: '41630:3', + }, + medium: { + id: 'fb11567729c298ca37c9da4e3a27716a23480824', $figmaCollectionId: 'VariableCollectionId:36248:20757', $figmaModeId: '41630:1', - group: 'Size', }, - { + large: { id: 'd49b9eebeb48a4f165a74b7261733d0a73370f0e', - name: 'large', - $figmaStyleReferences: {}, - selectedTokenSets: { - 'primitives/modes/size/large': TokenSetStatus.SOURCE, - 'primitives/modes/size/global': TokenSetStatus.ENABLED, - 'primitives/modes/typography/size/large': TokenSetStatus.ENABLED, - }, $figmaCollectionId: 'VariableCollectionId:36248:20757', $figmaModeId: '41630:2', - group: 'Size', }, - { - id: 'fb11567729c298ca37c9da4e3a27716a23480824', - name: 'small', - $figmaStyleReferences: {}, - selectedTokenSets: { - 'primitives/modes/size/small': TokenSetStatus.SOURCE, - 'primitives/modes/size/global': TokenSetStatus.ENABLED, - 'primitives/modes/typography/size/small': TokenSetStatus.ENABLED, - }, - $figmaCollectionId: 'VariableCollectionId:36248:20757', - $figmaModeId: '41630:3', - group: 'Size', + }; + return sizesWithDefaultFirst.map((size) => ({ + name: size, + group: 'Size', + selectedTokenSets: { + [`primitives/modes/size/${size}`]: TokenSetStatus.SOURCE, + 'primitives/modes/size/global': TokenSetStatus.ENABLED, + ...Object.fromEntries( + themes.flatMap((theme) => + fonts[theme].flatMap((font) => [ + [`primitives/modes/size/global/${theme}/font-${font}`, TokenSetStatus.ENABLED], + [`primitives/modes/size/${size}/${theme}/font-${font}`, TokenSetStatus.ENABLED], + ]), + ), + ), }, - ]; + ...existingFigmaIds[size], + $figmaStyleReferences: {}, + })); } const colorSchemeDefaults: Record = { diff --git a/packages/cli/src/tokens/create/generators/primitives/typography.ts b/packages/cli/src/tokens/create/generators/primitives/typography.ts index aaddafc161..1a307b59e6 100644 --- a/packages/cli/src/tokens/create/generators/primitives/typography.ts +++ b/packages/cli/src/tokens/create/generators/primitives/typography.ts @@ -1,221 +1,201 @@ -import type { SizeModes, TokenSet, Typography } from '../../../types.js'; +import * as R from 'ramda'; +import type { TypographyFontSchema, TypographySizeSchema } from '../../../../config.js'; +import type { Token, TokenSet } from '../../../types.js'; -export const generateTypography = (themeName: string, { fontFamily }: Typography): TokenSet => ({ - [themeName]: { - 'font-family': { - $type: 'fontFamilies', - $value: fontFamily, - }, - 'font-weight': { - medium: { - $type: 'fontWeights', - $value: 'Medium', - }, - semibold: { - $type: 'fontWeights', - $value: 'Semi bold', - }, - regular: { - $type: 'fontWeights', - $value: 'Regular', - }, - }, - }, -}); - -export const generateFontSizes = (size: SizeModes): TokenSet => fontSizes[size]; - -const lineHeights = { - sm: { - $type: 'lineHeights', - $value: '130%', - }, - md: { - $type: 'lineHeights', - $value: '150%', - }, - lg: { - $type: 'lineHeights', - $value: '170%', - }, -}; - -const letterSpacings = { - '1': { - $type: 'letterSpacing', - $value: '-1%', - }, - '2': { - $type: 'letterSpacing', - $value: '-0.5%', - }, - '3': { - $type: 'letterSpacing', - $value: '-0.25%', - }, - '4': { - $type: 'letterSpacing', - $value: '-0.15%', - }, - '5': { - $type: 'letterSpacing', - $value: '0%', - }, - '6': { - $type: 'letterSpacing', - $value: '0.15%', - }, - '7': { - $type: 'letterSpacing', - $value: '0.25%', +const defaults = { + small: { + base: 16, + ratio: 1.14234, }, - '8': { - $type: 'letterSpacing', - $value: '0.5%', + medium: { + base: 18, + ratio: 1.143136, }, - '9': { - $type: 'letterSpacing', - $value: '1.5%', + large: { + base: 21, + ratio: 1.143136, }, }; -const fontSizes = { - large: { - 'line-height': lineHeights, - 'font-size': { - '1': { - $type: 'fontSizes', - $value: '13', - }, - '2': { - $type: 'fontSizes', - $value: '16', - }, - '3': { - $type: 'fontSizes', - $value: '18', - }, - '4': { - $type: 'fontSizes', - $value: '21', - }, - '5': { - $type: 'fontSizes', - $value: '24', - }, - '6': { - $type: 'fontSizes', - $value: '30', - }, - '7': { - $type: 'fontSizes', - $value: '36', - }, - '8': { - $type: 'fontSizes', - $value: '48', - }, - '9': { - $type: 'fontSizes', - $value: '60', - }, - '10': { - $type: 'fontSizes', - $value: '72', +export function generateFontSizeGlobal(themeName: string, fontName: string) { + return { + [themeName]: { + fonts: { + [fontName]: { + 'font-size': { + '1': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} / pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 3), 0)`, + }, + '2': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} / pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 2), 0)`, + }, + '3': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} / pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 1), 0)`, + }, + '4': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 0), 0)`, + }, + '5': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 1), 0)`, + }, + '6': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 2), 0)`, + }, + '7': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 3), 0)`, + }, + '8': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 4), 0)`, + }, + '9': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 5), 0)`, + }, + '10': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 6), 0)`, + }, + '11': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 7), 0)`, + }, + '12': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 8), 0)`, + }, + '13': { + $type: 'fontSizes', + $value: `roundTo({${themeName}.fonts.${fontName}.font-scale._base} * pow({${themeName}.fonts.${fontName}.font-scale._ratio}, 9), 0)`, + }, + }, + }, }, }, - 'letter-spacing': letterSpacings, - }, - medium: { - 'line-height': lineHeights, - 'font-size': { - '1': { - $type: 'fontSizes', - $value: '12', - }, - '2': { - $type: 'fontSizes', - $value: '14', - }, - '3': { - $type: 'fontSizes', - $value: '16', - }, - '4': { - $type: 'fontSizes', - $value: '18', - }, - '5': { - $type: 'fontSizes', - $value: '21', - }, - '6': { - $type: 'fontSizes', - $value: '24', - }, - '7': { - $type: 'fontSizes', - $value: '30', - }, - '8': { - $type: 'fontSizes', - $value: '36', - }, - '9': { - $type: 'fontSizes', - $value: '48', - }, - '10': { - $type: 'fontSizes', - $value: '60', + }; +} + +export function generateFontSizeMode( + size: 'small' | 'medium' | 'large', + themeName: string, + fontName: string, + config?: TypographySizeSchema, +) { + const sizeConfig = config?.[size]; + return { + [themeName]: { + fonts: { + [fontName]: { + 'font-scale': { + _base: { + $type: 'number', + $value: `${sizeConfig?.base ?? defaults[size].base}`, + } satisfies Token, + _ratio: { + $type: 'number', + $value: `${sizeConfig?.ratio ?? defaults[size].ratio}`, + } satisfies Token, + }, + ...(sizeConfig?.overrides && { + 'font-size': R.map( + (fontSizeInPx) => + ({ + $type: 'fontSizes', + $value: `${fontSizeInPx}px`, + }) satisfies Token, + sizeConfig.overrides, + ), + }), + }, }, }, - 'letter-spacing': letterSpacings, - }, - small: { - 'line-height': lineHeights, - 'font-size': { - '1': { - $type: 'fontSizes', - $value: '11', - }, - '2': { - $type: 'fontSizes', - $value: '13', - }, - '3': { - $type: 'fontSizes', - $value: '14', - }, - '4': { - $type: 'fontSizes', - $value: '16', - }, - '5': { - $type: 'fontSizes', - $value: '18', - }, - '6': { - $type: 'fontSizes', - $value: '21', - }, - '7': { - $type: 'fontSizes', - $value: '24', - }, - '8': { - $type: 'fontSizes', - $value: '30', - }, - '9': { - $type: 'fontSizes', - $value: '36', - }, - '10': { - $type: 'fontSizes', - $value: '48', + }; +} + +export const generateFont = (themeName: string, fontName: string, fontDefinition: TypographyFontSchema): TokenSet => { + return { + [themeName]: { + fonts: { + [fontName]: { + 'font-family': { + $type: 'fontFamilies', + $value: fontDefinition.fontFamily, + }, + 'font-weight': { + medium: { + $type: 'fontWeights', + $value: fontDefinition.fontWeight.medium, + }, + semibold: { + $type: 'fontWeights', + $value: fontDefinition.fontWeight.semibold, + }, + regular: { + $type: 'fontWeights', + $value: fontDefinition.fontWeight.regular, + }, + }, + 'line-height': { + sm: { + $type: 'lineHeights', + $value: '130%', + }, + md: { + $type: 'lineHeights', + $value: '150%', + }, + lg: { + $type: 'lineHeights', + $value: '170%', + }, + }, + 'letter-spacing': { + '1': { + $type: 'letterSpacing', + $value: '-1%', + }, + '2': { + $type: 'letterSpacing', + $value: '-0.5%', + }, + '3': { + $type: 'letterSpacing', + $value: '-0.25%', + }, + '4': { + $type: 'letterSpacing', + $value: '-0.15%', + }, + '5': { + $type: 'letterSpacing', + $value: '0%', + }, + '6': { + $type: 'letterSpacing', + $value: '0.15%', + }, + '7': { + $type: 'letterSpacing', + $value: '0.25%', + }, + '8': { + $type: 'letterSpacing', + $value: '0.5%', + }, + '9': { + $type: 'letterSpacing', + $value: '1.5%', + }, + }, + }, }, }, - 'letter-spacing': letterSpacings, - }, + } satisfies TokenSet; }; diff --git a/packages/cli/src/tokens/create/generators/semantic/style.ts b/packages/cli/src/tokens/create/generators/semantic/style.ts index e454fc41f1..56c075a77b 100644 --- a/packages/cli/src/tokens/create/generators/semantic/style.ts +++ b/packages/cli/src/tokens/create/generators/semantic/style.ts @@ -2,236 +2,7 @@ import type { Token, TokenSet } from '../../../types.js'; export function generateSemanticStyle(): TokenSet { return { - typography: { - heading: { - '2xl': { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.medium}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.10}', - letterSpacing: '{letter-spacing.1}', - }, - }, - xl: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.medium}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.9}', - letterSpacing: '{letter-spacing.1}', - }, - }, - lg: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.medium}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.8}', - letterSpacing: '{letter-spacing.2}', - }, - }, - md: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.medium}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.7}', - letterSpacing: '{letter-spacing.3}', - }, - }, - sm: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.medium}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.6}', - letterSpacing: '{letter-spacing.5}', - }, - }, - xs: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.medium}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.5}', - letterSpacing: '{letter-spacing.6}', - }, - }, - '2xs': { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.medium}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.4}', - letterSpacing: '{letter-spacing.6}', - }, - }, - }, - body: { - xl: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.md}', - fontSize: '{font-size.6}', - letterSpacing: '{letter-spacing.8}', - }, - }, - lg: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.md}', - fontSize: '{font-size.5}', - letterSpacing: '{letter-spacing.8}', - }, - }, - md: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.md}', - fontSize: '{font-size.4}', - letterSpacing: '{letter-spacing.8}', - }, - }, - sm: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.md}', - fontSize: '{font-size.3}', - letterSpacing: '{letter-spacing.7}', - }, - }, - xs: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.md}', - fontSize: '{font-size.2}', - letterSpacing: '{letter-spacing.6}', - }, - }, - short: { - xl: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.6}', - letterSpacing: '{letter-spacing.8}', - }, - }, - lg: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.5}', - letterSpacing: '{letter-spacing.8}', - }, - }, - md: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.4}', - letterSpacing: '{letter-spacing.8}', - }, - }, - sm: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.3}', - letterSpacing: '{letter-spacing.7}', - }, - }, - xs: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.sm}', - fontSize: '{font-size.2}', - letterSpacing: '{letter-spacing.6}', - }, - }, - }, - long: { - xl: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.lg}', - fontSize: '{font-size.6}', - letterSpacing: '{letter-spacing.8}', - }, - }, - lg: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.lg}', - fontSize: '{font-size.5}', - letterSpacing: '{letter-spacing.8}', - }, - }, - md: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.lg}', - fontSize: '{font-size.4}', - letterSpacing: '{letter-spacing.8}', - }, - }, - sm: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.lg}', - fontSize: '{font-size.3}', - letterSpacing: '{letter-spacing.7}', - }, - }, - xs: { - $type: 'typography', - $value: { - fontFamily: '{font-family}', - fontWeight: '{font-weight.regular}', - lineHeight: '{line-height.lg}', - fontSize: '{font-size.2}', - letterSpacing: '{letter-spacing.6}', - }, - }, - }, - }, - }, + typography: generateTypography(), opacity: { disabled: { $type: 'opacity', @@ -395,7 +166,7 @@ function generateSingleTypographyToken(component: 'heading' | 'body', size: stri }; } -function _generateTypography() { +function generateTypography() { const headingSizes = ['2xl', 'xl', 'lg', 'md', 'sm', 'xs', '2xs']; const heading = Object.fromEntries( headingSizes.map((size) => [size, generateSingleTypographyToken('heading', size)] as const), diff --git a/packages/cli/src/tokens/types.ts b/packages/cli/src/tokens/types.ts index 731f818cc7..cb94c1bcde 100644 --- a/packages/cli/src/tokens/types.ts +++ b/packages/cli/src/tokens/types.ts @@ -1,5 +1,6 @@ import type { Config as SDConfig } from 'style-dictionary/types'; import type { ConfigSchemaTheme } from '../config.js'; +import type { ColorScheme } from '../index.js'; import type { GetStyleDictionaryConfig } from './process/configs/shared.js'; export type Token = @@ -49,6 +50,20 @@ export type ThemePermutation = { export type ThemeDimension = keyof ThemePermutation; +export type TokenSetDimensions = { + colorSchemes: ColorScheme[]; + sizeModes: SizeModes[]; + fontNames: string[]; +}; + +/** + * `colorSchemes` and `sizeModes` have to be the same across all themes, + * but `fontNames` can be completely unique per theme + */ +export type TokenSetDimensionsForAllThemes = Omit & { + fontNamesPerTheme: Record; +}; + export type GetSDConfigOptions = { tokensDir?: string; dry?: boolean; From d174497afee22e101cda6236ac42a0a105802916 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 19 Mar 2026 09:21:10 +0100 Subject: [PATCH 2/4] theme --- .../tokens/create/generators/themes/theme.ts | 586 +++++++++++++++++- 1 file changed, 584 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/tokens/create/generators/themes/theme.ts b/packages/cli/src/tokens/create/generators/themes/theme.ts index 413552abdf..b5faf6cc6e 100644 --- a/packages/cli/src/tokens/create/generators/themes/theme.ts +++ b/packages/cli/src/tokens/create/generators/themes/theme.ts @@ -1,13 +1,16 @@ import * as R from 'ramda'; import { baseColorNames } from '../../../../colors/colorMetadata.js'; import { type ColorNumber, semanticColorMap } from '../../../../colors/types.js'; -import type { Colors, Token, TokenSet } from '../../../types.js'; +import type { Colors, Token, TokenSet, Typography } from '../../../types.js'; -export const generateTheme = (colors: Colors, themeName: string, borderRadius: number) => { +export const generateTheme = (colors: Colors, themeName: string, borderRadius: number, typography: Typography) => { const mainColorNames = Object.keys(colors.main); const supportColorNames = Object.keys(colors.support); const customColors = [...mainColorNames, 'neutral', ...supportColorNames, ...baseColorNames]; + // TODO handle default font name somewhere else + const defaultFont = Object.keys(typography.fonts ?? {}).at(0) ?? 'primary'; + const themeColorTokens = Object.fromEntries( customColors.map((colorName) => [colorName, generateColorScaleTokens(colorName, themeName)]), ); @@ -34,6 +37,11 @@ export const generateTheme = (colors: Colors, themeName: string, borderRadius: n }, }, }, + // Generate a backward-compatibility layer for the old "font-size", "line-height" etc tokens + ...generateBackwardCompatibilityFontTokens(themeName, defaultFont), + // The mapping from typography to fonts can't be in the typography tokens themself due + // to how Figma generates typography styles. Therefore we have this extra layer. + 'typography-mapping': generateThemeTypography(themeName, typography), ...remainingThemeFile, }; @@ -124,3 +132,577 @@ const generateColorScaleTokens = (colorName: string, themeName: string): Record< return colorScale; }; + +function generateBackwardCompatibilityFontTokens(themeName: string, defaultFont: string) { + const lineHeights = ['sm', 'md', 'lg']; + const fontWeights = ['medium', 'semibold', 'regular']; + const fontSizes = { + // mapping from old to new font scale + '1': '1', + '2': '2', + '3': '3', + '4': '4', + '5': '5', + '6': '6', + '7': '8', + '8': '9', + '9': '11', + '10': '13', + }; + const letterSpacings = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; + return { + 'line-height': Object.fromEntries( + lineHeights.map( + (lineHeight) => + [ + lineHeight, + { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${defaultFont}.line-height.${lineHeight}}`, + }, + ] as const, + ), + ), + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${defaultFont}.font-family}`, + }, + 'font-weight': Object.fromEntries( + fontWeights.map( + (weight) => + [ + weight, + { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${defaultFont}.font-weight.${weight}}`, + }, + ] as const, + ), + ), + 'font-size': Object.fromEntries( + Object.entries(fontSizes).map( + ([oldSize, newSize]) => + [ + oldSize, + { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${defaultFont}.font-size.${newSize}}`, + }, + ] as const, + ), + ), + 'letter-spacing': Object.fromEntries( + letterSpacings.map( + (letterSpacing) => + [ + letterSpacing, + { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${defaultFont}.letter-spacing.${letterSpacing}}`, + }, + ] as const, + ), + ), + }; +} + +function generateThemeTypography(themeName: string, typography: Typography) { + // TODO handle fallback font somewhere else? + const defaultFont = Object.keys(typography.fonts ?? []).at(0) ?? 'primary'; + const headingFont = typography.components?.heading?.font ?? defaultFont; + const bodyFont = typography.components?.body?.font ?? defaultFont; + return { + heading: { + '2xl': { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${headingFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${headingFont}.font-weight.medium}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${headingFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${headingFont}.font-size.13}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${headingFont}.letter-spacing.1}`, + }, + }, + xl: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${headingFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${headingFont}.font-weight.medium}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${headingFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${headingFont}.font-size.11}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${headingFont}.letter-spacing.1}`, + }, + }, + lg: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${headingFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${headingFont}.font-weight.medium}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${headingFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${headingFont}.font-size.9}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${headingFont}.letter-spacing.2}`, + }, + }, + md: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${headingFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${headingFont}.font-weight.medium}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${headingFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${headingFont}.font-size.8}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${headingFont}.letter-spacing.3}`, + }, + }, + sm: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${headingFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${headingFont}.font-weight.medium}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${headingFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${headingFont}.font-size.6}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${headingFont}.letter-spacing.5}`, + }, + }, + xs: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${headingFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${headingFont}.font-weight.medium}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${headingFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${headingFont}.font-size.5}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${headingFont}.letter-spacing.6}`, + }, + }, + '2xs': { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${headingFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${headingFont}.font-weight.medium}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${headingFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${headingFont}.font-size.4}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${headingFont}.letter-spacing.6}`, + }, + }, + }, + body: { + xl: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.md}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.6}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.8}`, + }, + }, + lg: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.md}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.5}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.8}`, + }, + }, + md: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.md}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.4}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.8}`, + }, + }, + sm: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.md}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.3}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.7}`, + }, + }, + xs: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.md}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.2}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.6}`, + }, + }, + short: { + xl: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.6}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.8}`, + }, + }, + lg: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.5}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.8}`, + }, + }, + md: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.4}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.8}`, + }, + }, + sm: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.3}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.7}`, + }, + }, + xs: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.sm}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.2}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.6}`, + }, + }, + }, + long: { + xl: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.lg}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.6}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.8}`, + }, + }, + lg: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.lg}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.5}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.8}`, + }, + }, + md: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.lg}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.4}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.8}`, + }, + }, + sm: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.lg}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.3}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.7}`, + }, + }, + xs: { + 'font-family': { + $type: 'fontFamilies', + $value: `{${themeName}.fonts.${bodyFont}.font-family}`, + }, + 'font-weight': { + $type: 'fontWeights', + $value: `{${themeName}.fonts.${bodyFont}.font-weight.regular}`, + }, + 'line-height': { + $type: 'lineHeights', + $value: `{${themeName}.fonts.${bodyFont}.line-height.lg}`, + }, + 'font-size': { + $type: 'fontSizes', + $value: `{${themeName}.fonts.${bodyFont}.font-size.2}`, + }, + 'letter-spacing': { + $type: 'letterSpacing', + $value: `{${themeName}.fonts.${bodyFont}.letter-spacing.6}`, + }, + }, + }, + }, + }; +} From a70d1599de7ec88dfecbf698deda3a140cafd706 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Fri, 20 Mar 2026 13:56:29 +0100 Subject: [PATCH 3/4] wip --- .../src/tokens/process/configs/semantic.ts | 2 +- .../src/tokens/process/configs/size-mode.ts | 59 +++++---- .../src/tokens/process/configs/type-scale.ts | 88 +++++++------ .../tokens/process/formats/css/size-mode.ts | 9 +- .../tokens/process/formats/css/type-scale.ts | 108 ++++++++++++---- .../cli/src/tokens/process/output/theme.ts | 1 + packages/cli/src/tokens/process/platform.ts | 120 ++++++++++-------- 7 files changed, 236 insertions(+), 151 deletions(-) diff --git a/packages/cli/src/tokens/process/configs/semantic.ts b/packages/cli/src/tokens/process/configs/semantic.ts index 4b5aaad0a9..62d73ed99b 100644 --- a/packages/cli/src/tokens/process/configs/semantic.ts +++ b/packages/cli/src/tokens/process/configs/semantic.ts @@ -30,7 +30,7 @@ export const semanticVariables: GetStyleDictionaryConfig = ({ theme }) => { const isUwantedToken = R.anyPass([R.includes('primitives/global')])(token.filePath); const isPrivateToken = R.includes('_', token.path); const unwantedPaths = pathStartsWithOneOf( - ['size', '_size', 'font-size', 'line-height', 'letter-spacing'], + ['size', '_size', 'font-size', 'line-height', 'letter-spacing', 'font-scale'], token, ); const unwantedTypes = typeEquals(['color', 'fontWeight', 'fontFamily', 'typography'], token); diff --git a/packages/cli/src/tokens/process/configs/size-mode.ts b/packages/cli/src/tokens/process/configs/size-mode.ts index 66408a75a8..2f85732839 100644 --- a/packages/cli/src/tokens/process/configs/size-mode.ts +++ b/packages/cli/src/tokens/process/configs/size-mode.ts @@ -1,36 +1,39 @@ import * as R from 'ramda'; +import { pathStartsWithOneOf } from '../../utils.js'; import { formats } from '../formats/css.js'; - import { basePxFontSize, dsTransformers, type GetStyleDictionaryConfig, prefix } from './shared.js'; -export const sizeModeVariables: GetStyleDictionaryConfig = ({ theme, size }) => { - const selector = `:root`; - const layer = `ds.theme.size-mode`; +export const sizeModeVariables = + (typeScaleValues: 'modular' | 'static'): GetStyleDictionaryConfig => + ({ theme, size }) => { + const selector = `:root`; + const layer = `ds.theme.size-mode`; - return { - preprocessors: ['tokens-studio'], - platforms: { - css: { - // custom - size, - theme, - basePxFontSize, - selector, - layer, - // - prefix, - buildPath: `${theme}/`, - transforms: dsTransformers, - files: [ - { - destination: `size-mode/${size}.css`, - format: formats.sizeMode.name, - filter: (token) => { - return R.equals(['_size', 'mode-font-size'], token.path); + return { + preprocessors: ['tokens-studio'], + platforms: { + css: { + // custom + size, + theme, + basePxFontSize, + selector, + layer, + typeScaleValues, + // + prefix, + buildPath: `${theme}/`, + transforms: dsTransformers, + files: [ + { + destination: `size-mode/${size}.css`, + format: formats.sizeMode.name, + filter: (token) => { + return R.equals(['_size', 'mode-font-size'], token.path) || pathStartsWithOneOf(['font-scale'], token); + }, }, - }, - ], + ], + }, }, - }, + }; }; -}; diff --git a/packages/cli/src/tokens/process/configs/type-scale.ts b/packages/cli/src/tokens/process/configs/type-scale.ts index 1aae5805e5..09ae10797f 100644 --- a/packages/cli/src/tokens/process/configs/type-scale.ts +++ b/packages/cli/src/tokens/process/configs/type-scale.ts @@ -3,53 +3,57 @@ import { formats } from '../formats/css.js'; import { sizeRem, typographyName } from '../transformers.js'; import { basePxFontSize, type GetStyleDictionaryConfig, prefix } from './shared.js'; -export const typeScaleVariables: GetStyleDictionaryConfig = ({ theme }) => { - const selector = ':root, [data-size]'; - const layer = `ds.theme.type-scale`; +export const typeScaleVariables = + (scaleValues: 'static' | 'modular'): GetStyleDictionaryConfig => + ({ theme, size }) => { + const selector = ':root'; + const layer = `ds.theme.type-scale`; - return { - usesDtcg: true, - preprocessors: ['tokens-studio'], - expand: { - include: ['typography'], - }, - platforms: { - css: { - prefix, - selector, - layer, - buildPath: `${theme}/`, - basePxFontSize, - transforms: [ - 'name/kebab', - 'ts/size/px', - sizeRem.name, - 'ts/size/lineheight', - 'ts/typography/fontWeight', - typographyName.name, - ], - files: [ - { - destination: `type-scale.css`, - format: formats.typeScale.name, - filter: (token) => { - const included = typeEquals(['typography', 'dimension', 'fontsize'], token); + return { + usesDtcg: true, + preprocessors: ['tokens-studio'], + expand: { + include: ['typography'], + }, + platforms: { + css: { + size: scaleValues === 'static' ? size : undefined, + prefix, + selector, + layer, + buildPath: `${theme}/`, + basePxFontSize, + transforms: [ + 'name/kebab', + ...(scaleValues === 'static' ? ['ts/resolveMath'] : []), + 'ts/size/px', + sizeRem.name, + 'ts/size/lineheight', + 'ts/typography/fontWeight', + typographyName.name, + ], + files: [ + { + destination: scaleValues === 'static' ? `type-scale/${size}.css` : `type-scale.css`, + format: formats.typeScale.name, + filter: (token) => { + const included = typeEquals(['typography', 'dimension', 'fontsize', 'number'], token); - // Remove primitive typgography tokens - if (/primitives\/modes\/typography\/(primary|secondary)/.test(token.filePath)) return false; + // Remove primitive typgography tokens + if (/primitives\/modes\/typography\/(primary|secondary)/.test(token.filePath)) return false; - return ( - included && - !pathStartsWithOneOf(['spacing', 'sizing', 'size', 'border-width', 'border-radius'], token) && - (pathStartsWithOneOf(['font-size'], token) || token.path.includes('fontSize')) - ); + return ( + included && + !pathStartsWithOneOf(['spacing', 'sizing', 'size', 'border-width', 'border-radius'], token) && + (pathStartsWithOneOf(['font-size', 'font-scale'], token) || token.path.includes('fontSize')) + ); + }, }, + ], + options: { + outputReferences: (token) => pathStartsWithOneOf(['typography'], token) && token.path.includes('fontSize'), }, - ], - options: { - outputReferences: (token) => pathStartsWithOneOf(['typography'], token) && token.path.includes('fontSize'), }, }, - }, + }; }; -}; diff --git a/packages/cli/src/tokens/process/formats/css/size-mode.ts b/packages/cli/src/tokens/process/formats/css/size-mode.ts index 14f827762f..040f8abd78 100644 --- a/packages/cli/src/tokens/process/formats/css/size-mode.ts +++ b/packages/cli/src/tokens/process/formats/css/size-mode.ts @@ -12,14 +12,19 @@ const formatBaseSizeToken = ...token, originalName: token.name, name: `${token.name}--${shortSizeName(size)}`, - $value: token.$value / basePxFontSize, + $value: token.path.includes('_ratio') ? token.$value : token.$value / basePxFontSize, + $description: undefined, // removes comment from output }); export const sizeMode: Format = { name: 'ds/css-size-mode', format: async ({ dictionary, file, options, platform }) => { const { outputReferences, usesDtcg } = options; - const { selector, layer, size } = platform; + const { selector, layer, size } = platform as { + selector: string; + layer?: string; + size: string; + }; const destination = file.destination as string; const format = createPropertyFormatter({ diff --git a/packages/cli/src/tokens/process/formats/css/type-scale.ts b/packages/cli/src/tokens/process/formats/css/type-scale.ts index 91f042741b..81fef2d2af 100644 --- a/packages/cli/src/tokens/process/formats/css/type-scale.ts +++ b/packages/cli/src/tokens/process/formats/css/type-scale.ts @@ -1,44 +1,73 @@ import * as R from 'ramda'; -import type { Format, TransformedToken } from 'style-dictionary/types'; +import type { Dictionary, Format, TransformedToken } from 'style-dictionary/types'; import { createPropertyFormatter } from 'style-dictionary/utils'; -import { basePxFontSize } from '../../configs/shared.js'; +import { orderBySize, shortSizeName } from '../../../utils.js'; import { buildOptions } from '../../platform.js'; import { sizingTemplate } from './size.js'; +import { wrapInLayer } from './size-mode.js'; // Predicate to filter tokens with .path array that includes both typography and fontFamily const isTypographyFontFamilyToken = R.allPass([ R.pathSatisfies(R.includes('typography'), ['path']), R.pathSatisfies(R.includes('fontFamily'), ['path']), ]); +// Predicate to filter font-scale tokens +const isFontScaleToken = R.pathSatisfies(R.includes('font-scale'), ['path']); -type TokensWithCalcAndRoundFormatting = { tokens: TransformedToken[]; calc: string[]; round: string[] }; +type TokensWithCalcAndRoundFormatting = { + tokens: TransformedToken[]; + calc: string[]; + round: string[]; + name: string[]; + originalName: string[]; +}; const formatTypographySizeToken = ( + dictionary: Dictionary, format: (t: TransformedToken) => string, token: TransformedToken, -): { name: string; calc: string; round: string } => { - const [name, value] = format(token).replace(/;$/, '').split(': '); + size?: string, +): { name: string; originalName: string; calc: string; round: string } => { + const [originalName, value] = format(token).trim().replace(/;$/, '').split(': '); + // If we have a size, we're using static type scale values, and need to output the static values per mode. + const name = + size && R.startsWith(['font-size'], token.path) ? `${originalName}--${shortSizeName(size)}` : originalName; + let calc: string; let round: string | undefined; - if (R.startsWith(['font-size'], token.path)) { - calc = `calc(${value} * var(--_ds-font-size-factor))`; + // If we don't have a size, it means we're using modular type scale values. + // That means we need to translate the Tokens Studio formulas to css. + if (!size && R.startsWith(['font-size'], token.path)) { + const originalWithCssReference = (token.original.$value as string).replaceAll(/\{font-scale\.[^}]+\}/g, (match) => { + const t = dictionary.unfilteredTokenMap?.get(match); + return `var(--${t?.name as string})`; + }); + const cssCalcValue = originalWithCssReference.replace(/^roundTo\((.*), 0\)$/, '$1'); + calc = `calc(1rem * ${cssCalcValue})`; round = `round(${calc}, 1px)`; } else { calc = value; } - return { name, calc, round: round ?? calc }; + return { name, originalName, calc, round: round ?? calc }; }; -const formatTypographySizeTokens = (format: (t: TransformedToken) => string, tokens: TransformedToken[]) => +const formatTypographySizeTokens = ( + dictionary: Dictionary, + format: (t: TransformedToken) => string, + tokens: TransformedToken[], + size?: string, +) => R.reduce( (acc, token) => { - const { name, calc, round } = formatTypographySizeToken(format, token); + const { name, calc, round, originalName } = formatTypographySizeToken(dictionary, format, token, size); acc.tokens.push(token); + acc.name.push(name); + acc.originalName.push(originalName); acc.calc.push(`${name}: ${calc};`); acc.round.push(`${name}: ${round};`); return acc; }, - { tokens: [], calc: [], round: [] }, + { tokens: [], calc: [], round: [], name: [], originalName: [] }, tokens, ); @@ -46,7 +75,7 @@ export const typeScale: Format = { name: 'ds/css-type-scale', format: async ({ dictionary, file, options, platform }) => { const { outputReferences, usesDtcg } = options; - const { selector, layer } = platform as { selector: string; layer: string }; + const { selector, layer, size } = platform as { selector: string; layer?: string; size?: string }; const destination = file.destination as string; const format = createPropertyFormatter({ @@ -56,20 +85,55 @@ export const typeScale: Format = { usesDtcg, }); - const filteredTokens = R.reject(R.anyPass([isTypographyFontFamilyToken]), dictionary.allTokens); - const formattedTokens = formatTypographySizeTokens(format, filteredTokens); + const filteredTokens = R.reject(R.anyPass([isTypographyFontFamilyToken, isFontScaleToken]), dictionary.allTokens); + const [typeScaleTokens, restTokens] = R.partition((t) => R.startsWith(['font-size'], t.path), filteredTokens); + const formatted = formatTypographySizeTokens(dictionary, format, typeScaleTokens, size); + const formattedReferences = formatTypographySizeTokens(dictionary, format, restTokens, size); - const formattedMap = formattedTokens.round.map((t, i) => ({ - token: formattedTokens.tokens[i], - formatted: t, + const formattedMap = formatted.round.map((s, i) => ({ + token: formatted.tokens[i], + // Remove the `--` suffix for the token listing, since that is the only token we actually use + formatted: s.replace(formatted.name[i], formatted.originalName[i]), })); - buildOptions.buildTokenFormats[destination] = formattedMap; + const formattedReferencesMap = formattedReferences.round.map((s, i) => { + return { token: formattedReferences.tokens[i], formatted: s }; + }); + + buildOptions.buildTokenFormats[destination] = [...formattedMap, ...formattedReferencesMap]; + + const optionalSizeComment = size ? ` /* ${size} */` : ''; + const content = `${selector}${optionalSizeComment} {${sizingTemplate(formatted)}\n}`; + const body = wrapInLayer(content, layer); + + /* + * The following CSS is only generated once, not per mode + */ + const sizes = orderBySize(buildOptions?.sizeModes ?? []).map(shortSizeName); + + const fontScaleToggles = size + ? formatted.originalName + .map( + (variable) => ` ${variable}: +${sizes.map((size) => ` var(--ds-size--${size}, var(${variable}--${size}))`).join('\n')};`, + ) + .join('\n') + : ` --ds-font-scale-base: +${sizes.map((size) => ` var(--ds-size--${size}, var(--ds-font-scale-base--${size}))`).join('\n')}; + --ds-font-scale-ratio: +${sizes.map((size) => ` var(--ds-size--${size}, var(--ds-font-scale-ratio--${size}))`).join('\n')};`; + + const referenceVariables = restTokens.map(format).join('\n'); + const sharedContent = `:root, [data-size] { +${fontScaleToggles} +${referenceVariables} +}`; - const sizeFactor = ` --_ds-font-size-factor: calc(var(--ds-size-mode-font-size) / (var(--ds-size-base) / ${basePxFontSize}));`; - const content = `${selector} {\n${sizeFactor}${sizingTemplate(formattedTokens)}\n}`; - const body = R.isNotNil(layer) ? `@layer ${layer} {\n${content}\n}` : content; + const sharedBody = !size || shortSizeName(size) === R.last(sizes) ? `\n${wrapInLayer(sharedContent, layer)}` : ''; + /* + * End of generated-once CSS + */ - return body; + return body + sharedBody; }, }; diff --git a/packages/cli/src/tokens/process/output/theme.ts b/packages/cli/src/tokens/process/output/theme.ts index a8406ebda3..d57cc4d32e 100644 --- a/packages/cli/src/tokens/process/output/theme.ts +++ b/packages/cli/src/tokens/process/output/theme.ts @@ -58,6 +58,7 @@ export const createThemeCSSFiles = ({ const sortOrder = [ 'size-mode/', 'type-scale', + 'type-scale/', 'color-scheme/light', 'typography/secondary', 'size', diff --git a/packages/cli/src/tokens/process/platform.ts b/packages/cli/src/tokens/process/platform.ts index 0872ef1a49..8386b2b46b 100644 --- a/packages/cli/src/tokens/process/platform.ts +++ b/packages/cli/src/tokens/process/platform.ts @@ -2,6 +2,7 @@ import pc from 'picocolors'; import * as R from 'ramda'; import StyleDictionary from 'style-dictionary'; import type { TransformedToken } from 'style-dictionary/types'; +import type { BuildConfigSchema } from '../../config.js'; import type { OutputFile, TokenSet } from '../types.js'; import { type BuildConfig, colorCategories, type ThemePermutation } from '../types.js'; import { configs, getConfigsForThemeDimensions } from './configs.js'; @@ -22,7 +23,7 @@ type SharedOptions = { colorGroups?: string[]; /** Build token format map */ buildTokenFormats: Record; -}; +} & BuildConfigSchema['build']; export type BuildOptions = { type: 'build'; @@ -40,7 +41,7 @@ export type FormatOptions = { export type ProcessOptions = BuildOptions | FormatOptions; -type ProcessedBuildConfigs = Record; +type ProcessedBuildConfigs = Record, T>; export type ProcessReturn = ProcessedBuildConfigs; @@ -75,44 +76,48 @@ const sd = new StyleDictionary(); /* * Declarative configuration of the build output */ -const buildConfigs = { - typography: { getConfig: configs.typographyVariables, dimensions: ['typography'] }, - sizeMode: { getConfig: configs.sizeModeVariables, dimensions: ['size'] }, - size: { getConfig: configs.sizeVariables, dimensions: ['semantic'] }, - typeScale: { getConfig: configs.typeScaleVariables, dimensions: ['semantic'] }, - 'color-scheme': { getConfig: configs.colorSchemeVariables, dimensions: ['color-scheme'] }, - 'main-color': { getConfig: configs.mainColorVariables, dimensions: ['main-color'] }, - 'support-color': { getConfig: configs.supportColorVariables, dimensions: ['support-color'] }, - 'neutral-color': { - getConfig: configs.neutralColorVariables, - dimensions: ['semantic'], - log: ({ permutation: { theme } }) => `${theme} - neutral`, - }, - 'success-color': { - getConfig: configs.successColorVariables, - dimensions: ['semantic'], - log: ({ permutation: { theme } }) => `${theme} - success`, - }, - 'danger-color': { - getConfig: configs.dangerColorVariables, - dimensions: ['semantic'], - log: ({ permutation: { theme } }) => `${theme} - danger`, - }, - 'warning-color': { - getConfig: configs.warningColorVariables, - dimensions: ['semantic'], - log: ({ permutation: { theme } }) => `${theme} - warning`, - }, - 'info-color': { - getConfig: configs.infoColorVariables, - dimensions: ['semantic'], - log: ({ permutation: { theme } }) => `${theme} - info`, - }, - semantic: { getConfig: configs.semanticVariables, dimensions: ['semantic'] }, -} satisfies Record; +const buildConfigs = (typographySizeValues: 'modular' | 'static') => + ({ + typography: { getConfig: configs.typographyVariables, dimensions: ['typography'] }, + sizeMode: { getConfig: configs.sizeModeVariables(typographySizeValues), dimensions: ['size'] }, + size: { getConfig: configs.sizeVariables, dimensions: ['semantic'] }, + typeScale: { + getConfig: configs.typeScaleVariables(typographySizeValues), + dimensions: typographySizeValues === 'modular' ? ['semantic'] : ['semantic', 'size'], + }, + 'color-scheme': { getConfig: configs.colorSchemeVariables, dimensions: ['color-scheme'] }, + 'main-color': { getConfig: configs.mainColorVariables, dimensions: ['main-color'] }, + 'support-color': { getConfig: configs.supportColorVariables, dimensions: ['support-color'] }, + 'neutral-color': { + getConfig: configs.neutralColorVariables, + dimensions: ['semantic'], + log: ({ permutation: { theme } }) => `${theme} - neutral`, + }, + 'success-color': { + getConfig: configs.successColorVariables, + dimensions: ['semantic'], + log: ({ permutation: { theme } }) => `${theme} - success`, + }, + 'danger-color': { + getConfig: configs.dangerColorVariables, + dimensions: ['semantic'], + log: ({ permutation: { theme } }) => `${theme} - danger`, + }, + 'warning-color': { + getConfig: configs.warningColorVariables, + dimensions: ['semantic'], + log: ({ permutation: { theme } }) => `${theme} - warning`, + }, + 'info-color': { + getConfig: configs.infoColorVariables, + dimensions: ['semantic'], + log: ({ permutation: { theme } }) => `${theme} - info`, + }, + semantic: { getConfig: configs.semanticVariables, dimensions: ['semantic'] }, + }) satisfies Record; export async function processPlatform(options: ProcessOptions): Promise { - const { type, processed$themes } = options; + const { type, processed$themes, typographySizeValues } = options; const platform = 'css'; const tokenSets = type === 'format' ? options.tokenSets : undefined; const tokensDir = type === 'build' ? options.tokensDir : undefined; @@ -164,27 +169,30 @@ export async function processPlatform(options: ProcessOptions): Promise { - const sdConfigs = getConfigsForThemeDimensions(buildConfig.getConfig, processed$themes, buildConfig.dimensions, { - tokensDir, - tokenSets, - }); + const buildAndSdConfigs = R.map( + (buildConfig: BuildConfig) => { + const sdConfigs = getConfigsForThemeDimensions(buildConfig.getConfig, processed$themes, buildConfig.dimensions, { + tokensDir, + tokenSets, + }); - // Disable build if all sdConfigs dimensions permutation are unknown - const unknownConfigs = buildConfig.dimensions.map((dimension) => - sdConfigs.filter((x) => x.permutation[dimension] === 'unknown'), - ); - for (const unknowns of unknownConfigs) { - if (unknowns.length === sdConfigs.length) { - buildConfig.enabled = () => false; + // Disable build if all sdConfigs dimensions permutation are unknown + const unknownConfigs = buildConfig.dimensions.map((dimension) => + sdConfigs.filter((x) => x.permutation[dimension] === 'unknown'), + ); + for (const unknowns of unknownConfigs) { + if (unknowns.length === sdConfigs.length) { + buildConfig.enabled = () => false; + } } - } - return { - buildConfig, - sdConfigs, - }; - }, buildConfigs); + return { + buildConfig, + sdConfigs, + }; + }, + buildConfigs(typographySizeValues ?? 'modular'), + ); const processedBuilds: ProcessedBuildConfigs> = { 'color-scheme': [initResult], From 29d292f39d59a9d97f1433a3bc8fa68e4229a194 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Fri, 20 Mar 2026 14:09:17 +0100 Subject: [PATCH 4/4] wip --- packages/cli/bin/designsystemet.ts | 8 ++++++-- packages/cli/src/scripts/update-preview-tokens.ts | 12 +++++++----- packages/cli/src/tokens/create.ts | 2 +- packages/cli/src/tokens/create/files.ts | 10 ++++++---- packages/cli/src/tokens/format.ts | 11 ++++++++--- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index c301277814..429d2c60bf 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -147,8 +147,12 @@ function makeTokenCommands() { // Casting as missing properties should be validated by `getDefaultOrExplicitOption` to default values const theme = { name, ...themeWithoutName } as Theme; - const { tokenSets } = await createTokens(theme); - files = files.concat(await createTokenFiles({ outDir, theme, tokenSets })); + const { tokenSets, themeDimensions } = await createTokens(theme); + const tokenSetDimensions = { + ...themeDimensions, + fontNamesPerTheme: { [theme.name]: themeDimensions.fontNames }, + }; + files = files.concat(await createTokenFiles({ outDir, theme, tokenSets, tokenSetDimensions })); } } diff --git a/packages/cli/src/scripts/update-preview-tokens.ts b/packages/cli/src/scripts/update-preview-tokens.ts index 7171d313c0..1efd271703 100644 --- a/packages/cli/src/scripts/update-preview-tokens.ts +++ b/packages/cli/src/scripts/update-preview-tokens.ts @@ -5,7 +5,7 @@ import { generate$Themes } from '../tokens/create/generators/$themes.js'; import { createTokens } from '../tokens/create.js'; import { buildOptions, processPlatform } from '../tokens/process/platform.js'; import { processThemeObject } from '../tokens/process/utils/getMultidimensionalThemes.js'; -import type { SizeModes, Theme } from '../tokens/types.js'; +import type { Theme } from '../tokens/types.js'; import { dsfs } from '../utils/filesystem.js'; const OUTDIR = '../../internal/components/src/tokens/design-tokens'; @@ -24,11 +24,13 @@ const toPreviewToken = (tokens: { token: TransformedToken; formatted: string }[] type PreviewToken = { variable: string; value: string }; export const formatTheme = async (themeConfig: Theme) => { - const { tokenSets } = await createTokens(themeConfig); + const { tokenSets, themeDimensions } = await createTokens(themeConfig); - const sizeModes: SizeModes[] = ['small', 'medium', 'large']; - - const $themes = await generate$Themes(['dark', 'light'], [themeConfig.name], themeConfig.colors, sizeModes); + const tokenSetDimensions = { + ...themeDimensions, + fontNamesPerTheme: { [themeConfig.name]: themeDimensions.fontNames }, + }; + const $themes = await generate$Themes(tokenSetDimensions, [themeConfig.name], themeConfig.colors); const processed$themes = $themes.map(processThemeObject); // We run this to populate the `buildOptions.buildTokenFormats` with transformed tokens diff --git a/packages/cli/src/tokens/create.ts b/packages/cli/src/tokens/create.ts index ca93b0d4e4..4deb81ebef 100644 --- a/packages/cli/src/tokens/create.ts +++ b/packages/cli/src/tokens/create.ts @@ -73,7 +73,7 @@ export const createTokens = async (theme: Theme) => { ...colorSchemes.flatMap((scheme): [string, TokenSet][] => [ [`primitives/modes/color-scheme/${scheme}/${name}`, generateColorScheme(name, scheme, colors, overrides)], ]), - [`themes/${name}`, generateTheme(colors, name, borderRadius)], + [`themes/${name}`, generateTheme(colors, name, borderRadius, typography)], ['semantic/color', generateSemanticColors(colors, name)], // maps out semantic modes, ieg 'semantic/modes/main-color/accent', and 'semantic/modes/support-color/brand1' ...Object.entries(generateColorModes(colors, name)).flatMap(([mode, colors]): [string, TokenSet][] => diff --git a/packages/cli/src/tokens/create/files.ts b/packages/cli/src/tokens/create/files.ts index 274949e034..e4ae2662a4 100644 --- a/packages/cli/src/tokens/create/files.ts +++ b/packages/cli/src/tokens/create/files.ts @@ -3,7 +3,7 @@ import type { ThemeObject } from '@tokens-studio/types'; import pc from 'picocolors'; import * as R from 'ramda'; import { dsfs } from '../../utils/filesystem.js'; -import type { OutputFile, SizeModes, Theme, TokenSets } from '../types.js'; +import type { OutputFile, SizeModes, Theme, TokenSetDimensionsForAllThemes, TokenSets } from '../types.js'; import { generate$Designsystemet } from './generators/$designsystemet.js'; import { generate$Metadata } from './generators/$metadata.js'; import { generate$Themes } from './generators/$themes.js'; @@ -14,6 +14,7 @@ type CreateTokenFilesOptions = { outDir: string; theme: Theme; tokenSets: TokenSets; + tokenSetDimensions: TokenSetDimensionsForAllThemes; }; export const createTokenFiles = async (options: CreateTokenFilesOptions) => { @@ -21,13 +22,14 @@ export const createTokenFiles = async (options: CreateTokenFilesOptions) => { outDir, tokenSets, theme: { name: themeName, colors }, + tokenSetDimensions, } = options; const $themesPath = '$themes.json'; const $metadataPath = '$metadata.json'; const $designsystemetPath = '$designsystemet.jsonc'; let themeObjects: ThemeObject[] = []; - const sizeModes: SizeModes[] = ['small', 'medium', 'large']; + const _sizeModes: SizeModes[] = ['small', 'medium', 'large']; await dsfs.mkdir(outDir); @@ -52,8 +54,8 @@ export const createTokenFiles = async (options: CreateTokenFilesOptions) => { console.log(`\nThemes: ${pc.blue(themes.join(', '))}`); // Create metadata and themes json for Token Studio and build script - const $themes = await generate$Themes(['dark', 'light'], themes, colors, sizeModes); - const $metadata = generate$Metadata(['dark', 'light'], themes, colors, sizeModes); + const $themes = await generate$Themes(tokenSetDimensions, themes, colors); + const $metadata = generate$Metadata(tokenSetDimensions, themes, colors); const $designsystemet = generate$Designsystemet(); const files: OutputFile[] = []; diff --git a/packages/cli/src/tokens/format.ts b/packages/cli/src/tokens/format.ts index bbd0cfb133..5665e1d6ed 100644 --- a/packages/cli/src/tokens/format.ts +++ b/packages/cli/src/tokens/format.ts @@ -17,10 +17,15 @@ export const formatTokens = async (options: Omit { - const { tokenSets } = await createTokens(themeConfig); - const sizeModes: SizeModes[] = ['small', 'medium', 'large']; + const { tokenSets, themeDimensions } = await createTokens(themeConfig); + const _sizeModes: SizeModes[] = ['small', 'medium', 'large']; - const $themes = await generate$Themes(['dark', 'light'], [themeConfig.name], themeConfig.colors, sizeModes); + const tokenSetThemeDimensions = { + ...themeDimensions, + fontNamesPerTheme: { [themeConfig.name]: themeDimensions.fontNames }, + }; + + const $themes = await generate$Themes(tokenSetThemeDimensions, [themeConfig.name], themeConfig.colors); const processed$themes = $themes.map(processThemeObject); const processedBuilds = await formatTokens({