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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion designsystemet.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
}
Expand Down
14 changes: 8 additions & 6 deletions packages/cli/bin/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as R from 'ramda';
import {
type BuildConfigSchema,
type CreateConfigSchema,
commonConfig,
configFileBuildSchema,
configFileCreateSchema,
parseConfig,
validateConfig,
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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<CreateConfigSchema>(configFileCreateSchema, unvalidatedConfig, configFilePath);
}
Expand All @@ -108,5 +110,5 @@ export async function parseBuildConfig(
): Promise<BuildConfigSchema> {
const configParsed: BuildConfigSchema = parseConfig<BuildConfigSchema>(configFile, configFilePath);

return validateConfig<BuildConfigSchema>(commonConfig, configParsed, configFilePath);
return validateConfig<BuildConfigSchema>(configFileBuildSchema, configParsed, configFilePath);
}
28 changes: 25 additions & 3 deletions packages/cli/bin/designsystemet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ 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';
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 = `
_____ _ _ _
Expand Down Expand Up @@ -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<CreateConfigSchema>(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 });
Expand All @@ -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);
Expand Down Expand Up @@ -129,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 }));
}
}

Expand Down
109 changes: 96 additions & 13 deletions packages/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>.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.<name>.typography.fonts',
),
})
.optional(),
});

const overridesSchema = z
.object({
colors: semanticColorOverrideSchema.optional(),
Expand All @@ -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
Expand All @@ -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.<name>.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(),
Expand All @@ -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<typeof commonConfig>;
export type BuildConfigSchema = z.infer<typeof commonConfig>;
export type BuildConfigSchema = z.infer<typeof configFileBuildSchema>;
export type CreateConfigSchema = z.infer<typeof configFileCreateSchema>;
export type ConfigSchemaTheme = z.infer<typeof themeSchema>;
export type ColorOverrideSchema = z.infer<typeof overridesSchema>;
export type TypographySizeSchema = z.infer<typeof typographySizeSchema>;
export type TypographyFontSchema = z.infer<typeof typographyFontSchema>;
2 changes: 1 addition & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
4 changes: 2 additions & 2 deletions packages/cli/src/scripts/createJsonSchema.ts
Original file line number Diff line number Diff line change
@@ -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'),
Expand Down
12 changes: 7 additions & 5 deletions packages/cli/src/scripts/update-preview-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down
Loading
Loading