Skip to content

Commit

Permalink
feat(cli): support custom colors (#2733)
Browse files Browse the repository at this point in the history
The theme builder and Figma plugin have been changed to support the new
CLI options format that is necessary to support arbitrary number and
names for colors. However, unlike the CLI they are still hard-coded to
create and accept only the colors *accent, neutral, brand1, brand2* and
*brand3*
  • Loading branch information
unekinn authored Nov 8, 2024
1 parent d75ffad commit f304115
Show file tree
Hide file tree
Showing 42 changed files with 811 additions and 1,393 deletions.
5 changes: 5 additions & 0 deletions .changeset/stale-tables-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@digdir/designsystemet": minor
---

CLI now supports creating themes with 1 or more "main" colors, a neutral color, and 1 or more "support" colors. The "main" and "support" colors can have arbitrary names. There can not be more than 4 colors of each category unless you're using Figma on the Enterprise plan, due to plan-based restrictions on the number of variable modes per collection.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
dist
ignore
tsc-build
*.tsbuildinfo

# Yarn stuff; we're not using PnP/Zero installs
.pnp.*
Expand Down
22 changes: 13 additions & 9 deletions apps/theme/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
ColorInfo,
ColorMode,
ContrastMode,
ThemeColors,
ThemeInfo,
} from '@digdir/designsystemet/color';
import {
Expand All @@ -24,6 +23,7 @@ import { useEffect, useRef, useState } from 'react';
import { Previews, Scales, ThemeToolbar } from '../components';
import { Settings } from '../settings';
import { useThemeStore } from '../store';
import type { ThemeColors } from '../types';
import { mapTokens } from '../utils/tokenMapping';

import classes from './page.module.css';
Expand Down Expand Up @@ -71,21 +71,25 @@ export default function Home() {
// Generate color scales
const colors = generateColorTheme({
colors: {
accent: queryAccent,
main: {
accent: queryAccent,
},
support: {
brand1: queryBrand1,
brand2: queryBrand2,
brand3: queryBrand3,
},
neutral: queryNeutral,
brand1: queryBrand1,
brand2: queryBrand2,
brand3: queryBrand3,
},
contrastMode,
});

// Update colors and themes
updateColor('accent', queryAccent, colors.accent);
updateColor('accent', queryAccent, colors.main.accent);
updateColor('neutral', queryNeutral, colors.neutral);
updateColor('brand1', queryBrand1, colors.brand1);
updateColor('brand2', queryBrand2, colors.brand2);
updateColor('brand3', queryBrand3, colors.brand3);
updateColor('brand1', queryBrand1, colors.support.brand1);
updateColor('brand2', queryBrand2, colors.support.brand2);
updateColor('brand3', queryBrand3, colors.support.brand3);
}, [contrastMode]);

useEffect(() => {
Expand Down
3 changes: 2 additions & 1 deletion apps/theme/components/Color/Color.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { omit } from '@digdir/designsystemet-react';
import type { ColorInfo, ThemeColors } from '@digdir/designsystemet/color';
import type { ColorInfo } from '@digdir/designsystemet/color';
import { SunIcon } from '@navikt/aksel-icons';
import cl from 'clsx/lite';
import { forwardRef } from 'react';

import { useThemeStore } from '../../store';
import type { ThemeColors } from '../../types';

import classes from './Color.module.css';

Expand Down
3 changes: 2 additions & 1 deletion apps/theme/components/Group/Group.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { RovingFocusItem } from '@digdir/designsystemet-react';
import type { ColorInfo, ThemeColors } from '@digdir/designsystemet/color';
import type { ColorInfo } from '@digdir/designsystemet/color';
import cl from 'clsx/lite';

import type { ThemeColors } from '../../types';
import { Color } from '../Color/Color';

import classes from './Group.module.css';
Expand Down
4 changes: 2 additions & 2 deletions apps/theme/components/Previews/Components/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ export const Components = () => {
Utfør
</Button>
</div>
<Search className={classes.tableSearch}>
<Search className={classes.tableSearch} data-size='sm'>
<Search.Input
aria-label='Søk etter bruker'
placeholder='Søk etter bruker'
placeholder='Søk etter bruker...'
/>
<Search.Clear />
<Search.Button />
Expand Down
4 changes: 2 additions & 2 deletions apps/theme/components/Scale/Scale.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { RovingFocusRoot } from '@digdir/designsystemet-react';
import type { ColorInfo, ThemeColors } from '@digdir/designsystemet/color';
import type { ColorInfo } from '@digdir/designsystemet/color';
import { useEffect, useState } from 'react';

import type { modeType } from '../../types';
import type { ThemeColors, modeType } from '../../types';
import { Group } from '../Group/Group';

import classes from './Scale.module.css';
Expand Down
7 changes: 2 additions & 5 deletions apps/theme/components/ThemeToolbar/ThemeToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import type { CssColor } from '@adobe/leonardo-contrast-colors';
import { Button, Tooltip } from '@digdir/designsystemet-react';
import type {
ColorError,
ContrastMode,
ThemeColors,
} from '@digdir/designsystemet/color';
import type { ColorError, ContrastMode } from '@digdir/designsystemet/color';
import cl from 'clsx/lite';
import { useState } from 'react';

import { useThemeStore } from '../../store';
import type { ThemeColors } from '../../types';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { TokenModal } from '../TokenModal/TokenModal';

Expand Down
22 changes: 12 additions & 10 deletions apps/theme/components/TokenModal/TokenModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
Paragraph,
Textfield,
} from '@digdir/designsystemet-react';
import { createTokens } from '@digdir/designsystemet/tokens';
import { colorCliOptions, createTokens } from '@digdir/designsystemet/tokens';
import { CodeIcon, InformationSquareIcon } from '@navikt/aksel-icons';
import { CodeSnippet } from '@repo/components';
import { useEffect, useRef, useState } from 'react';
Expand Down Expand Up @@ -44,23 +44,25 @@ export const TokenModal = ({
const [themeName, setThemeName] = useState('theme');

const cliSnippet = `npx @digdir/designsystemet@next tokens create \\
--accent "${accentColor}" \\
--neutral "${neutralColor}" \\
--brand1 "${brand1Color}" \\
--brand2 "${brand2Color}" \\
--brand3 "${brand3Color}" \\
--${colorCliOptions.main} "accent:${accentColor}" \\
--${colorCliOptions.neutral} "${neutralColor}" \\
--${colorCliOptions.support} "brand1:${brand1Color}" "brand2:${brand2Color}" "brand3:${brand3Color}" \\
--theme "${themeName}" \\
--write
`;

useEffect(() => {
const tokens = createTokens({
colors: {
accent: accentColor,
main: {
accent: accentColor,
},
neutral: neutralColor,
brand1: brand1Color,
brand2: brand2Color,
brand3: brand3Color,
support: {
brand1: brand1Color,
brand2: brand2Color,
brand3: brand3Color,
},
},
typography: { fontFamily: 'Inter' },
themeName: 'theme',
Expand Down
7 changes: 2 additions & 5 deletions apps/theme/store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import type { CssColor } from '@adobe/leonardo-contrast-colors';
import type {
ColorInfo,
ThemeColors,
ThemeInfo,
} from '@digdir/designsystemet/color';
import type { ColorInfo, ThemeInfo } from '@digdir/designsystemet/color';
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';

import { Settings } from './settings';
import type { ThemeColors } from './types';

type StoreThemeType = {
theme: ThemeInfo;
Expand Down
1 change: 1 addition & 0 deletions apps/theme/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CssColor } from '@adobe/leonardo-contrast-colors';

export type modeType = 'light' | 'dark' | 'contrast';
export type ThemeColors = 'accent' | 'neutral' | 'brand1' | 'brand2' | 'brand3';

export type colorType = {
color: CssColor;
Expand Down
32 changes: 17 additions & 15 deletions packages/cli/bin/designsystemet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import { Argument, createCommand, program } from '@commander-js/extra-typings';
import chalk from 'chalk';

import type { CssColor } from '@adobe/leonardo-contrast-colors';
import { convertToHex } from '../src/colors/index.js';
import migrations from '../src/migrations/index.js';
import { buildTokens } from '../src/tokens/build.js';
import { createTokens } from '../src/tokens/create.js';
import { colorCliOptions, createTokens } from '../src/tokens/create.js';
import { writeTokens } from '../src/tokens/write.js';

program.name('Designsystemet').description('CLI for working with Designsystemet').showHelpAfterError();
program.name('designsystemet').description('CLI for working with Designsystemet').showHelpAfterError();

function makeTokenCommands() {
const tokenCmd = createCommand('tokens');
Expand All @@ -27,17 +28,14 @@ function makeTokenCommands() {
const preview = opts.preview;
const verbose = opts.verbose;
console.log(`Building tokens in ${chalk.green(tokens)}`);
return buildTokens({ tokens, out, preview, verbose, accentColor: 'accent' });
return buildTokens({ tokens, out, preview, verbose });
});

tokenCmd
.command('create')
.description('Create Designsystemet tokens')
.requiredOption('-a, --accent <number>', `Accent hex color`)
.requiredOption('-n, --neutral <number>', `Neutral hex color`)
.requiredOption('-b1, --brand1 <number>', `Brand1 hex color`)
.requiredOption('-b2, --brand2 <number>', `Brand2 hex color`)
.requiredOption('-b3, --brand3 <number>', `Brand3 hex color`)
.requiredOption(`-m, --${colorCliOptions.main} <name:hex...>`, `Main colors`, parseColorValues)
.requiredOption(`-s, --${colorCliOptions.support} <name:hex...>`, `Support colors`, parseColorValues)
.requiredOption(`-n, --${colorCliOptions.neutral} <hex>`, `Neutral hex color`, convertToHex)
.option('-w, --write [string]', `Output directory for created ${chalk.blue('design-tokens')}`, DEFAULT_TOKENSDIR)
.option('-f, --font-family <string>', `Font family`, 'Inter')
.option('--theme <string>', `Theme name`, 'theme')
Expand All @@ -49,11 +47,9 @@ function makeTokenCommands() {
const props = {
themeName: theme,
colors: {
accent: convertToHex(opts.accent),
neutral: convertToHex(opts.neutral),
brand1: convertToHex(opts.brand1),
brand2: convertToHex(opts.brand2),
brand3: convertToHex(opts.brand3),
main: opts.mainColors,
support: opts.supportColors,
neutral: opts.neutralColor,
},
typography: {
fontFamily: fontFamily,
Expand All @@ -63,7 +59,7 @@ function makeTokenCommands() {
const tokens = createTokens(props);

if (write) {
await writeTokens({ writeDir: write, tokens, themeName: theme });
await writeTokens({ writeDir: write, tokens, themeName: theme, colors: props.colors });
}

return Promise.resolve();
Expand Down Expand Up @@ -104,3 +100,9 @@ program
});

await program.parseAsync(process.argv);

function parseColorValues(value: string, previous: Record<string, CssColor> = {}): Record<string, CssColor> {
const [name, hex] = value.split(':');
previous[name] = convertToHex(hex);
return previous;
}
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"build": "tsup && yarn build:types",
"build:swc": "yarn clean && swc src bin --copy-files -d dist && yarn build:types",
"build:types": "tsc --emitDeclarationOnly --declaration",
"test:tokens-create": "yarn designsystemet tokens create -a #007682 -n #003333 -b1 #12404f -b2 #0054a6 -b3 #942977 -w ./test-tokens-create",
"test:tokens-create": "yarn designsystemet tokens create -m dominant:#007682 complimentary:#ff0000 -n #003333 -s support1:#12404f support2:#0054a6 support3:#942977 -w ./test-tokens-create",
"test:tokens-build": "yarn designsystemet tokens build --verbose -t ./test-tokens-create -o ./test-tokens-build",
"test:tokens-create-and-build": "rimraf test-tokens-create && rimraf test-tokens-build && yarn test:tokens-create && yarn test:tokens-build",
"test": "yarn test:tokens-create-and-build",
Expand Down
29 changes: 15 additions & 14 deletions packages/cli/src/colors/theme.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { CssColor } from '@adobe/leonardo-contrast-colors';
import { BackgroundColor, Color, Theme } from '@adobe/leonardo-contrast-colors';
import { Hsluv } from 'hsluv';
import * as R from 'ramda';

import type { ColorInfo, ColorMode, ColorNumber, ContrastMode, GlobalColors, ThemeColors, ThemeInfo } from './types.js';
import type { Colors } from '../tokens/types.js';
import type { ColorInfo, ColorMode, ColorNumber, ContrastMode, GlobalColors, ThemeInfo } from './types.js';
import { getContrastFromHex, getContrastFromLightness, getLightnessFromHex } from './utils.js';

export const baseColors: Record<GlobalColors, CssColor> = {
Expand All @@ -14,8 +16,6 @@ export const baseColors: Record<GlobalColors, CssColor> = {
yellow: '#D4B12F',
};

type Colors = Record<ThemeColors, CssColor>;

export type ColorError = 'none' | 'decorative' | 'interaction';

type GlobalGenType = {
Expand Down Expand Up @@ -189,26 +189,27 @@ export const generateGlobalColors = ({ contrastMode = 'aa' }: GlobalGenType): Re
};
};

type GeneratedColorTheme = {
main: Record<string, ThemeInfo>;
support: Record<string, ThemeInfo>;
neutral: ThemeInfo;
};
/**
* This function generates a complete theme for a set of colors.
*
* @param colors Which colors to generate the theme for
* @param contrastMode The contrast mode to use
* @returns
*/
export const generateColorTheme = ({ colors, contrastMode = 'aa' }: ThemeGenType): Record<ThemeColors, ThemeInfo> => {
const accentTheme = generateThemeForColor(colors.accent, contrastMode);
const neutralTheme = generateThemeForColor(colors.neutral, contrastMode);
const brand1Theme = generateThemeForColor(colors.brand1, contrastMode);
const brand2Theme = generateThemeForColor(colors.brand2, contrastMode);
const brand3Theme = generateThemeForColor(colors.brand3, contrastMode);
export const generateColorTheme = ({ colors, contrastMode = 'aa' }: ThemeGenType): GeneratedColorTheme => {
const main = R.map((color) => generateThemeForColor(color, contrastMode), colors.main);
const support = R.map((color) => generateThemeForColor(color, contrastMode), colors.support);
const neutral = generateThemeForColor(colors.neutral, contrastMode);

return {
accent: accentTheme,
neutral: neutralTheme,
brand1: brand1Theme,
brand2: brand2Theme,
brand3: brand3Theme,
main,
support,
neutral,
};
};

Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/colors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export type ColorMode = 'light' | 'dark' | 'contrast';
export type ContrastMode = 'aa' | 'aaa';
export type ColorNumber = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;
export type GlobalColors = 'red' | 'blue' | 'green' | 'orange' | 'purple' | 'yellow';
export type ThemeColors = 'accent' | 'neutral' | 'brand1' | 'brand2' | 'brand3';
export type ColorInfo = {
hex: CssColor;
number: ColorNumber;
Expand Down
14 changes: 11 additions & 3 deletions packages/cli/src/tokens/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type Options = {
/** Enable verbose output */
verbose: boolean;
/** Set the default "accent" color, if not overridden with data-color */
accentColor: string;
accentColor?: string;
};

export let buildOptions: Options | undefined;
Expand Down Expand Up @@ -72,13 +72,21 @@ export async function buildTokens(options: Options): Promise<void> {
/*
* Build the themes
*/
const $themes = JSON.parse(await fs.readFile(path.resolve(`${tokensDir}/$themes.json`), 'utf-8')) as ThemeObject[];
const $themes = (
JSON.parse(await fs.readFile(path.resolve(`${tokensDir}/$themes.json`), 'utf-8')) as ThemeObject[]
).map(processThemeObject);

const relevant$themes = $themes
.map(processThemeObject)
// We only use the 'default' theme for the 'size' group
.filter((theme) => R.not(theme.group === 'size' && theme.name !== 'default'));

if (!buildOptions.accentColor) {
const firstMainColor = relevant$themes.find((theme) => theme.group === 'main-color');
buildOptions.accentColor = firstMainColor?.name;
}

console.log('default accent color:', buildOptions.accentColor);

const buildAndSdConfigs = R.map(
(val: BuildConfig) => ({
buildConfig: val,
Expand Down
Loading

0 comments on commit f304115

Please sign in to comment.