Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(css,theme,react): use data-color in all components, with color inheritance for some #2703

Merged
merged 20 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8eb0c56
fix(storybook): incorrect css variables shown in List docs
unekinn Nov 12, 2024
52cbe7e
fix(storybook): list all --dsc- properties in CssVariables regardless…
unekinn Nov 14, 2024
67ca1e7
refactor(css,react): rewrite existing data-color components to use ge…
unekinn Oct 19, 2024
fe45e17
feat(css): change all components from color-accent to generic, inheri…
unekinn Oct 29, 2024
e7867a4
feat(react): remove default accent color, let it be inherited through…
unekinn Oct 29, 2024
55520e6
fix(test): remove check for data-color="accent" in Avatar test
unekinn Oct 29, 2024
102e1b2
fix(css,react): spinner with multiple colors
unekinn Oct 30, 2024
dd9d194
fix(cli): require an "accent" color
unekinn Nov 15, 2024
195b776
feat(react): add very permissible type to data-color
unekinn Nov 12, 2024
c6cc412
feat(react): color -> data-color + document data-{color,size} for all…
unekinn Nov 15, 2024
278f2bb
fix(css,react): avatar, button and link default to accent color
unekinn Nov 15, 2024
67db79b
fix: Merge -> MergeRight
unekinn Nov 18, 2024
58ecb78
fix: remove "import ... from 'packages/react/..."
unekinn Nov 18, 2024
d69f109
fix(css): 🚫 --dsc-tabs__ ✅ --dsc-tabs-
unekinn Nov 18, 2024
092d302
fix(cli): don't require "accent", but generate accent vars if not def…
unekinn Nov 18, 2024
b08ade8
fix(css): button variants should also default to accent
unekinn Nov 19, 2024
131db58
docs(react): simplify data-color jsdoc
unekinn Nov 19, 2024
0193499
Add changeset
unekinn Nov 19, 2024
54241b5
fix cli errors using downstream
mimarz Nov 20, 2024
44ea581
Merge branch 'next' into feat/use-data-colors-on-all-components
mimarz Nov 20, 2024
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
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
Loading