Skip to content

Commit

Permalink
feat(css,theme,react): use data-color in all components, with color i…
Browse files Browse the repository at this point in the history
…nheritance for some (#2703)

React components and css now support custom colors through the
`data-color` attribute.

**BREAKING CHANGE**: All React components that had a `color` prop have
been changed to use `data-color`.

All<sup>1</sup> css targeting `data-color` has been changed to work with
all custom colors generated by the CLI.

`Avatar`, `Badge`, `Button`, and `Link` use
`--ds-color-accent-*`<sup>2</sup>, unless `data-color` is set directly
on the element.

For components that had a `color` prop, but defaulted to something other
than `"accent"`, `data-color` must also be set directly on the element.

All other components that defaulted to `"accent"`, or previously only
existed in `"accent"` color, now support `data-color`. They will also
inherit their color from the closest `data-color` attribute. If none is
found, they use `--ds-color-accent-*`<sup>2</sup>.

<sup>1</sup>: ...except `Alert`, which only supports `info`, `warning`,
`danger` and `success` colors.
<sup>2</sup>: If an `"accent"` color is not defined in the theme, the
`--ds-color-accent-*` variables will point to the first `main-color`.

---------

Co-authored-by: Michael Marszalek <[email protected]>
  • Loading branch information
unekinn and mimarz authored Nov 20, 2024
1 parent a16e83e commit 1767724
Show file tree
Hide file tree
Showing 84 changed files with 1,069 additions and 959 deletions.
21 changes: 21 additions & 0 deletions .changeset/rude-lies-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@digdir/designsystemet-react": major
"@digdir/designsystemet-css": minor
"@digdir/designsystemet-theme": minor
"@digdir/designsystemet": minor
---

React components and css now support custom colors through the `data-color` attribute.

**BREAKING CHANGE**: All React components that had a `color` prop have been changed to use `data-color`.

All<sup>1</sup> css targeting `data-color` has been changed to work with all custom colors generated by the CLI.

`Avatar`, `Badge`, `Button`, and `Link` use `--ds-color-accent-*`<sup>2</sup>, unless `data-color` is set directly on the element.

For components that had a `color` prop, but defaulted to something other than `"accent"`, `data-color` must also be set directly on the element.

All other components that defaulted to `"accent"`, or previously only existed in `"accent"` color, now support `data-color`. They will also inherit their color from the closest `data-color` attribute. If none is found, they use `--ds-color-accent-*`<sup>2</sup>.

<sup>1</sup>: ...except `Alert`, which only supports `info`, `warning`, `danger` and `success` colors.
<sup>2</sup>: If an `"accent"` color is not defined in the theme, the `--ds-color-accent-*` variables will point to the first `main-color`.
37 changes: 24 additions & 13 deletions apps/storybook/docs-components/CssVariables/CssVariables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,32 @@ export const CssVariables = forwardRef<HTMLTableElement, CssVariablesProps>(
function getCssVariables(css: string) {
const res: { [key: string]: string } = {};

/* get first block of css */
const cssBlock = css.match(/(?<={)([^}]*)/)?.[0];
if (!cssBlock) {
return res;
}

/* Create a temporary element */
const tempElement = document.createElement('div');
tempElement.style.cssText = cssBlock;
// temporarily remove inline strings, as they may contain ; and } characters
// and thus ruin the matching for property declarations
const stringsRemovedFromCss = Array.from(css.matchAll(/"[^"]*"/g)).map(
(x) => x[0],
);
const cssWithRemovedStrings = stringsRemovedFromCss.reduce(
(prev, curr, idx) => prev.replace(curr, `<placeholder-${idx}>`),
css,
);
// get all --dsc-* property declarations
const cssVars = Array.from(
cssWithRemovedStrings.matchAll(/(?<!var\()(--dsc-[^;}]+)[;}]/g),
).map((matches) => matches[1]);

/* Iterate over the CSS properties */
for (let i = 0; i < tempElement.style.length; i++) {
const name = tempElement.style[i];
if (name.startsWith('--dsc')) {
res[name] = tempElement.style.getPropertyValue(name).trim();
for (const declaration of cssVars) {
const [name, value] = declaration.split(':');
// Choose the earliest declaration of the property.
// We assume later declarations are part of a sub-selector.
if (!res[name]) {
// Return the original inline string from the value, if it was removed earlier
const valueWithOriginalString = value.replace(
/<placeholder-(\d+)>/,
(_, p1: string) => stringsRemovedFromCss[parseInt(p1)],
);
res[name] = valueWithOriginalString;
}
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
],
"scripts": {
"test": "vitest",
"test:cli": "yarn workspace @digdir/designsystemet test",
"test:cli": "yarn workspace @digdir/designsystemet test --verbose",
"test:storybook": "yarn workspace @designsystemet/storybook run-and-test-storybook",
"test:coverage": "vitest run --coverage",
"storybook": "yarn workspace @designsystemet/storybook dev",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
"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 -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": "yarn designsystemet tokens create -m dominant:#007682 secondary:#ff0000 -n #003333 -s support1:#12404f support2:#0054a6 support3:#942977 -w ./test-tokens-create",
"test:tokens-build": "yarn designsystemet tokens build -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",
"clean": "rimraf dist",
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/src/tokens/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type { BuildConfig, ThemePermutation } from './build/types.js';
import { makeEntryFile } from './build/utils/entryfile.js';
import { processThemeObject } from './build/utils/getMultidimensionalThemes.js';

export const DEFAULT_COLOR = 'accent';

type Options = {
/** Design tokens path */
tokens: string;
Expand Down Expand Up @@ -81,11 +83,15 @@ export async function buildTokens(options: Options): Promise<void> {
.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;
const accentOrFirstMainColor =
relevant$themes.find((theme) => theme.name === DEFAULT_COLOR) ||
relevant$themes.find((theme) => theme.group === 'main-color');
buildOptions.accentColor = accentOrFirstMainColor?.name;
}

console.log('default accent color:', buildOptions.accentColor);
if (buildOptions.accentColor !== DEFAULT_COLOR) {
console.log('accent color:', buildOptions.accentColor);
}

const buildAndSdConfigs = R.map(
(val: BuildConfig) => ({
Expand Down
62 changes: 46 additions & 16 deletions packages/cli/src/tokens/build/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import StyleDictionary from 'style-dictionary';
import type { Config as StyleDictionaryConfig, TransformedToken } from 'style-dictionary/types';
import { outputReferencesFilter } from 'style-dictionary/utils';

import { buildOptions } from '../build.js';
import { DEFAULT_COLOR, buildOptions } from '../build.js';
import { formats } from './formats/css.js';
import { jsTokens } from './formats/js-tokens.js';
import { nameKebab, resolveMath, sizeRem, typographyName } from './transformers.js';
Expand Down Expand Up @@ -70,7 +70,7 @@ export type GetStyleDictionaryConfig = (
options: {
outPath?: string;
},
) => StyleDictionaryConfig;
) => StyleDictionaryConfig | { config: StyleDictionaryConfig; permutationOverrides?: Partial<ThemePermutation> }[];

const colorModeVariables: GetStyleDictionaryConfig = ({ mode = 'light', theme }, { outPath }) => {
const selector = `${mode === 'light' ? ':root, ' : ''}[data-ds-color-mode="${mode}"]`;
Expand Down Expand Up @@ -114,7 +114,7 @@ const colorCategoryVariables =
const isDefault = color === buildOptions?.accentColor;
const selector = `${isDefault ? ':root, ' : ''}[data-color="${color}"]`;

return {
const config: StyleDictionaryConfig = {
usesDtcg,
preprocessors: ['tokens-studio'],
platforms: {
Expand Down Expand Up @@ -143,6 +143,31 @@ const colorCategoryVariables =
},
},
};
if (isDefault && color !== DEFAULT_COLOR) {
console.log(
`Creating "${DEFAULT_COLOR}" color variables pointing to "${color}", since a color named "${DEFAULT_COLOR}" is not defined`,
);
// Create a --ds-color-accent-* scale which points to the default color
const defaultColorConfig = R.mergeDeepRight(config, {
platforms: {
css: {
selector: ':root',
files: [
{
...config.platforms?.css?.files?.[0],
destination: `color/${DEFAULT_COLOR}.css`,
},
],
options: { replaceCategoryWith: DEFAULT_COLOR },
},
},
} satisfies StyleDictionaryConfig);
return [
{ config },
{ config: defaultColorConfig, permutationOverrides: { 'main-color': `${DEFAULT_COLOR}${color}` } },
];
}
return config;
};

const semanticVariables: GetStyleDictionaryConfig = ({ theme }, { outPath }) => {
Expand Down Expand Up @@ -308,24 +333,29 @@ export const getConfigsForThemeDimensions = (

const permutations = getMultidimensionalThemes(themes, dimensions);
return permutations
.map(({ selectedTokenSets, permutation }) => {
.flatMap(({ selectedTokenSets, permutation }) => {
const setsWithPaths = selectedTokenSets.map((x) => `${tokensDir}/${x}.json`);

const [source, include] = paritionPrimitives(setsWithPaths);

const config_ = getConfig(permutation, { outPath });

const config: StyleDictionaryConfig = {
...config_,
log: {
...config_?.log,
verbosity: buildOptions?.verbose ? 'verbose' : 'silent',
},
source,
include,
};
const configOrConfigs = getConfig(permutation, { outPath });
const configs_ = Array.isArray(configOrConfigs) ? configOrConfigs : [{ config: configOrConfigs }];

return { permutation, config };
const configs: SDConfigForThemePermutation[] = configs_.map(({ config, permutationOverrides }) => {
return {
permutation: { ...permutation, ...permutationOverrides },
config: {
...config,
log: {
...config?.log,
verbosity: buildOptions?.verbose ? 'verbose' : 'silent',
},
source,
include,
},
};
});
return configs;
})
.sort();
};
13 changes: 11 additions & 2 deletions packages/cli/src/tokens/build/formats/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,16 @@ const colormode: Format = {
},
};

declare module 'style-dictionary/types' {
export interface LocalOptions {
replaceCategoryWith?: string;
}
}

const colorcategory: Format = {
name: 'ds/css-colorcategory',
format: async ({ dictionary, file, options, platform }) => {
const { outputReferences, usesDtcg } = options;
const { outputReferences, usesDtcg, replaceCategoryWith = '' } = options;
const { selector, layer } = platform;

const header = await fileHeader({ file });
Expand All @@ -72,7 +78,10 @@ const colorcategory: Format = {
}),
(token: TransformedToken) => ({
...token,
name: token.name.replace(new RegExp(`-(${colorCategories.main}|${colorCategories.support})-`), '-'),
name: token.name.replace(
new RegExp(`-(${colorCategories.main}|${colorCategories.support})-`),
replaceCategoryWith ? `-${replaceCategoryWith}-` : '-',
),
}),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TokenSetStatus } from '@tokens-studio/types';
import chalk from 'chalk';
import { kebabCase } from 'change-case';
import * as R from 'ramda';
import { buildOptions } from '../../build';
import { buildOptions } from '../../build.js';
import type { ThemeDimension, ThemePermutation } from '../types';

/**
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/tokens/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import type { ThemeObject } from '@tokens-studio/types';
import * as R from 'ramda';
import originalColorJson from '../../../../design-tokens/semantic/color.json';
import originalColorCategoryJson from '../../../../design-tokens/semantic/modes/main-color/accent.json';
import originalThemeJson from '../../../../design-tokens/themes/theme.json';
import originalColorJson from '../../../../design-tokens/semantic/color.json' with { type: 'json' };
import originalColorCategoryJson from '../../../../design-tokens/semantic/modes/main-color/accent.json' with {
type: 'json',
};
import originalThemeJson from '../../../../design-tokens/themes/theme.json' with { type: 'json' };
import { stringify } from './write';

const DIRNAME: string = import.meta.dirname || __dirname;
Expand Down
14 changes: 9 additions & 5 deletions packages/cli/src/tokens/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import type { ThemeObject } from '@tokens-studio/types';
import chalk from 'chalk';
import * as R from 'ramda';
import type { ColorMode } from '../colors/types.js';
import semanticColorBaseFile from './design-tokens/template/semantic/color-base-file.json';
import customColorTemplate from './design-tokens/template/semantic/modes/category-color/category-color-template.json';
import semanticColorTemplate from './design-tokens/template/semantic/semantic-color-template.json';
import themeBaseFile from './design-tokens/template/themes/theme-base-file.json';
import themeColorTemplate from './design-tokens/template/themes/theme-color-template.json';
import semanticColorBaseFile from './design-tokens/template/semantic/color-base-file.json' with { type: 'json' };
import customColorTemplate from './design-tokens/template/semantic/modes/category-color/category-color-template.json' with {
type: 'json',
};
import semanticColorTemplate from './design-tokens/template/semantic/semantic-color-template.json' with {
type: 'json',
};
import themeBaseFile from './design-tokens/template/themes/theme-base-file.json' with { type: 'json' };
import themeColorTemplate from './design-tokens/template/themes/theme-color-template.json' with { type: 'json' };
import type { Collection, Colors, File, Tokens, TokensSet, TypographyModes } from './types.js';
import { generateMetadataJson } from './write/generate$metadata.js';
import { generateThemesJson } from './write/generate$themes.js';
Expand Down
66 changes: 30 additions & 36 deletions packages/css/accordion.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
.ds-accordion-group {
/* default color: neutral */
--dsc-accordion-background: var(--ds-color-neutral-background-default);
--dsc-accordion-heading-background--hover: var(--ds-color-neutral-surface-default);
--dsc-accordion-heading-background--open: var(--ds-color-neutral-background-subtle);
--dsc-accordion-heading-background: var(--ds-color-neutral-background-default);
--dsc-accordion-border-color: var(--ds-color-neutral-border-subtle);

&[data-color]:where(:not([data-color='subtle'])) {
--dsc-accordion-background: var(--ds-color-background-subtle);
--dsc-accordion-heading-background--hover: var(--ds-color-surface-hover);
--dsc-accordion-heading-background--open: var(--ds-color-surface-default);
--dsc-accordion-heading-background: var(--ds-color-surface-default);
--dsc-accordion-border-color: var(--ds-color-border-subtle);
}

&[data-color='neutral'] {
--dsc-accordion-background: var(--ds-color-background-default);
--dsc-accordion-heading-background--hover: var(--ds-color-surface-default);
--dsc-accordion-heading-background--open: var(--ds-color-background-subtle);
--dsc-accordion-heading-background: var(--ds-color-background-default);
}

&[data-color='subtle'] {
--dsc-accordion-background: var(--ds-color-neutral-background-subtle);
--dsc-accordion-heading-background--hover: var(--ds-color-neutral-surface-hover);
--dsc-accordion-heading-background--open: var(--ds-color-neutral-surface-default);
--dsc-accordion-heading-background: var(--ds-color-neutral-background-subtle);
}

--dsc-accordion-border: 1px solid var(--dsc-accordion-border-color);
--dsc-accordion-border-radius: var(--ds-border-radius-md);
--dsc-accordion-border: 1px solid var(--ds-color-neutral-border-subtle);
--dsc-accordion-chevron-gap: var(--ds-spacing-2);
--dsc-accordion-chevron-size: var(--ds-spacing-6);
--dsc-accordion-chevron-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5.97 9.47a.75.75 0 0 1 1.06 0L12 14.44l4.97-4.97a.75.75 0 1 1 1.06 1.06l-5.5 5.5a.75.75 0 0 1-1.06 0l-5.5-5.5a.75.75 0 0 1 0-1.06'/%3E%3C/svg%3E");
--dsc-accordion-heading-background--hover: var(--ds-color-neutral-surface-default);
--dsc-accordion-heading-background--open: var(--ds-color-neutral-background-subtle);
--dsc-accordion-heading-background: var(--ds-color-neutral-background-default);

--dsc-accordion-padding: var(--ds-spacing-2) var(--ds-spacing-4);
--dsc-accordion-size: var(--ds-sizing-14);

Expand All @@ -26,38 +52,6 @@
border-bottom-right-radius: var(--dsc-accordion-border-radius);
}
}

&[data-color='subtle'] {
--dsc-accordion-background: var(--ds-color-neutral-background-subtle);
--dsc-accordion-border: 1px solid var(--ds-color-neutral-border-subtle);
--dsc-accordion-heading-background--hover: var(--ds-color-neutral-surface-hover);
--dsc-accordion-heading-background--open: var(--ds-color-neutral-surface-default);
--dsc-accordion-heading-background: var(--ds-color-neutral-background-subtle);
}

&[data-color='brand1'] {
--dsc-accordion-background: var(--ds-color-brand1-background-subtle);
--dsc-accordion-border: 1px solid var(--ds-color-brand1-border-subtle);
--dsc-accordion-heading-background--hover: var(--ds-color-brand1-surface-hover);
--dsc-accordion-heading-background--open: var(--ds-color-brand1-surface-default);
--dsc-accordion-heading-background: var(--ds-color-brand1-surface-default);
}

&[data-color='brand2'] {
--dsc-accordion-background: var(--ds-color-brand2-background-subtle);
--dsc-accordion-border: 1px solid var(--ds-color-brand2-border-subtle);
--dsc-accordion-heading-background--hover: var(--ds-color-brand2-surface-hover);
--dsc-accordion-heading-background--open: var(--ds-color-brand2-surface-default);
--dsc-accordion-heading-background: var(--ds-color-brand2-surface-default);
}

&[data-color='brand3'] {
--dsc-accordion-background: var(--ds-color-brand3-background-subtle);
--dsc-accordion-border: 1px solid var(--ds-color-brand3-border-subtle);
--dsc-accordion-heading-background--hover: var(--ds-color-brand3-surface-hover);
--dsc-accordion-heading-background--open: var(--ds-color-brand3-surface-default);
--dsc-accordion-heading-background: var(--ds-color-brand3-surface-default);
}
}

.ds-accordion__item {
Expand Down
Loading

0 comments on commit 1767724

Please sign in to comment.