diff --git a/.changeset/dry-bugs-sniff.md b/.changeset/dry-bugs-sniff.md new file mode 100644 index 0000000000..21086363f3 --- /dev/null +++ b/.changeset/dry-bugs-sniff.md @@ -0,0 +1,5 @@ +--- +'@sumup/design-tokens': minor +--- + +Exported all CSS custom properties as `@sumup/design-tokens/light.css`. diff --git a/.changeset/flat-lamps-glow.md b/.changeset/flat-lamps-glow.md index 6ff9e6ee01..3726577850 100644 --- a/.changeset/flat-lamps-glow.md +++ b/.changeset/flat-lamps-glow.md @@ -1,5 +1,4 @@ --- -'@sumup/design-tokens': major '@sumup/circuit-ui': major '@sumup/icons': major --- diff --git a/.changeset/grumpy-donuts-flash.md b/.changeset/grumpy-donuts-flash.md index 176f00947c..c56c202a56 100644 --- a/.changeset/grumpy-donuts-flash.md +++ b/.changeset/grumpy-donuts-flash.md @@ -1,5 +1,4 @@ --- -'@sumup/design-tokens': major '@sumup/circuit-ui': major '@sumup/icons': major --- diff --git a/.changeset/late-students-own.md b/.changeset/late-students-own.md new file mode 100644 index 0000000000..612740b977 --- /dev/null +++ b/.changeset/late-students-own.md @@ -0,0 +1,5 @@ +--- +'@sumup/eslint-plugin-circuit-ui': minor +--- + +Added `circuit-ui/prefer-custom-properties` rule to replace the Emotion.js theme with CSS custom properties. diff --git a/.changeset/ninety-months-end.md b/.changeset/ninety-months-end.md new file mode 100644 index 0000000000..8835a2b494 --- /dev/null +++ b/.changeset/ninety-months-end.md @@ -0,0 +1,5 @@ +--- +'@sumup/design-tokens': minor +--- + +Deprecated the Emotion.js theme in favor of CSS custom properties. Use the [`circuit-ui/prefer-custom-properties`](https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui/prefer-custom-properties) ESLint rule to automatically migrate your code. diff --git a/.changeset/red-experts-lie.md b/.changeset/red-experts-lie.md index 2d752d9fb6..a0a93b73b4 100644 --- a/.changeset/red-experts-lie.md +++ b/.changeset/red-experts-lie.md @@ -1,5 +1,4 @@ --- -'@sumup/design-tokens': major '@sumup/circuit-ui': major '@sumup/icons': major --- diff --git a/.eslintrc.js b/.eslintrc.js index e534fed5ce..03c1764c9d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,6 +13,7 @@ module.exports = require('@sumup/foundry/eslint')({ }, rules: { '@sumup/circuit-ui/no-invalid-custom-properties': 'error', + '@sumup/circuit-ui/prefer-custom-properties': 'warn', 'react/no-unknown-property': ['error', { ignore: ['css'] }], }, parserOptions: { diff --git a/.storybook/components/BorderRadius.tsx b/.storybook/components/BorderRadius.tsx deleted file mode 100644 index 4ed8d14b7b..0000000000 --- a/.storybook/components/BorderRadius.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import styled from '@emotion/styled'; -import { css } from '@emotion/react'; -import { light, Theme } from '@sumup/design-tokens'; -import { Body, spacing } from '@sumup/circuit-ui'; - -interface BorderRadiusProps { - size: keyof Theme['borderRadius']; -} - -const Box = styled.div( - ({ theme, size }) => css` - width: ${theme.spacings.tera}; - height: ${theme.spacings.tera}; - border-radius: ${theme.borderRadius[size]}; - background-color: var(--cui-bg-accent-strong); - margin-right: ${theme.spacings.mega}; - `, -); - -const Wrapper = styled.div( - ({ theme }) => css` - display: flex; - align-items: center; - margin-bottom: ${theme.spacings.mega}; - `, -); - -const BorderRadius = ({ size, ...props }: BorderRadiusProps) => ( - - -
- {size} - - {light.borderRadius[size]} - -
-
-); - -export default BorderRadius; diff --git a/.storybook/components/BorderWidth.tsx b/.storybook/components/BorderWidth.tsx deleted file mode 100644 index f48fc0ed8a..0000000000 --- a/.storybook/components/BorderWidth.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import styled from '@emotion/styled'; -import { css, ThemeProvider } from '@emotion/react'; -import { light, Theme } from '@sumup/design-tokens'; -import { Body, spacing } from '@sumup/circuit-ui'; - -interface BorderWidthProps { - size: keyof Theme['borderWidth']; -} - -const Box = styled.div( - ({ theme, size }) => css` - width: ${theme.spacings.tera}; - height: ${theme.spacings.tera}; - border-radius: ${theme.borderRadius.bit}; - border: ${theme.borderWidth[size]} solid var(--cui-border-accent); - background-color: var(--cui-bg-normal); - margin-right: ${theme.spacings.mega}; - `, -); - -const Wrapper = styled.div` - ${({ theme }) => css` - display: flex; - align-items: center; - margin-bottom: ${theme.spacings.mega}; - `}; -`; - -const BorderWidth = ({ size }: BorderWidthProps) => ( - - - -
- {size} - - {light.borderWidth[size]} - -
-
-
-); - -export default BorderWidth; diff --git a/.storybook/components/ColorsTable.tsx b/.storybook/components/ColorsTable.tsx deleted file mode 100644 index f15a81460b..0000000000 --- a/.storybook/components/ColorsTable.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright 2023, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { useEffect, useState } from 'react'; -import { css, ThemeProvider } from '@emotion/react'; -import { Unstyled } from '@storybook/addon-docs'; -import { light, schema } from '@sumup/design-tokens'; -import { - Anchor, - spacing, - Table, - ToastProvider, - useNotificationToast, -} from '@sumup/circuit-ui'; - -function filterCustomProperties(usage: string): string[] { - return schema - .filter( - (token) => - token.type === 'color' && token.name.startsWith(`--cui-${usage}`), - ) - .map((token) => token.name); -} - -function getCustomProperty(property: string) { - return getComputedStyle(document.documentElement).getPropertyValue(property); -} - -function Copy({ property }: { property: string }) { - const { setToast } = useNotificationToast(); - return ( - - navigator.clipboard - .writeText(property) - .then(() => setToast({ body: 'Copied!', variant: 'confirm' })) - } - > - Copy - - ); -} - -const getRows = (customProperties: string[][]) => { - return customProperties.map(([property, value]) => [ - { - children: ( -
- {property} - -
- ), - }, - { children: {value} }, - { - align: 'right' as const, - children: ( - - ), - }, - ]); -}; - -function ColorsTable({ usage }: { usage: string }) { - const [colors, setColors] = useState(); - - useEffect(() => { - const customProperties = filterCustomProperties(usage); - setColors( - customProperties.map((property) => [ - property, - getCustomProperty(property), - ]), - ); - }, [usage]); - - return ( - - - - {colors && ( - - )} - - - - ); -} - -export default ColorsTable; diff --git a/.storybook/components/IconSize.tsx b/.storybook/components/IconSize.tsx deleted file mode 100644 index 25fc1c36aa..0000000000 --- a/.storybook/components/IconSize.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import styled from '@emotion/styled'; -import { css, ThemeProvider } from '@emotion/react'; -import { light, Theme } from '@sumup/design-tokens'; -import { Body, spacing } from '@sumup/circuit-ui'; - -interface IconSizeProps { - size: keyof Theme['iconSizes']; -} - -const Box = styled.div( - ({ theme, size }) => css` - width: ${theme.iconSizes[size]}; - height: ${theme.iconSizes[size]}; - background-color: var(--cui-bg-accent-strong); - margin-right: ${theme.spacings.mega}; - `, -); - -const Wrapper = styled.div( - ({ theme }) => css` - display: flex; - align-items: center; - margin-bottom: ${theme.spacings.mega}; - `, -); - -const IconSize = ({ size }: IconSizeProps) => ( - - - -
- - {size} - - - {light.iconSizes[size]} - -
-
-
-); - -export default IconSize; diff --git a/.storybook/components/MediaQueriesTable.tsx b/.storybook/components/MediaQueriesTable.tsx deleted file mode 100644 index 7c197faec5..0000000000 --- a/.storybook/components/MediaQueriesTable.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { useTheme, ThemeProvider } from '@emotion/react'; -import { light } from '@sumup/design-tokens'; -import { Table } from '@sumup/circuit-ui'; - -const HEADERS = ['Breakpoint name', 'Query']; - -const TableWrapper = () => { - const theme = useTheme(); - return ( -
-
[ - bp, - theme.breakpoints[bp], - ])} - /> - - ); -}; - -const MediaQueriesTable = () => ( - - - -); - -export default MediaQueriesTable; diff --git a/.storybook/components/Spacing.tsx b/.storybook/components/Spacing.tsx deleted file mode 100644 index cbd4f6a8ac..0000000000 --- a/.storybook/components/Spacing.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import styled from '@emotion/styled'; -import { css, ThemeProvider } from '@emotion/react'; -import { light, Theme } from '@sumup/design-tokens'; -import { Body, spacing } from '@sumup/circuit-ui'; - -interface SpacingProps { - spacingName: keyof Theme['spacings']; -} - -const Box = styled.div( - ({ theme, spacingName }) => css` - width: ${theme.spacings[spacingName]}; - height: ${theme.spacings[spacingName]}; - background-color: var(--cui-bg-accent-strong); - margin-right: ${theme.spacings.mega}; - `, -); - -const Wrapper = styled.div( - ({ theme }) => css` - display: flex; - align-items: center; - margin-bottom: ${theme.spacings.mega}; - `, -); - -const Spacing = ({ spacingName }: SpacingProps) => ( - - - -
- - {spacingName} - - - {light.spacings[spacingName]} - -
-
-
-); - -export default Spacing; diff --git a/.storybook/components/Theme.tsx b/.storybook/components/Theme.tsx new file mode 100644 index 0000000000..e3adef83e7 --- /dev/null +++ b/.storybook/components/Theme.tsx @@ -0,0 +1,281 @@ +/** + * Copyright 2023, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentType, useEffect, useState } from 'react'; +import { ThemeProvider, useTheme } from '@emotion/react'; +import { Unstyled } from '@storybook/addon-docs'; +import { light, schema } from '@sumup/design-tokens'; +import { SumUpLogomark } from '@sumup/icons'; +import { + Anchor, + Table, + TableHeaderCell, + TableRow, + ToastProvider, + useNotificationToast, +} from '@sumup/circuit-ui'; + +type CustomPropertyName = `--cui-${string}`; +type CustomPropertyValue = string; +type CustomProperty = [CustomPropertyName, CustomPropertyValue]; +type CustomProperties = CustomProperty[]; + +type PreviewProps = { name: CustomPropertyName }; +type PreviewComponent = ComponentType; + +function filterCustomProperties( + namespace: string, + type?: string, +): CustomPropertyName[] { + return schema + .filter((token) => { + const isNamespace = token.name.startsWith(`--cui-${namespace}`); + const isType = type ? token.type === type : true; + return isNamespace && isType; + }) + .map((token) => token.name); +} + +function getCustomPropertyValue(name: CustomPropertyName): CustomPropertyValue { + return getComputedStyle(document.documentElement).getPropertyValue(name); +} + +function CopyButton({ name }: { name: CustomPropertyName }) { + const { setToast } = useNotificationToast(); + return ( + + navigator.clipboard + .writeText(name) + .then(() => setToast({ body: 'Copied!', variant: 'success' })) + } + > + Copy + + ); +} + +function getRows( + customProperties: CustomProperties, + Preview?: PreviewComponent, +) { + return customProperties.map(([name, value]) => { + const row: TableRow = [ + { + children: ( +
+ {name} + +
+ ), + }, + { children: {value} }, + ]; + + if (Preview) { + row.push({ + align: 'right', + children: , + }); + } + + return row; + }); +} + +interface CustomPropertiesTableProps { + namespace: string; + preview?: PreviewComponent; + type?: string; +} + +export function CustomPropertiesTable({ + namespace, + type, + preview, +}: CustomPropertiesTableProps) { + const [customProperties, setCustomProperties] = useState(); + + useEffect(() => { + const names = filterCustomProperties(namespace, type); + setCustomProperties( + names.map((name) => [name, getCustomPropertyValue(name)]), + ); + }, [namespace, type]); + + const headers: TableHeaderCell[] = ['Property name', 'Value']; + + if (preview) { + headers.push({ children: 'Preview', align: 'right' }); + } + + return ( + + + + {customProperties && ( +
+ )} + + + + ); +} + +export function BorderRadius({ name }: PreviewProps) { + return ( +
+ ); +} + +export function BorderWidth({ name }: PreviewProps) { + return ( +
+ ); +} + +export function Color({ name }: PreviewProps) { + return ( + + ); +} + +export function FontStack({ name }: PreviewProps) { + return ( +

+ Lorem ipsum +

+ ); +} + +export function FontWeight({ name }: PreviewProps) { + return ( +

+ Lorem ipsum +

+ ); +} + +export function IconSize({ name }: PreviewProps) { + return ( + + ); +} + +export function Spacing({ name }: PreviewProps) { + return ( +
+ ); +} + +export function Transition({ name }: PreviewProps) { + const [scale, setScale] = useState(1); + + useEffect(() => { + const id = setInterval(() => { + setScale((prev) => (prev === 100 ? 50 : 100)); + }, 2000); + + return () => { + clearInterval(id); + }; + }, []); + + return ( +
+ ); +} + +const HEADERS = ['Media query name', 'Value']; + +function TableWrapper() { + const theme = useTheme(); + return ( + +
[ + { children: {`theme.mq.${bp}`} }, + { children: {theme.mq[bp]} }, + ])} + /> + + ); +} + +export function MediaQueriesTable() { + return ( + + + + ); +} diff --git a/.storybook/components/Type.tsx b/.storybook/components/Type.tsx deleted file mode 100644 index bba7d5db21..0000000000 --- a/.storybook/components/Type.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Unstyled } from '@storybook/addon-docs'; -import { Fragment, createElement, ComponentType } from 'react'; -import styled from '@emotion/styled'; -import { css, ThemeProvider } from '@emotion/react'; -import { light } from '@sumup/design-tokens'; - -import { Body } from '@sumup/circuit-ui'; - -interface TypeProps { - component: ComponentType; - name: string; - size?: string; - fontWeight?: string; -} - -const TypePx = styled(Body)( - ({ theme }) => css` - margin-left: ${theme.spacings.mega}; - margin-bottom: ${theme.spacings.giga}; - text-transform: lowercase; - `, -); - -const Type = ({ size, component, name, fontWeight, ...props }: TypeProps) => { - // The fontSize can be either on typography[name][size] (body, headline) or - // on typography[name] directly (subHeadline). - const { fontSize, lineHeight } = - light.typography[name][size] || light.typography[name]; - const weight = light.fontWeight[fontWeight]; - - return ( - - - {createElement(component, { - children: ( - - This is {name} {size ? `size ${size}` : ''} - - {weight ? `${weight}` : `${fontSize}, ${lineHeight}`} - - - ), - size, - ...props, - })} - - - ); -}; - -export default Type; diff --git a/.storybook/components/index.ts b/.storybook/components/index.ts index 3455512809..9d7c88c590 100644 --- a/.storybook/components/index.ts +++ b/.storybook/components/index.ts @@ -25,16 +25,22 @@ export { default as DocsContainer } from './DocsContainer'; export { default as Status } from './Statuses'; export { default as Preview } from './Preview'; export { default as Story } from './Story'; -export { default as Type } from './Type'; -export { default as Spacing } from './Spacing'; -export { default as MediaQueriesTable } from './MediaQueriesTable'; -export { default as ColorsTable } from './ColorsTable'; -export { default as BorderWidth } from './BorderWidth'; -export { default as BorderRadius } from './BorderRadius'; -export { default as IconSize } from './IconSize'; export { default as Icons } from './Icons'; export { default as Intro } from './Intro'; export { default as Teaser } from './Teaser'; export { default as Link } from './Link'; export { default as Image } from './Image'; export { default as Stack } from './Stack'; + +export { + CustomPropertiesTable, + Color, + Spacing, + IconSize, + BorderRadius, + BorderWidth, + FontStack, + FontWeight, + Transition, + MediaQueriesTable, +} from './Theme'; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 8239f4d42d..c365e2aebf 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,3 +1,5 @@ +import '@sumup/design-tokens/light.css'; + import { light, components } from './themes'; import { withThemeProvider } from './decorators/withThemeProvider'; import { withUnmountWhenHidden } from './decorators/withUnmountWhenHidden'; diff --git a/docs/features/1-theme.mdx b/docs/features/1-theme.mdx index b7908f09ca..4c17ab3889 100644 --- a/docs/features/1-theme.mdx +++ b/docs/features/1-theme.mdx @@ -2,13 +2,16 @@ import { Intro, Meta, Preview, + MediaQueriesTable, + CustomPropertiesTable, + Color, Spacing, IconSize, BorderRadius, BorderWidth, - Type, - MediaQueriesTable, - ColorsTable, + FontStack, + FontWeight, + Transition, } from '../../.storybook/components'; import { Headline, SubHeadline, Body } from '@sumup/circuit-ui'; @@ -18,152 +21,83 @@ import { Headline, SubHeadline, Body } from '@sumup/circuit-ui'; -The theme used throughout Circuit UI comes from SumUp's design token library, [`@sumup/design-tokens`](Packages/design-tokens/Docs), which is a required peer dependency of `@sumup/circuit-ui`. Refer to its [documentation](Packages/design-tokens/Docs) for usage instructions. +The theme used throughout Circuit UI comes from SumUp's design token library, [`@sumup/design-tokens`](Packages/design-tokens/Docs), which is a required peer dependency of `@sumup/circuit-ui`. Refer to its [documentation](Packages/design-tokens/Docs) for installation and usage instructions. -## Colors - -Circuit UI colors have moved from color scales (e.g. `g900`) to semantic color tokens (e.g. `fg-success`). This makes theme customizations easier and more reliable, and enables theming for sub-brands or for supporting multiple color modes. For details and usage guidelines, refer to the [Figma documentation](https://www.figma.com/file/OgPQeoNZ2QoY7hZvy0ybk2/%F0%9F%8C%88-COLOR-TOKENS?node-id=913%3A3903&t=b9BsTOJnzKDomZ9E-4) (_internal link_). Uses of legacy colors (e.g. `color: ${theme.colors.g900};`) should be replaced with semantic color tokens (e.g. `color: var(--cui-fg-success)`). +Circuit UI is moving from the JavaScript theme for Emotion.js to [CSS custom properties](https://www.w3.org/TR/css-variables-1/) (aka CSS variables) to improve performance and compatibility with other frameworks. We recommend installing the [`@sumup/eslint-plugin-circuit-ui`](Packages/eslint-plugin-circuit-ui/Docs) ESLint plugin, and turning on the `prefer-custom-properties` and `no-invalid-custom-properties` rules to help with the migration. -While legacy colors are part of the JS theme (exported from `@sumup/design-tokens`), semantic color tokens are declared as [CSS custom properties](https://www.w3.org/TR/css-variables-1/) (CSS variables). This has performance benefits over CSS-in-JS theming. The custom properties are declared in the Circuit UI `BaseStyles` from version 6.1.0. Eventually, all design tokens will be declared as CSS custom properties and imported as a CSS stylesheet. +## Colors -To make it easier to work with custom properties, we recommend using the [`@sumup/eslint-plugin-circuit-ui`](Packages/eslint-plugin-circuit-ui/Docs) ESLint plugin, and turning on the `no-invalid-custom-properties` rule. +Colors have moved from color scales (e.g. `g900`) to semantic color tokens (e.g. `fg-success`). This makes theme customizations easier and more reliable, and enables theming for sub-brands or for supporting multiple color modes. For details and usage guidelines, refer to the [Figma documentation](https://www.figma.com/file/OgPQeoNZ2QoY7hZvy0ybk2/%F0%9F%8C%88-COLOR-TOKENS?node-id=913%3A3903&t=b9BsTOJnzKDomZ9E-4) (_internal link_). Uses of legacy colors (e.g. `color: ${theme.colors.g900};`) should be replaced with semantic color tokens (e.g. `color: var(--cui-fg-success)`). ### Background colors - + ### Foreground colors - + ### Border colors - + ## Spacings -``` -theme.spacings.[bit|byte|kilo|mega|giga|tera|peta|exa|zetta] -``` - Use spacings for gutters, margins, and paddings. Don't use it for border width, border radius, icon size, font size, or line height. Use the dedicated theme properties instead. - - - - - - - - - - - + ## Icon sizes -``` -theme.iconSizes.[kilo|mega|giga] -``` - - - - - - + ## Border radius -``` -theme.borderRadius.[bit|byte|kilo|mega|circle|pill] -``` - - - - - - - - - + ## Border width -``` -theme.borderWidth.[kilo|mega] -``` - - - - - + ## Typography -``` -theme.typography.[headline|subHeadline|body]..[fontSize|lineHeight] -``` +Avoid using the `var(--cui-typography-*)` CSS custom properties directly in your styles. Instead, use the typography components [`Title`](Typography/Title/Docs), [`Headline`](Typography/Headline/Docs), [`SubHeadline`](Typography/SubHeadline/Docs), [`Body`](Typography/Body/Docs), and [`BodyLarge`](Typography/BodyLarge/Docs). -Avoid using `theme.typography` directly in your styles, rather use the specialized components [`Headline`](Typography/Headline/Base), [`SubHeadline`](Typography/SubHeadline/Base), and [`Body`](Typography/Body/Base). +## Font stack -### Headlines + - - - - - - +## Font weight -### Subheadline + - - - +Pass `variant="highlight"` to the `Body` component to make it bold. The font weight of the `Title`, `Headline` and `SubHeadline` components is bold by default and should not be overwritten. -### Body +## Transitions - - - - + -## Font stack +## Z-index -``` -theme.fontStack.[default|mono] -``` +Z-indices for various components to ensure they stack properly. -## Font weight + -``` -theme.fontWeight.[regular|bold] -``` - -You can only toggle the boldness of `Body`, but not `Headline` or `SubHeadline`. +## Legacy - - - - +The following properties haven't been translated to CSS custom properties and are only available as part of the JS theme. They will be removed eventually. -## Grid +### Grid ``` theme.grid..[priority|breakpoint|cols|maxWidth|gutter] ``` -Refer to the [grid](Layout/Grid) documentation for an overview of the grid system. +Refer to the [`Grid`](Layout/Grid/Docs) documentation for an overview of the grid system. -## Breakpoints and media queries +### Breakpoints and media queries ``` theme.mq.[untilKilo|kilo|kiloToMega|mega|untilMega|megaToGiga|giga|gigaToTera|tera] @@ -174,34 +108,12 @@ Avoid using `theme.breakpoints` directly, instead use the named media query help -### Usage in code - -```js -import styled from '@emotion/styled'; +```ts import { css } from '@emotion/react'; -const ResponsiveDiv = styled.div( - ({ theme }) => css` - ${theme.mq.mega} { - padding-top: ${theme.spacings.peta}; - } - `, -); -``` - -## Transitions - -``` -theme.transitions.[default|slow] +const styles = ({ theme }) => css` + ${theme.mq.mega} { + padding-top: ${theme.spacings.peta}; + } +`; ``` - -For now, there are just two transitions. In the future, when we -have animations, there may be more :) - -## Z-index - -``` -theme.zIndex.[default|absolute|input|modal|popover|sidebar|tooltip] -``` - -Z-indices for various components to ensure they stack properly. diff --git a/package-lock.json b/package-lock.json index 648bc8577e..556d972891 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12046,10 +12046,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", - "dev": true, + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "funding": [ { "type": "opencollective", @@ -12058,13 +12057,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" @@ -12296,10 +12299,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001478", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz", - "integrity": "sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw==", - "dev": true, + "version": "1.0.30001503", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001503.tgz", + "integrity": "sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==", "funding": [ { "type": "opencollective", @@ -13903,6 +13905,17 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -14255,10 +14268,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.365", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.365.tgz", - "integrity": "sha512-FRHZO+1tUNO4TOPXmlxetkoaIY8uwHzd1kKopK/Gx2SKn1L47wJXWD44wxP5CGRyyP98z/c8e1eBzJrgPeiBOg==", - "dev": true + "version": "1.4.433", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.433.tgz", + "integrity": "sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==" }, "node_modules/elegant-spinner": { "version": "1.0.1", @@ -14597,7 +14609,6 @@ "version": "3.1.1", "resolved": "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz", "integrity": "sha1-2M/ccACWXFoBdLSoLqpcBVJ0LkA= sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -20448,6 +20459,183 @@ "node": ">=4" } }, + "node_modules/lightningcss": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.21.0.tgz", + "integrity": "sha512-HDznZexdDMvC98c79vRE+oW5vFncTlLjJopzK4azReOilq6n4XIscCMhvgiXkstYMM/dCe6FJw0oed06ck8AtA==", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.21.0", + "lightningcss-darwin-x64": "1.21.0", + "lightningcss-linux-arm-gnueabihf": "1.21.0", + "lightningcss-linux-arm64-gnu": "1.21.0", + "lightningcss-linux-arm64-musl": "1.21.0", + "lightningcss-linux-x64-gnu": "1.21.0", + "lightningcss-linux-x64-musl": "1.21.0", + "lightningcss-win32-x64-msvc": "1.21.0" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.21.0.tgz", + "integrity": "sha512-WcJmVmbNUnCbUqqXV46ZsriFtWJujcPkn+w2cu4R+EgpXuibyTP/gzahmX0gc4RYQxTz2zXIeGx4cF2gr8fLwA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.21.0.tgz", + "integrity": "sha512-xHwMHfcTIHX6fY4YQimI1V/KcbozoNVeKMncZzrp/3NAj0sp3ktxobCj1e0sGqVJMUMaHu/SWvt0mS8jAIhkYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.21.0.tgz", + "integrity": "sha512-rk1cr+C2IA1QHvh0QJAPXsQ2vrwCksms7fgfaw43RIERBWa6EEM5p0/1CWhdZ5zrl9veUdY6NRaNGRJjJL0iLw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.21.0.tgz", + "integrity": "sha512-JkOG8K2Y4m5MeP3DlaHOgGDDtHbhbJcN8JcizFN0snUIIru1qxYNWPhAQsEwysuTRY9aANP0nScZJkALpcYmgA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.21.0.tgz", + "integrity": "sha512-4Zx51DbR41neTFMs28CI9cZpX/mF5Urc6pChTio5nZhrz6FC1pRGiwxNJ+G15a/YPvRmPmvQd3Mz1N4WEgbj2A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.21.0.tgz", + "integrity": "sha512-PN33pPK/O3b4qMfWcJ2eis7NLqEkyW2NEh9X4rWfJrBtOnSbgafuYUuEtO5Ylu+dL3oUKc5usB07FGeil3RzeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.21.0.tgz", + "integrity": "sha512-S51OT7TRfS5x8aN/8frv/JSXCGm+11VuhM4WCiTqDPjhHUDWd8nwiN/7s5juiwrlrpOxb5UKq21EKDrISoGQpw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.21.0.tgz", + "integrity": "sha512-yW6/ZDJAHrSWtRltH1tr2I+2sn374gK2yclc44HMfpxfjIYgXMUkzqstalloMUQpZFR6M0ltXo5/tuLWoBydGQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -22958,10 +23146,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", - "dev": true + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==" }, "node_modules/nopt": { "version": "5.0.0", @@ -29557,10 +29744,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "funding": [ { "type": "opencollective", @@ -29569,6 +29755,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { @@ -29576,7 +29766,7 @@ "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -30601,15 +30791,15 @@ "version": "6.0.0-next.1", "license": "Apache-2.0", "dependencies": { + "browserslist": "^4.21.9", + "lightningcss": "^1.21.0", "prop-types": "^15.8.1" }, "devDependencies": { "@types/node": "^18.15.11", "@types/prop-types": "^15.7.5", + "tsx": "^3.12.6", "typescript": "^5.0.4" - }, - "engines": { - "node": ">=16" } }, "packages/eslint-plugin-circuit-ui": { @@ -30620,7 +30810,7 @@ "@typescript-eslint/utils": "^5.59.6" }, "devDependencies": { - "@sumup/design-tokens": "^5.3.0", + "@sumup/design-tokens": "^6.0.0-next", "@tsconfig/node18": "^2.0.0", "typescript": "^5.0.4" }, @@ -30628,15 +30818,6 @@ "@sumup/design-tokens": ">=5.3.0" } }, - "packages/eslint-plugin-circuit-ui/node_modules/@sumup/design-tokens": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@sumup/design-tokens/-/design-tokens-5.3.0.tgz", - "integrity": "sha512-xwEGGqGahCvtUefVnEEtT/aoeHrxHVgzsp8ehbzcDLbTXy4SuFs5i7e1OhYwj2lr1Vs0qC4fTQtulfXZx8lf/g==", - "dev": true, - "dependencies": { - "prop-types": "^15.8.1" - } - }, "packages/eslint-plugin-circuit-ui/node_modules/@typescript-eslint/scope-manager": { "version": "5.59.7", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz", @@ -30944,7 +31125,7 @@ "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { - "@sumup/design-tokens": "^5.3.0", + "@sumup/design-tokens": "^6.0.0-next", "@tsconfig/node18": "^2.0.0", "jest-preset-stylelint": "^6.1.0", "typescript": "^5.0.4" @@ -30953,15 +31134,6 @@ "@sumup/design-tokens": ">=5.3.0", "stylelint": "^15.0.0" } - }, - "packages/stylelint-plugin-circuit-ui/node_modules/@sumup/design-tokens": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@sumup/design-tokens/-/design-tokens-5.3.0.tgz", - "integrity": "sha512-xwEGGqGahCvtUefVnEEtT/aoeHrxHVgzsp8ehbzcDLbTXy4SuFs5i7e1OhYwj2lr1Vs0qC4fTQtulfXZx8lf/g==", - "dev": true, - "dependencies": { - "prop-types": "^15.8.1" - } } }, "dependencies": { @@ -37904,28 +38076,22 @@ "requires": { "@types/node": "^18.15.11", "@types/prop-types": "^15.7.5", + "browserslist": "^4.21.9", + "lightningcss": "^1.21.0", "prop-types": "^15.8.1", + "tsx": "^3.12.6", "typescript": "^5.0.4" } }, "@sumup/eslint-plugin-circuit-ui": { "version": "file:packages/eslint-plugin-circuit-ui", "requires": { - "@sumup/design-tokens": "^5.3.0", + "@sumup/design-tokens": "^6.0.0-next", "@tsconfig/node18": "^2.0.0", "@typescript-eslint/utils": "^5.59.6", "typescript": "^5.0.4" }, "dependencies": { - "@sumup/design-tokens": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@sumup/design-tokens/-/design-tokens-5.3.0.tgz", - "integrity": "sha512-xwEGGqGahCvtUefVnEEtT/aoeHrxHVgzsp8ehbzcDLbTXy4SuFs5i7e1OhYwj2lr1Vs0qC4fTQtulfXZx8lf/g==", - "dev": true, - "requires": { - "prop-types": "^15.8.1" - } - }, "@typescript-eslint/scope-manager": { "version": "5.59.7", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz", @@ -38251,21 +38417,10 @@ "@sumup/stylelint-plugin-circuit-ui": { "version": "file:packages/stylelint-plugin-circuit-ui", "requires": { - "@sumup/design-tokens": "^5.3.0", + "@sumup/design-tokens": "^6.0.0-next", "@tsconfig/node18": "^2.0.0", "jest-preset-stylelint": "^6.1.0", "typescript": "^5.0.4" - }, - "dependencies": { - "@sumup/design-tokens": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@sumup/design-tokens/-/design-tokens-5.3.0.tgz", - "integrity": "sha512-xwEGGqGahCvtUefVnEEtT/aoeHrxHVgzsp8ehbzcDLbTXy4SuFs5i7e1OhYwj2lr1Vs0qC4fTQtulfXZx8lf/g==", - "dev": true, - "requires": { - "prop-types": "^15.8.1" - } - } } }, "@testing-library/dom": { @@ -40156,15 +40311,14 @@ } }, "browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", - "dev": true, + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "requires": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" } }, "bser": { @@ -40339,10 +40493,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001478", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz", - "integrity": "sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw==", - "dev": true + "version": "1.0.30001503", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001503.tgz", + "integrity": "sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==" }, "ccount": { "version": "2.0.1", @@ -41560,6 +41713,11 @@ "integrity": "sha1-WSSF67v2s7GrK+F1yDk9BMoNV+Y= sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -41845,10 +42003,9 @@ } }, "electron-to-chromium": { - "version": "1.4.365", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.365.tgz", - "integrity": "sha512-FRHZO+1tUNO4TOPXmlxetkoaIY8uwHzd1kKopK/Gx2SKn1L47wJXWD44wxP5CGRyyP98z/c8e1eBzJrgPeiBOg==", - "dev": true + "version": "1.4.433", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.433.tgz", + "integrity": "sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==" }, "elegant-spinner": { "version": "1.0.1", @@ -42125,8 +42282,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha1-2M/ccACWXFoBdLSoLqpcBVJ0LkA= sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha1-2M/ccACWXFoBdLSoLqpcBVJ0LkA= sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-html": { "version": "1.0.3", @@ -46580,6 +46736,70 @@ } } }, + "lightningcss": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.21.0.tgz", + "integrity": "sha512-HDznZexdDMvC98c79vRE+oW5vFncTlLjJopzK4azReOilq6n4XIscCMhvgiXkstYMM/dCe6FJw0oed06ck8AtA==", + "requires": { + "detect-libc": "^1.0.3", + "lightningcss-darwin-arm64": "1.21.0", + "lightningcss-darwin-x64": "1.21.0", + "lightningcss-linux-arm-gnueabihf": "1.21.0", + "lightningcss-linux-arm64-gnu": "1.21.0", + "lightningcss-linux-arm64-musl": "1.21.0", + "lightningcss-linux-x64-gnu": "1.21.0", + "lightningcss-linux-x64-musl": "1.21.0", + "lightningcss-win32-x64-msvc": "1.21.0" + } + }, + "lightningcss-darwin-arm64": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.21.0.tgz", + "integrity": "sha512-WcJmVmbNUnCbUqqXV46ZsriFtWJujcPkn+w2cu4R+EgpXuibyTP/gzahmX0gc4RYQxTz2zXIeGx4cF2gr8fLwA==", + "optional": true + }, + "lightningcss-darwin-x64": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.21.0.tgz", + "integrity": "sha512-xHwMHfcTIHX6fY4YQimI1V/KcbozoNVeKMncZzrp/3NAj0sp3ktxobCj1e0sGqVJMUMaHu/SWvt0mS8jAIhkYw==", + "optional": true + }, + "lightningcss-linux-arm-gnueabihf": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.21.0.tgz", + "integrity": "sha512-rk1cr+C2IA1QHvh0QJAPXsQ2vrwCksms7fgfaw43RIERBWa6EEM5p0/1CWhdZ5zrl9veUdY6NRaNGRJjJL0iLw==", + "optional": true + }, + "lightningcss-linux-arm64-gnu": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.21.0.tgz", + "integrity": "sha512-JkOG8K2Y4m5MeP3DlaHOgGDDtHbhbJcN8JcizFN0snUIIru1qxYNWPhAQsEwysuTRY9aANP0nScZJkALpcYmgA==", + "optional": true + }, + "lightningcss-linux-arm64-musl": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.21.0.tgz", + "integrity": "sha512-4Zx51DbR41neTFMs28CI9cZpX/mF5Urc6pChTio5nZhrz6FC1pRGiwxNJ+G15a/YPvRmPmvQd3Mz1N4WEgbj2A==", + "optional": true + }, + "lightningcss-linux-x64-gnu": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.21.0.tgz", + "integrity": "sha512-PN33pPK/O3b4qMfWcJ2eis7NLqEkyW2NEh9X4rWfJrBtOnSbgafuYUuEtO5Ylu+dL3oUKc5usB07FGeil3RzeA==", + "optional": true + }, + "lightningcss-linux-x64-musl": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.21.0.tgz", + "integrity": "sha512-S51OT7TRfS5x8aN/8frv/JSXCGm+11VuhM4WCiTqDPjhHUDWd8nwiN/7s5juiwrlrpOxb5UKq21EKDrISoGQpw==", + "optional": true + }, + "lightningcss-win32-x64-msvc": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.21.0.tgz", + "integrity": "sha512-yW6/ZDJAHrSWtRltH1tr2I+2sn374gK2yclc44HMfpxfjIYgXMUkzqstalloMUQpZFR6M0ltXo5/tuLWoBydGQ==", + "optional": true + }, "lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -48510,10 +48730,9 @@ "dev": true }, "node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", - "dev": true + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==" }, "nopt": { "version": "5.0.0", @@ -53538,10 +53757,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "requires": { "escalade": "^3.1.1", "picocolors": "^1.0.0" diff --git a/packages/design-tokens/.gitignore b/packages/design-tokens/.gitignore new file mode 100644 index 0000000000..b3a5267117 --- /dev/null +++ b/packages/design-tokens/.gitignore @@ -0,0 +1 @@ +*.css diff --git a/packages/design-tokens/README.md b/packages/design-tokens/README.md index bc9baa6ce8..f55877000c 100644 --- a/packages/design-tokens/README.md +++ b/packages/design-tokens/README.md @@ -2,7 +2,7 @@ # @sumup/design-tokens -Visual primitives such as typography, color, and spacing that are shared across platforms. +Visual primitives such as typography, color, and spacing for [Circuit UI Web](https://github.com/sumup-oss/circuit-ui/tree/main/packages/circuit-ui). [![Stars](https://img.shields.io/github/stars/sumup-oss/circuit-ui?style=social)](https://github.com/sumup-oss/circuit-ui/) [![Version](https://img.shields.io/npm/v/@sumup/design-tokens)](https://www.npmjs.com/package/@sumup/design-tokens) [![Coverage](https://img.shields.io/codecov/c/github/sumup-oss/circuit-ui)](https://codecov.io/gh/sumup-oss/circuit-ui) [![License](https://img.shields.io/badge/license--lightgrey.svg)](https://github.com/sumup-oss/circuit-ui/tree/main/packages/design-tokens/LICENSE) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.1%20adopted-ff69b4.svg)](https://github.com/sumup-oss/circuit-ui/tree/main/CODE_OF_CONDUCT.md) @@ -22,9 +22,30 @@ yarn add @sumup/design-tokens ## Usage -The package currently exports a single `light` theme that is meant to be used with SumUp's React component library, [Circuit UI](https://github.com/sumup-oss/circuit-ui/tree/main/packages/circuit-ui). Pass the theme to Emotion's [`ThemeProvider`](https://emotion.sh/docs/theming): +The [legacy JavaScript theme](#legacy-javascript-theme) for Emotion.js is being replaced by [CSS custom properties](#css-custom-properties) (aka CSS variables) to improve performance and compatibility with other frameworks. We recommend installing the [`@sumup/eslint-plugin-circuit-ui`](https://circuit.sumup.com/?path=/docs/packages-eslint-plugin-circuit-ui--docs) ESLint plugin and turning on the `prefer-custom-properties` and `no-invalid-custom-properties` rules to help with the migration. -```jsx +Refer to the [theme documentation](https://circuit.sumup.com/?path=/docs/features-theme--docs) for a full reference of the available tokens. + +### CSS custom properties + +The `@sumup/design-tokens/light.css` CSS file contains all CSS custom properties that are used by Circuit UI. Import it globally in your application, such as in Next.js' `_app.tsx` file: + +```tsx +// _app.tsx +import '@sumup/design-tokens/light.css'; + +function App({ Component, pageProps }) { + return ; +} +``` + +The application code must be processed by a bundler that can handle CSS files. [Next.js](https://nextjs.org/docs/pages/building-your-application/styling), [Create React App](https://create-react-app.dev/docs/adding-a-stylesheet), [Vite](https://vitejs.dev/guide/features.html#css-modules), [Parcel](https://parceljs.org/languages/css/#css-modules), and others support importing CSS files out of the box. + +### Legacy JavaScript theme + +The `light` theme is meant to be used with Emotion.js' [`ThemeProvider`](https://emotion.sh/docs/theming): + +```tsx import { light } from '@sumup/design-tokens'; import { ThemeProvider } from '@emotion/react'; import styled from '@emotion/styled'; @@ -33,20 +54,22 @@ const Bold = styled.strong` font-weight: ${(p) => p.theme.fontWeight.bold}; `; -const App = () => ( - - This styled component has access to the theme. - -); +function App() { + return ( + + This styled component has access to the theme. + + ); +} ``` The theme is a plain JavaScript object, so you can use it in other ways, too. -### With prop types +#### With prop types The package exports a `themePropType` which can be used to check the `theme` prop: -```js +```tsx import PropTypes from 'prop-types'; import { withTheme } from '@emotion/react'; import { themePropType } from '@sumup/design-tokens'; @@ -63,13 +86,15 @@ ComponentWithInlineStyles.propTypes = { export default function withTheme(ComponentWithInlineStyles); ``` -### With TypeScript +#### With TypeScript -The package exports a `Theme` interface which can be used to type Emotion's `styled` function. Create a custom `styled` instance as described in the [Emotion docs](https://emotion.sh/docs/typescript): +The package exports a `Theme` interface that can be used to augment Emotion.js' types as described in the [Emotion.js docs](https://emotion.sh/docs/typescript#define-a-theme): -```tsx -import styled, { CreateStyled } from '@emotion/styled'; -import { Theme } from '@sumup/design-tokens'; +```ts +import '@emotion/react'; +import { Theme as CircuitUITheme } from '@sumup/design-tokens'; -export default styled as CreateStyled; +declare module '@emotion/react' { + export interface Theme extends CircuitUITheme {} +} ``` diff --git a/packages/design-tokens/index.ts b/packages/design-tokens/index.ts index 2bb451bd25..42e6cefbfb 100644 --- a/packages/design-tokens/index.ts +++ b/packages/design-tokens/index.ts @@ -14,14 +14,21 @@ */ import type { Theme } from './types/index.js'; -import * as lightBase from './themes/light.js'; +import * as legacy from './themes/legacy/light.js'; export { schema } from './themes/schema.js'; export { themePropType } from './utils/theme-prop-type.js'; +/** + * @deprecated + * + * Use the CSS custom properties from `@sumup/design-tokens` instead. + * Use the [`circuit-ui/prefer-custom-properties`](https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui/prefer-custom-properties) + * ESLint rule to automatically migrate your code. + */ // HACK: Copy the theme, otherwise, it gets exported as 'module'. -const light: Theme = { ...lightBase }; +const light: Theme = { ...legacy }; export type { Theme }; export { light }; diff --git a/packages/design-tokens/package.json b/packages/design-tokens/package.json index faed086620..afba18088a 100644 --- a/packages/design-tokens/package.json +++ b/packages/design-tokens/package.json @@ -2,13 +2,13 @@ "name": "@sumup/design-tokens", "version": "6.0.0-next.1", "description": "Visual primitives such as typography, color, and spacing that are shared across platforms.", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": "./dist/index.js", + "main": "dist/cjs/index.js", + "module": "dist/es/index.js", + "types": "dist/es/index.d.ts", "sideEffects": false, "files": [ - "dist" + "dist", + "*.css" ], "repository": { "type": "git", @@ -22,20 +22,23 @@ "homepage": "https://github.com/sumup-oss/circuit-ui/tree/main/packages/design-tokens/README.md", "scripts": { "start": "tsc --watch", - "build": "tsc", + "build": "npm run build:es && npm run build:cjs && npm run build:styles", + "build:es": "tsc", + "build:cjs": "tsc --project tsconfig.cjs.json", + "build:styles": "tsx ./scripts/build.ts", "lint": "foundry run eslint . --ext .js,.jsx,.ts,.tsx", "lint:fix": "npm run lint -- --fix", "test": "vitest" }, "dependencies": { + "browserslist": "^4.21.9", + "lightningcss": "^1.21.0", "prop-types": "^15.8.1" }, "devDependencies": { "@types/node": "^18.15.11", "@types/prop-types": "^15.7.5", + "tsx": "^3.12.6", "typescript": "^5.0.4" - }, - "engines": { - "node": ">=16" } } diff --git a/packages/design-tokens/scripts/build.spec.ts b/packages/design-tokens/scripts/build.spec.ts new file mode 100644 index 0000000000..4863fd41eb --- /dev/null +++ b/packages/design-tokens/scripts/build.spec.ts @@ -0,0 +1,77 @@ +/** + * Copyright 2023, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; + +import type { Token } from '../types/index.js'; + +import { validateTokens, createCSSCustomProperties } from './build.js'; + +describe('build', () => { + describe('validateTokens', () => { + it('should throw an error when required tokens are missing', () => { + const tokens = [ + { + name: '--cui-bg-normal', + description: 'Use as normal background color in any given interface', + value: '#00f2b840', + type: 'color', + }, + ] as Token[]; + + const actual = () => validateTokens(tokens); + + expect(actual).toThrow( + 'The theme is missing the required "--cui-bg-normal-hovered" token.', + ); + }); + + it('should throw an error when a token does not match the expected type', () => { + const tokens = [ + { + name: '--cui-bg-normal', + description: 'Use as normal background color in any given interface', + value: '#00f2b840', + type: 'spacing', + }, + ] as unknown as Token[]; + + const actual = () => validateTokens(tokens); + + expect(actual).toThrow( + 'The "--cui-bg-normal" token does not match the expected type. Expected "color". Received "spacing."', + ); + }); + }); + + describe('createCSSCustomProperties', () => { + const tokens = [ + { + name: '--cui-bg-normal', + description: 'Use as normal background color in any given interface', + value: '#00f2b840', + type: 'color', + }, + ] as Token[]; + + it('should create CSS custom properties from the color tokens', () => { + const actual = createCSSCustomProperties(tokens); + + expect(actual).toMatchInlineSnapshot( + '":root, ::selection, ::backdrop { /* Use as normal background color in any given interface */ --cui-bg-normal: #00f2b840; }"', + ); + }); + }); +}); diff --git a/packages/design-tokens/scripts/build.ts b/packages/design-tokens/scripts/build.ts new file mode 100755 index 0000000000..f60ec53497 --- /dev/null +++ b/packages/design-tokens/scripts/build.ts @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +/** + * Copyright 2023, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +import browserslist from 'browserslist'; +import { transform, browserslistToTargets } from 'lightningcss'; + +import { schema } from '../themes/schema.js'; +import { light } from '../themes/light.js'; + +import type { Token } from '../types/index.js'; + +function main(): void { + const themes = [{ name: 'light', tokens: light }]; + + const targets = browserslistToTargets(browserslist()); + + themes.forEach((theme) => { + validateTokens(theme.tokens); + + const filename = `${theme.name}.css`; + const filepath = path.join(__dirname, '../', filename); + const customProperties = createCSSCustomProperties(theme.tokens); + const { code } = transform({ + filename, + code: Buffer.from(customProperties), + targets, + }); + + fs.writeFileSync(filepath, code, { flag: 'w' }); + }); +} + +/** + * Validates that the theme includes all expected tokens + * and that the token values match the expected type. + */ +export function validateTokens(tokens: Token[]): void { + schema.forEach(({ name, type }) => { + const token = tokens.find((t) => t.name === name); + + if (!token) { + throw new Error(`The theme is missing the required "${name}" token.`); + } + + if (token.type !== type) { + throw new Error( + [ + `The "${name}" token does not match the expected type.`, + `Expected "${type as string}". Received "${token.type as string}."`, + ].join(' '), + ); + } + }); +} + +/** + * Generates CSS custom properties from the tokens + */ +export function createCSSCustomProperties( + tokens: Token[], + selector = ':root, ::selection, ::backdrop', +): string { + const customProperties = tokens + .flatMap((token) => { + const { description, name, value } = token; + const lines: string[] = []; + + if (description) { + lines.push(`/* ${description} */`); + } + + lines.push(`${name}: ${value};`); + + return lines; + }) + .join(' '); + return `${selector} { ${customProperties} }`; +} + +try { + main(); +} catch (error) { + console.error((error as Error).message); + process.exit(1); +} diff --git a/packages/design-tokens/themes/legacy/light.ts b/packages/design-tokens/themes/legacy/light.ts new file mode 100644 index 0000000000..f969d0626d --- /dev/null +++ b/packages/design-tokens/themes/legacy/light.ts @@ -0,0 +1,125 @@ +/** + * Copyright 2020, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Colors } from '../../types/index.js'; + +export const type = 'light'; + +const white = '#FFF'; +const black = '#000'; + +// NOTE: The neutral color names & values don't match up with what's in Figma. +// Fixing this would be a big breaking change, so we're leaving it as is. +const neutrals = { + n100: '#F5F5F5', + n200: '#E6E6E6', + n300: '#CCC', + n500: '#999', + n700: '#666', + n800: '#333', + n900: '#1A1A1A', +}; + +const blues = { + b100: '#F0F6FF', + b200: '#DAEAFF', + b300: '#AFD0FE', + b400: '#7FB5FF', + b500: '#3063E9', + b700: '#234BC3', + b900: '#1A368E', +}; + +const greens = { + g100: '#E4EABB', + g200: '#D4DB8F', + g300: '#BED630', + g500: '#8CC13F', + g700: '#138849', + g900: '#356560', +}; + +const violets = { + v100: '#E9CFF2', + v200: '#D7A9DC', + v300: '#C781C9', + v500: '#CA58FF', + v700: '#8928A2', + v900: '#5F1D6B', +}; + +const oranges = { + o100: '#EFD0BB', + o200: '#F7B97C', + o300: '#ED7000', + o500: '#CE6C0B', + o700: '#8E4503', + o900: '#66391B', +}; + +const yellows = { + y100: '#F2E9C7', + y200: '#EDDD8E', + y300: '#F6CC1B', + y500: '#D8A413', + y700: '#AD7A14', + y900: '#725514', +}; + +const reds = { + r100: '#F4CBCB', + r200: '#EDA2A2', + r300: '#EA7A7A', + r500: '#D23F47', + r700: '#B22426', + r900: '#941618', +}; + +const primary = { + p100: blues.b100, + p200: blues.b200, + p300: blues.b300, + p400: blues.b400, + p500: blues.b500, + p700: blues.b700, + p900: blues.b900, +}; + +const misc = { + shadow: 'rgba(12, 15, 20, 0.07)', + overlay: 'rgba(0, 0, 0, 0.4)', + bodyBg: white, + bodyColor: neutrals.n900, + info: '#3063E9', + confirm: '#018730', + alert: '#DE331D', + notify: '#F5A720', +}; + +export const colors: Colors = { + white, + black, + ...neutrals, + ...blues, + ...greens, + ...yellows, + ...reds, + ...oranges, + ...violets, + ...primary, + ...misc, +}; + +export * from './shared.js'; diff --git a/packages/design-tokens/themes/shared.ts b/packages/design-tokens/themes/legacy/shared.ts similarity index 99% rename from packages/design-tokens/themes/shared.ts rename to packages/design-tokens/themes/legacy/shared.ts index 6efb5c6a69..753be6f8fd 100644 --- a/packages/design-tokens/themes/shared.ts +++ b/packages/design-tokens/themes/legacy/shared.ts @@ -24,7 +24,7 @@ import { Breakpoints, MediaQueries, ZIndex, -} from '../types/index.js'; +} from '../../types/index.js'; export const spacings: Spacings = { bit: '4px', diff --git a/packages/design-tokens/themes/light.ts b/packages/design-tokens/themes/light.ts index 2f209bc4e5..336af4b2be 100644 --- a/packages/design-tokens/themes/light.ts +++ b/packages/design-tokens/themes/light.ts @@ -1,5 +1,5 @@ /** - * Copyright 2020, SumUp Ltd. + * Copyright 2023, SumUp Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -13,113 +13,997 @@ * limitations under the License. */ -import { Colors } from '../types/index.js'; +import type { Token } from '../types/index.js'; -export const type = 'light'; - -const white = '#FFF'; -const black = '#000'; - -// NOTE: The neutral color names & values don't match up with what's in Figma. -// Fixing this would be a big breaking change, so we're leaving it as is. -const neutrals = { - n100: '#F5F5F5', - n200: '#E6E6E6', - n300: '#CCC', - n500: '#999', - n700: '#666', - n800: '#333', - n900: '#1A1A1A', -}; - -const blues = { - b100: '#F0F6FF', - b200: '#DAEAFF', - b300: '#AFD0FE', - b400: '#7FB5FF', - b500: '#3063E9', - b700: '#234BC3', - b900: '#1A368E', -}; - -const greens = { - g100: '#E4EABB', - g200: '#D4DB8F', - g300: '#BED630', - g500: '#8CC13F', - g700: '#138849', - g900: '#356560', -}; - -const violets = { - v100: '#E9CFF2', - v200: '#D7A9DC', - v300: '#C781C9', - v500: '#CA58FF', - v700: '#8928A2', - v900: '#5F1D6B', -}; - -const oranges = { - o100: '#EFD0BB', - o200: '#F7B97C', - o300: '#ED7000', - o500: '#CE6C0B', - o700: '#8E4503', - o900: '#66391B', -}; - -const yellows = { - y100: '#F2E9C7', - y200: '#EDDD8E', - y300: '#F6CC1B', - y500: '#D8A413', - y700: '#AD7A14', - y900: '#725514', -}; - -const reds = { - r100: '#F4CBCB', - r200: '#EDA2A2', - r300: '#EA7A7A', - r500: '#D23F47', - r700: '#B22426', - r900: '#941618', -}; - -const primary = { - p100: blues.b100, - p200: blues.b200, - p300: blues.b300, - p400: blues.b400, - p500: blues.b500, - p700: blues.b700, - p900: blues.b900, -}; - -const misc = { - shadow: 'rgba(12, 15, 20, 0.07)', - overlay: 'rgba(0, 0, 0, 0.4)', - bodyBg: white, - bodyColor: neutrals.n900, - info: '#3063E9', - confirm: '#018730', - alert: '#DE331D', - notify: '#F5A720', -}; - -export const colors: Colors = { - white, - black, - ...neutrals, - ...blues, - ...greens, - ...yellows, - ...reds, - ...oranges, - ...violets, - ...primary, - ...misc, -}; - -export * from './shared.js'; +export const light = [ + /* Neutral backgrounds */ + { + name: '--cui-bg-normal', + value: '#ffffff', + type: 'color', + }, + { + name: '--cui-bg-normal-hovered', + value: '#f5f5f5', + type: 'color', + }, + { + name: '--cui-bg-normal-pressed', + value: '#e6e6e6', + type: 'color', + }, + { + name: '--cui-bg-normal-disabled', + value: 'rgba(255, 255, 255, 0.4)', + type: 'color', + }, + { + name: '--cui-bg-subtle', + value: '#f5f5f5', + type: 'color', + }, + { + name: '--cui-bg-subtle-hovered', + value: '#e6e6e6', + type: 'color', + }, + { + name: '--cui-bg-subtle-pressed', + value: '#cccccc', + type: 'color', + }, + { + name: '--cui-bg-subtle-disabled', + value: 'rgba(245, 245, 245, 0.4)', + type: 'color', + }, + { + name: '--cui-bg-highlight', + value: '#e6e6e6', + type: 'color', + }, + { + name: '--cui-bg-highlight-hovered', + value: '#cccccc', + type: 'color', + }, + { + name: '--cui-bg-highlight-pressed', + value: '#999999', + type: 'color', + }, + { + name: '--cui-bg-highlight-disabled', + value: 'rgba(230, 230, 230, 0.4)', + type: 'color', + }, + { + name: '--cui-bg-strong', + value: '#000000', + type: 'color', + }, + { + name: '--cui-bg-strong-hovered', + value: '#000000', + type: 'color', + }, + { + name: '--cui-bg-strong-pressed', + value: '#000000', + type: 'color', + }, + { + name: '--cui-bg-strong-disabled', + value: 'rgba(0, 0, 0, 0.4)', + type: 'color', + }, + /* Accent backgrounds */ + { + name: '--cui-bg-accent', + value: '#ebf4ff', + type: 'color', + }, + { + name: '--cui-bg-accent-hovered', + value: '#dbe9ff', + type: 'color', + }, + { + name: '--cui-bg-accent-pressed', + value: '#c7dbff', + type: 'color', + }, + { + name: '--cui-bg-accent-disabled', + value: 'rgba(235, 244, 255, 0.4)', + type: 'color', + }, + { + name: '--cui-bg-accent-strong', + value: '#3064e3', + type: 'color', + }, + { + name: '--cui-bg-accent-strong-hovered', + value: '#1c51d3', + type: 'color', + }, + { + name: '--cui-bg-accent-strong-pressed', + value: '#10399e', + type: 'color', + }, + { + name: '--cui-bg-accent-strong-disabled', + value: 'rgba(48, 100, 227, 0.4)', + type: 'color', + }, + /* Success backgrounds */ + { + name: '--cui-bg-success', + value: '#e9fbe9', + type: 'color', + }, + { + name: '--cui-bg-success-hovered', + value: '#d7f8d7', + type: 'color', + }, + { + name: '--cui-bg-success-pressed', + value: '#c1e8c1', + type: 'color', + }, + { + name: '--cui-bg-success-disabled', + value: 'rgba(193, 232, 193, 0.4)', + type: 'color', + }, + { + name: '--cui-bg-success-strong', + value: '#018850', + type: 'color', + }, + { + name: '--cui-bg-success-strong-hovered', + value: '#007a4e', + type: 'color', + }, + { + name: '--cui-bg-success-strong-pressed', + value: '#016c26', + type: 'color', + }, + { + name: '--cui-bg-success-strong-disabled', + value: 'rgba(1, 135, 48, 0.4)', + type: 'color', + }, + /* Warning backgrounds */ + { + name: '--cui-bg-warning', + value: '#fdf4db', + type: 'color', + }, + { + name: '--cui-bg-warning-hovered', + value: '#faeec6', + type: 'color', + }, + { + name: '--cui-bg-warning-pressed', + value: '#f5dea3', + type: 'color', + }, + { + name: '--cui-bg-warning-disabled', + value: 'rgba(245, 222, 163, 0.4)', + type: 'color', + }, + { + name: '--cui-bg-warning-strong', + value: '#e87c00', + type: 'color', + }, + { + name: '--cui-bg-warning-strong-hovered', + value: '#cc6d00', + type: 'color', + }, + { + name: '--cui-bg-warning-strong-pressed', + value: '#b25c00', + type: 'color', + }, + { + name: '--cui-bg-warning-strong-disabled', + value: 'rgba(232, 124, 0, 0.4)', + type: 'color', + }, + /* Danger backgrounds */ + { + name: '--cui-bg-danger', + value: '#fbe9e7', + type: 'color', + }, + { + name: '--cui-bg-danger-hovered', + value: '#fcddd9', + type: 'color', + }, + { + name: '--cui-bg-danger-pressed', + value: '#f7ccc7', + type: 'color', + }, + { + name: '--cui-bg-danger-disabled', + value: 'rgba(247, 204, 199, 0.4)', + type: 'color', + }, + { + name: '--cui-bg-danger-strong', + value: '#de331d', + type: 'color', + }, + { + name: '--cui-bg-danger-strong-hovered', + value: '#bd2c19', + type: 'color', + }, + { + name: '--cui-bg-danger-strong-pressed', + value: '#9e2415', + type: 'color', + }, + { + name: '--cui-bg-danger-strong-disabled', + value: 'rgba(222, 51, 29, 0.4)', + type: 'color', + }, + /* Promo backgrounds */ + { + name: '--cui-bg-promo', + value: '#f5edfe', + type: 'color', + }, + { + name: '--cui-bg-promo-hovered', + value: '#ede0fc', + type: 'color', + }, + { + name: '--cui-bg-promo-pressed', + value: '#e0c9f8', + type: 'color', + }, + { + name: '--cui-bg-promo-disabled', + value: 'rgba(224, 201, 248, 0.4)', + type: 'color', + }, + { + name: '--cui-bg-promo-strong', + value: '#9e33e0', + type: 'color', + }, + { + name: '--cui-bg-promo-strong-hovered', + value: '#8a1ecc', + type: 'color', + }, + { + name: '--cui-bg-promo-strong-pressed', + value: '#7219a9', + type: 'color', + }, + { + name: '--cui-bg-promo-strong-disabled', + value: 'rgba(149, 53, 208, 0.4)', + type: 'color', + }, + /* Neutral foregrounds */ + { + name: '--cui-fg-normal', + value: '#1a1a1a', + type: 'color', + }, + { + name: '--cui-fg-normal-hovered', + value: '#1a1a1a', + type: 'color', + }, + { + name: '--cui-fg-normal-pressed', + value: '#1a1a1a', + type: 'color', + }, + { + name: '--cui-fg-normal-disabled', + value: 'rgba(26, 26, 26, 0.4)', + type: 'color', + }, + { + name: '--cui-fg-subtle', + value: '#666666', + type: 'color', + }, + { + name: '--cui-fg-subtle-hovered', + value: '#333333', + type: 'color', + }, + { + name: '--cui-fg-subtle-pressed', + value: '#1a1a1a', + type: 'color', + }, + { + name: '--cui-fg-subtle-disabled', + value: 'rgba(102, 102, 102, 0.4)', + type: 'color', + }, + { + name: '--cui-fg-placeholder', + value: '#999999', + type: 'color', + }, + { + name: '--cui-fg-placeholder-hovered', + value: '#999999', + type: 'color', + }, + { + name: '--cui-fg-placeholder-pressed', + value: '#999999', + type: 'color', + }, + { + name: '--cui-fg-placeholder-disabled', + value: 'rgba(153, 153, 153, 0.4)', + type: 'color', + }, + { + name: '--cui-fg-on-strong', + value: '#ffffff', + type: 'color', + }, + { + name: '--cui-fg-on-strong-hovered', + value: '#ffffff', + type: 'color', + }, + { + name: '--cui-fg-on-strong-pressed', + value: '#ffffff', + type: 'color', + }, + { + name: '--cui-fg-on-strong-disabled', + value: 'rgba(255, 255, 255, 0.4)', + type: 'color', + }, + /* Accent foregrounds */ + { + name: '--cui-fg-accent', + value: '#3064e3', + type: 'color', + }, + { + name: '--cui-fg-accent-hovered', + value: '#1c51d3', + type: 'color', + }, + { + name: '--cui-fg-accent-pressed', + value: '#10399e', + type: 'color', + }, + { + name: '--cui-fg-accent-disabled', + value: 'rgba(48, 100, 227, 0.4)', + type: 'color', + }, + /* Success foregrounds */ + { + name: '--cui-fg-success', + value: '#018850', + type: 'color', + }, + { + name: '--cui-fg-success-hovered', + value: '#007a4e', + type: 'color', + }, + { + name: '--cui-fg-success-pressed', + value: '#016c26', + type: 'color', + }, + { + name: '--cui-fg-success-disabled', + value: 'rgba(1, 135, 48, 0.4)', + type: 'color', + }, + /* Warning foregrounds */ + { + name: '--cui-fg-warning', + value: '#e27900', + type: 'color', + }, + { + name: '--cui-fg-warning-hovered', + value: '#cc6d00', + type: 'color', + }, + { + name: '--cui-fg-warning-pressed', + value: '#b25c00', + type: 'color', + }, + { + name: '--cui-fg-warning-disabled', + value: 'rgba(232, 124, 0, 0.4)', + type: 'color', + }, + /* Danger foregrounds */ + { + name: '--cui-fg-danger', + value: '#de331d', + type: 'color', + }, + { + name: '--cui-fg-danger-hovered', + value: '#bd2c19', + type: 'color', + }, + { + name: '--cui-fg-danger-pressed', + value: '#9e2415', + type: 'color', + }, + { + name: '--cui-fg-danger-disabled', + value: 'rgba(222, 51, 29, 0.4)', + type: 'color', + }, + /* Promo foregrounds */ + { + name: '--cui-fg-promo', + value: '#9e33e0', + type: 'color', + }, + { + name: '--cui-fg-promo-hovered', + value: '#8a1ecc', + type: 'color', + }, + { + name: '--cui-fg-promo-pressed', + value: '#7219a9', + type: 'color', + }, + { + name: '--cui-fg-promo-disabled', + value: 'rgba(149, 53, 208, 0.4)', + type: 'color', + }, + /* Neutral borders */ + { + name: '--cui-border-normal', + value: '#cccccc', + type: 'color', + }, + { + name: '--cui-border-normal-hovered', + value: '#999999', + type: 'color', + }, + { + name: '--cui-border-normal-pressed', + value: '#666666', + type: 'color', + }, + { + name: '--cui-border-normal-disabled', + value: 'rgba(204, 204, 204, 0.4)', + type: 'color', + }, + { + name: '--cui-border-subtle', + value: '#e6e6e6', + type: 'color', + }, + { + name: '--cui-border-subtle-hovered', + value: '#cccccc', + type: 'color', + }, + { + name: '--cui-border-subtle-pressed', + value: '#999999', + type: 'color', + }, + { + name: '--cui-border-subtle-disabled', + value: 'rgba(230, 230, 230, 0.4)', + type: 'color', + }, + { + name: '--cui-border-divider', + value: '#cccccc', + type: 'color', + }, + { + name: '--cui-border-divider-hovered', + value: '#999999', + type: 'color', + }, + { + name: '--cui-border-divider-pressed', + value: '#666666', + type: 'color', + }, + { + name: '--cui-border-divider-disabled', + value: 'rgba(204, 204, 204, 0.4)', + type: 'color', + }, + { + name: '--cui-border-strong', + value: '#1a1a1a', + type: 'color', + }, + { + name: '--cui-border-strong-hovered', + value: '#000000', + type: 'color', + }, + { + name: '--cui-border-strong-pressed', + value: '#000000', + type: 'color', + }, + { + name: '--cui-border-strong-disabled', + value: 'rgba(0, 0, 0, 0.4)', + type: 'color', + }, + /* Accent borders */ + { + name: '--cui-border-accent', + value: '#3064e3', + type: 'color', + }, + { + name: '--cui-border-accent-hovered', + value: '#1c51d3', + type: 'color', + }, + { + name: '--cui-border-accent-pressed', + value: '#10399e', + type: 'color', + }, + { + name: '--cui-border-accent-disabled', + value: 'rgba(48, 100, 227, 0.4)', + type: 'color', + }, + /* Success borders */ + { + name: '--cui-border-success', + value: '#018850', + type: 'color', + }, + { + name: '--cui-border-success-hovered', + value: '#007a4e', + type: 'color', + }, + { + name: '--cui-border-success-pressed', + value: '#016c26', + type: 'color', + }, + { + name: '--cui-border-success-disabled', + value: 'rgba(1, 135, 48, 0.4)', + type: 'color', + }, + /* Warning borders */ + { + name: '--cui-border-warning', + value: '#e87c00', + type: 'color', + }, + { + name: '--cui-border-warning-hovered', + value: '#cc6d00', + type: 'color', + }, + { + name: '--cui-border-warning-pressed', + value: '#b25c00', + type: 'color', + }, + { + name: '--cui-border-warning-disabled', + value: 'rgba(232, 124, 0, 0.4)', + type: 'color', + }, + /* Danger borders */ + { + name: '--cui-border-danger', + value: '#de331d', + type: 'color', + }, + { + name: '--cui-border-danger-hovered', + value: '#bd2c19', + type: 'color', + }, + { + name: '--cui-border-danger-pressed', + value: '#9e2415', + type: 'color', + }, + { + name: '--cui-border-danger-disabled', + value: 'rgba(222, 51, 29, 0.4)', + type: 'color', + }, + /* Promo borders */ + { + name: '--cui-border-promo', + value: '#9e33e0', + type: 'color', + }, + { + name: '--cui-border-promo-hovered', + value: '#8a1ecc', + type: 'color', + }, + { + name: '--cui-border-promo-pressed', + value: '#7219a9', + type: 'color', + }, + { + name: '--cui-border-promo-disabled', + value: 'rgba(149, 53, 208, 0.4)', + type: 'color', + }, + /* Special colors */ + { + name: '--cui-bg-overlay', + value: 'rgba(0, 0, 0, 0.4)', + type: 'color', + }, + { + name: '--cui-bg-elevated', + value: '#ffffff', + type: 'color', + }, + { + name: '--cui-border-focus', + value: '#ebf4ff', + type: 'color', + }, + /* Border radii */ + { + name: '--cui-border-radius-bit', + value: '4px', + type: 'dimension', + }, + { + name: '--cui-border-radius-byte', + value: '8px', + type: 'dimension', + }, + { + name: '--cui-border-radius-kilo', + value: '12px', + type: 'dimension', + }, + { + name: '--cui-border-radius-mega', + value: '16px', + type: 'dimension', + }, + { + name: '--cui-border-radius-circle', + value: '100%', + type: 'dimension', + }, + { + name: '--cui-border-radius-pill', + value: '999999px', + type: 'dimension', + }, + /* Border widths */ + { + name: '--cui-border-width-kilo', + value: '1px', + type: 'dimension', + }, + { + name: '--cui-border-width-mega', + value: '2px', + type: 'dimension', + }, + /* Font families */ + { + name: '--cui-font-stack-default', + value: + 'aktiv-grotesk, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + type: 'fontFamily', + }, + { + name: '--cui-font-stack-mono', + value: + 'Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace', + type: 'fontFamily', + }, + /* Font weights */ + { + name: '--cui-font-weight-regular', + value: '400', + type: 'fontWeight', + }, + { + name: '--cui-font-weight-bold', + value: '700', + type: 'fontWeight', + }, + /* Icon sizes */ + { + name: '--cui-icon-sizes-kilo', + value: '16px', + type: 'dimension', + }, + { + name: '--cui-icon-sizes-mega', + value: '24px', + type: 'dimension', + }, + { + name: '--cui-icon-sizes-giga', + value: '32px', + type: 'dimension', + }, + { + name: '--cui-icon-sizes-tera', + value: '48px', + type: 'dimension', + }, + /* Spacings */ + { + name: '--cui-spacings-bit', + value: '4px', + type: 'dimension', + }, + { + name: '--cui-spacings-byte', + value: '8px', + type: 'dimension', + }, + { + name: '--cui-spacings-kilo', + value: '12px', + type: 'dimension', + }, + { + name: '--cui-spacings-mega', + value: '16px', + type: 'dimension', + }, + { + name: '--cui-spacings-giga', + value: '24px', + type: 'dimension', + }, + { + name: '--cui-spacings-tera', + value: '32px', + type: 'dimension', + }, + { + name: '--cui-spacings-peta', + value: '40px', + type: 'dimension', + }, + { + name: '--cui-spacings-exa', + value: '48px', + type: 'dimension', + }, + { + name: '--cui-spacings-zetta', + value: '56px', + type: 'dimension', + }, + /* Transitions */ + { + name: '--cui-transitions-default', + value: '120ms ease-in-out', + type: 'duration', + }, + { + name: '--cui-transitions-slow', + value: '300ms ease-in-out', + type: 'duration', + }, + /* Typography */ + { + name: '--cui-typography-headline-one-font-size', + value: '2rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-one-line-height', + value: '2.25rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-two-font-size', + value: '1.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-two-line-height', + value: '1.75rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-three-font-size', + value: '1.25rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-three-line-height', + value: '1.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-four-font-size', + value: '1.125rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-four-line-height', + value: '1.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-title-one-font-size', + value: '7.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-title-one-line-height', + value: '7.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-title-two-font-size', + value: '6rem', + type: 'dimension', + }, + { + name: '--cui-typography-title-two-line-height', + value: '6rem', + type: 'dimension', + }, + { + name: '--cui-typography-title-three-font-size', + value: '4rem', + type: 'dimension', + }, + { + name: '--cui-typography-title-three-line-height', + value: '4rem', + type: 'dimension', + }, + { + name: '--cui-typography-title-four-font-size', + value: '3.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-title-four-line-height', + value: '3.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-sub-headline-font-size', + value: '0.875rem', + type: 'dimension', + }, + { + name: '--cui-typography-sub-headline-line-height', + value: '1.25rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-one-font-size', + value: '1rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-one-line-height', + value: '1.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-two-font-size', + value: '0.875rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-two-line-height', + value: '1.25rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-large-font-size', + value: '1.25rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-large-line-height', + value: '1.75rem', + type: 'dimension', + }, + /* Z-indices */ + { + name: '--cui-z-index-default', + value: 0, + type: 'number', + }, + { + name: '--cui-z-index-absolute', + value: 1, + type: 'number', + }, + { + name: '--cui-z-index-input', + value: 20, + type: 'number', + }, + { + name: '--cui-z-index-popover', + value: 30, + type: 'number', + }, + { + name: '--cui-z-index-tooltip', + value: 40, + type: 'number', + }, + { + name: '--cui-z-index-header', + value: 600, + type: 'number', + }, + { + name: '--cui-z-index-backdrop', + value: 700, + type: 'number', + }, + { + name: '--cui-z-index-navigation', + value: 800, + type: 'number', + }, + { + name: '--cui-z-index-modal', + value: 1000, + type: 'number', + }, + { + name: '--cui-z-index-toast', + value: 1100, + type: 'number', + }, +] satisfies Token[]; diff --git a/packages/design-tokens/themes/schema.ts b/packages/design-tokens/themes/schema.ts index 6762b6e5a9..a6b4f810e7 100644 --- a/packages/design-tokens/themes/schema.ts +++ b/packages/design-tokens/themes/schema.ts @@ -15,7 +15,7 @@ import type { TokenName, TokenType } from '../types/index.js'; -export const schema: { name: TokenName; type: TokenType }[] = [ +export const schema = [ /* Neutral backgrounds */ { name: '--cui-bg-normal', type: 'color' }, { name: '--cui-bg-normal-hovered', type: 'color' }, @@ -166,4 +166,74 @@ export const schema: { name: TokenName; type: TokenType }[] = [ { name: '--cui-bg-overlay', type: 'color' }, { name: '--cui-bg-elevated', type: 'color' }, { name: '--cui-border-focus', type: 'color' }, -]; + /* Border radii */ + { name: '--cui-border-radius-bit', type: 'dimension' }, + { name: '--cui-border-radius-byte', type: 'dimension' }, + { name: '--cui-border-radius-kilo', type: 'dimension' }, + { name: '--cui-border-radius-mega', type: 'dimension' }, + { name: '--cui-border-radius-circle', type: 'dimension' }, + { name: '--cui-border-radius-pill', type: 'dimension' }, + /* Border widths */ + { name: '--cui-border-width-kilo', type: 'dimension' }, + { name: '--cui-border-width-mega', type: 'dimension' }, + /* Font families */ + { name: '--cui-font-stack-default', type: 'fontFamily' }, + { name: '--cui-font-stack-mono', type: 'fontFamily' }, + /* Font weights */ + { name: '--cui-font-weight-regular', type: 'fontWeight' }, + { name: '--cui-font-weight-bold', type: 'fontWeight' }, + /* Icon sizes */ + { name: '--cui-icon-sizes-kilo', type: 'dimension' }, + { name: '--cui-icon-sizes-mega', type: 'dimension' }, + { name: '--cui-icon-sizes-giga', type: 'dimension' }, + { name: '--cui-icon-sizes-tera', type: 'dimension' }, + /* Spacings */ + { name: '--cui-spacings-bit', type: 'dimension' }, + { name: '--cui-spacings-byte', type: 'dimension' }, + { name: '--cui-spacings-kilo', type: 'dimension' }, + { name: '--cui-spacings-mega', type: 'dimension' }, + { name: '--cui-spacings-giga', type: 'dimension' }, + { name: '--cui-spacings-tera', type: 'dimension' }, + { name: '--cui-spacings-peta', type: 'dimension' }, + { name: '--cui-spacings-exa', type: 'dimension' }, + { name: '--cui-spacings-zetta', type: 'dimension' }, + /* Transitions */ + { name: '--cui-transitions-default', type: 'duration' }, + { name: '--cui-transitions-slow', type: 'duration' }, + /* Typography */ + { name: '--cui-typography-headline-one-font-size', type: 'dimension' }, + { name: '--cui-typography-headline-one-line-height', type: 'dimension' }, + { name: '--cui-typography-headline-two-font-size', type: 'dimension' }, + { name: '--cui-typography-headline-two-line-height', type: 'dimension' }, + { name: '--cui-typography-headline-three-font-size', type: 'dimension' }, + { name: '--cui-typography-headline-three-line-height', type: 'dimension' }, + { name: '--cui-typography-headline-four-font-size', type: 'dimension' }, + { name: '--cui-typography-headline-four-line-height', type: 'dimension' }, + { name: '--cui-typography-title-one-font-size', type: 'dimension' }, + { name: '--cui-typography-title-one-line-height', type: 'dimension' }, + { name: '--cui-typography-title-two-font-size', type: 'dimension' }, + { name: '--cui-typography-title-two-line-height', type: 'dimension' }, + { name: '--cui-typography-title-three-font-size', type: 'dimension' }, + { name: '--cui-typography-title-three-line-height', type: 'dimension' }, + { name: '--cui-typography-title-four-font-size', type: 'dimension' }, + { name: '--cui-typography-title-four-line-height', type: 'dimension' }, + { name: '--cui-typography-sub-headline-font-size', type: 'dimension' }, + { name: '--cui-typography-sub-headline-line-height', type: 'dimension' }, + { name: '--cui-typography-body-one-font-size', type: 'dimension' }, + { name: '--cui-typography-body-one-line-height', type: 'dimension' }, + { name: '--cui-typography-body-two-font-size', type: 'dimension' }, + { name: '--cui-typography-body-two-line-height', type: 'dimension' }, + { name: '--cui-typography-body-large-font-size', type: 'dimension' }, + { name: '--cui-typography-body-large-line-height', type: 'dimension' }, + /* Z-indices */ + { name: '--cui-z-index-default', type: 'number' }, + { name: '--cui-z-index-absolute', type: 'number' }, + { name: '--cui-z-index-input', type: 'number' }, + { name: '--cui-z-index-popover', type: 'number' }, + { name: '--cui-z-index-tooltip', type: 'number' }, + { name: '--cui-z-index-header', type: 'number' }, + { name: '--cui-z-index-backdrop', type: 'number' }, + { name: '--cui-z-index-navigation', type: 'number' }, + { name: '--cui-z-index-modal', type: 'number' }, + { name: '--cui-z-index-toast', type: 'number' }, +] satisfies { name: TokenName; type: TokenType }[]; diff --git a/packages/design-tokens/tsconfig.cjs.json b/packages/design-tokens/tsconfig.cjs.json new file mode 100644 index 0000000000..8ab4792db4 --- /dev/null +++ b/packages/design-tokens/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "./dist/cjs" + } +} diff --git a/packages/design-tokens/tsconfig.json b/packages/design-tokens/tsconfig.json index 2dd4346d4e..3585aafd24 100644 --- a/packages/design-tokens/tsconfig.json +++ b/packages/design-tokens/tsconfig.json @@ -4,14 +4,15 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "./dist", + "moduleResolution": "Node", + "outDir": "./dist/es", "resolveJsonModule": true, "strict": true, + "skipLibCheck": true, "target": "es2019" }, "include": [ + "scripts/**/*", "tests/**/*", "themes/**/*", "types/**/*", diff --git a/packages/design-tokens/types/index.ts b/packages/design-tokens/types/index.ts index aaead8c342..b5c7ee4276 100644 --- a/packages/design-tokens/types/index.ts +++ b/packages/design-tokens/types/index.ts @@ -394,6 +394,13 @@ export type ZIndex = { toast: number; }; +/** + * @deprecated + * + * Use the CSS custom properties from `@sumup/design-tokens` instead. + * Use the [`circuit-ui/prefer-custom-properties`](https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui/prefer-custom-properties) + * ESLint rule to automatically migrate your code. + */ export interface Theme { /** * @deprecated @@ -443,11 +450,22 @@ export interface Theme { zIndex: ZIndex; } +/** + * The token definitions below are loosely based on + * https://github.com/design-tokens/community-group + */ + export type TokenName = `--cui-${string}`; export type TokenType = Token['type']; -export type Token = ColorToken; +export type Token = + | ColorToken + | DimensionToken + | DurationToken + | FontFamilyToken + | FontWeightToken + | NumberToken; interface BaseToken { name: TokenName; @@ -460,8 +478,38 @@ interface ColorToken extends BaseToken { type: 'color'; value: Color; } + export type Color = | `#${string}` | `rgb(${number},${number},${number})` | `rgb(${number},${number},${number},${number})` | `rgba(${number},${number},${number},${number})`; + +interface DimensionToken extends BaseToken { + type: 'dimension'; + value: Dimension; +} + +type Dimension = `${number}rem` | `${number}px` | `${number}%`; + +interface DurationToken extends BaseToken { + type: 'duration'; + value: Duration; +} + +type Duration = `${number}ms` | `${number}ms ${string}`; + +interface FontFamilyToken extends BaseToken { + type: 'fontFamily'; + value: string | string[]; +} + +interface FontWeightToken extends BaseToken { + type: 'fontWeight'; + value: string | number; +} + +interface NumberToken extends BaseToken { + type: 'number'; + value: number; +} diff --git a/packages/design-tokens/utils/theme-prop-type.ts b/packages/design-tokens/utils/theme-prop-type.ts index b8cbc9d230..b33f164d8a 100644 --- a/packages/design-tokens/utils/theme-prop-type.ts +++ b/packages/design-tokens/utils/theme-prop-type.ts @@ -52,6 +52,13 @@ const gridPropType = PropTypes.shape({ gutter: PropTypes.string.isRequired, }).isRequired; +/** + * @deprecated + * + * Use the CSS custom properties from `@sumup/design-tokens` instead. + * Use the [`circuit-ui/prefer-custom-properties`](https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui/prefer-custom-properties) + * ESLint rule to automatically migrate your code. + */ export const themePropType = PropTypes.shape({ colors: PropTypes.shape({ white: PropTypes.string.isRequired, diff --git a/packages/eslint-plugin-circuit-ui/README.md b/packages/eslint-plugin-circuit-ui/README.md index d04fd6f066..82ebf6260c 100644 --- a/packages/eslint-plugin-circuit-ui/README.md +++ b/packages/eslint-plugin-circuit-ui/README.md @@ -43,3 +43,4 @@ Rules are configured under the rules section: ## Supported Rules - [`no-invalid-custom-properties`](https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui/no-invalid-custom-properties) +- [`prefer-custom-properties`](https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui/prefer-custom-properties) diff --git a/packages/eslint-plugin-circuit-ui/index.ts b/packages/eslint-plugin-circuit-ui/index.ts index c70bf79961..254a5e16b7 100644 --- a/packages/eslint-plugin-circuit-ui/index.ts +++ b/packages/eslint-plugin-circuit-ui/index.ts @@ -14,7 +14,9 @@ */ import { noInvalidCustomProperties } from './no-invalid-custom-properties'; +import { preferCustomProperties } from './prefer-custom-properties'; export const rules = { 'no-invalid-custom-properties': noInvalidCustomProperties, + 'prefer-custom-properties': preferCustomProperties, }; diff --git a/packages/eslint-plugin-circuit-ui/no-invalid-custom-properties/index.ts b/packages/eslint-plugin-circuit-ui/no-invalid-custom-properties/index.ts index 435b3d3c49..1c56d72fde 100644 --- a/packages/eslint-plugin-circuit-ui/no-invalid-custom-properties/index.ts +++ b/packages/eslint-plugin-circuit-ui/no-invalid-custom-properties/index.ts @@ -40,7 +40,7 @@ export const noInvalidCustomProperties = createRule({ recommended: 'error', }, messages: { - invalid: '"{{name}}" is not a valid Circuit UI color token.', + invalid: '"{{name}}" is not a valid Circuit UI design token.', }, }, defaultOptions: [], diff --git a/packages/eslint-plugin-circuit-ui/package.json b/packages/eslint-plugin-circuit-ui/package.json index ef73b01159..92ac65dd8e 100644 --- a/packages/eslint-plugin-circuit-ui/package.json +++ b/packages/eslint-plugin-circuit-ui/package.json @@ -29,7 +29,7 @@ "@typescript-eslint/utils": "^5.59.6" }, "devDependencies": { - "@sumup/design-tokens": "^5.3.0", + "@sumup/design-tokens": "^6.0.0-next", "@tsconfig/node18": "^2.0.0", "typescript": "^5.0.4" }, diff --git a/packages/eslint-plugin-circuit-ui/prefer-custom-properties/README.md b/packages/eslint-plugin-circuit-ui/prefer-custom-properties/README.md new file mode 100644 index 0000000000..4a9f8fd076 --- /dev/null +++ b/packages/eslint-plugin-circuit-ui/prefer-custom-properties/README.md @@ -0,0 +1,60 @@ +# Do not use invalid Circuit UI custom properties (`prefer-custom-properties`) + +Circuit UI's Emotion.js theme is being replaced with CSS custom properties (a.k.a. CSS variables). This rule flags uses of the Emotion.js theme and can automatically rewrite them to CSS custom properties. + +## Rule Details + +This rule aims to automate the migration to CSS custom properties. CSS custom properties are widely supported and more performant to update than Emotion.js' theme object that is passed to components via React context. Setting the rule's error level to `warn` (or `1`) is recommended. + +Examples of **incorrect** code for this rule: + +```tsx +const styles = (theme) => css` + padding: ${theme.spacings.kilo}; +`; + +const Box = styled.div` + padding: ${(theme) => theme.spacings.kilo}; +`; + +function Component() { + const theme = useTheme(); + return
; +} +``` + +Examples of **correct** code for this rule: + +```tsx +const styles = () => css` + padding: var(--cui-spacings-kilo); +`; + +const Box = styled.div` + padding: var(--cui-spacings-kilo); +`; + +function Component() { + return
; +} + +// CSS custom properties aren't supported inside media queries, +// so the rule doesn't flag or replace `theme.mq.*` tokens. +const mediaQueries = (theme) => css` + ${theme.mq.kilo} { + display: flex; + } +`; +``` + +### Options + +n/a + +## When Not To Use It + +n/a + +## Further Reading + +- [Theme documentation](https://circuit.sumup.com/?path=/docs/features-theme--docs) on the Circuit UI docs diff --git a/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.spec.ts b/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.spec.ts new file mode 100644 index 0000000000..f80e71fd5a --- /dev/null +++ b/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.spec.ts @@ -0,0 +1,172 @@ +/** + * Copyright 2023, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// We disable the rule in this file because we explicitly test invalid cases +/* eslint-disable @sumup/circuit-ui/no-invalid-custom-properties */ + +import { ESLintUtils } from '@typescript-eslint/utils'; + +import { preferCustomProperties } from '.'; + +const ruleTester = new ESLintUtils.RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run('prefer-custom-properties', preferCustomProperties, { + valid: [ + { + name: 'tagged template expression that does not reference the `theme`', + code: ` + const styles = ({ padding }) => css\` + padding: \${padding}; + \`; + `, + }, + { + name: 'tagged template expression that is not passed to `css`', + code: ` + const message = log\`Current theme: \${theme.type}\`; + `, + }, + { + name: 'tagged template expression that uses an unsupported `theme` property', + code: ` + const styles = (theme) => css\` + \${theme.mq.kilo} { + display: flex; + } + \`; + `, + }, + ], + invalid: [ + { + name: 'tagged template expression that references the `theme` once', + code: ` + const styles = (theme) => css\` + padding: \${theme.spacings.kilo}; + \`; + `, + output: ` + const styles = (theme) => css\` + padding: var(--cui-spacings-kilo); + \`; + `, + errors: [{ messageId: 'replace' }], + }, + { + name: 'tagged template expression that references the `theme` on multiple lines', + code: ` + const styles = (theme) => css\` + padding: \${theme.spacings.kilo}; + margin: \${theme.spacings.kilo}; + \`; + `, + // The second occurrence would be auto-fixed on the second pass. + output: ` + const styles = (theme) => css\` + padding: var(--cui-spacings-kilo); + margin: \${theme.spacings.kilo}; + \`; + `, + errors: [{ messageId: 'replace' }, { messageId: 'replace' }], + }, + { + name: 'tagged template expression that references the `theme` multiple times inline', + code: ` + const styles = (theme) => css\` + padding: \${theme.spacings.kilo} \${theme.spacings.kilo}; + \`; + `, + // The second occurrence would be auto-fixed on the second pass. + output: ` + const styles = (theme) => css\` + padding: var(--cui-spacings-kilo) \${theme.spacings.kilo}; + \`; + `, + errors: [{ messageId: 'replace' }, { messageId: 'replace' }], + }, + { + name: 'tagged template expression that computes the `theme` property dynamically', + code: ` + const styles = ({ theme, size }) => css\` + padding: \${theme.spacings[size]}; + \`; + `, + errors: [{ messageId: 'refactor' }], + }, + { + name: 'tagged template expression that references a color token', + code: ` + const styles = (theme) => css\` + color: \${theme.colors.p500}; + \`; + `, + errors: [{ messageId: 'refactor' }], + }, + { + name: 'tagged template expression that does arithmetic with the `theme` property', + code: ` + const styles = (theme) => css\` + z-index: \${theme.zIndex.absolute + 1}; + \`; + `, + errors: [{ messageId: 'refactor' }], + }, + { + name: 'tagged template expression with conditional `theme` properties', + code: ` + const styles = ({ theme, large }) => css\` + padding: \${large ? theme.spacings.exa : theme.spacings.peta}; + \`; + `, + output: ` + const styles = ({ theme, large }) => css\` + padding: \${large ? 'var(--cui-spacings-exa)' : 'var(--cui-spacings-peta)'}; + \`; + `, + errors: [{ messageId: 'replace' }, { messageId: 'replace' }], + }, + { + name: 'tagged template expression with destructured `theme` property', + code: ` + const Box = styled.div\` + padding: \${({ theme }) => theme.spacings.peta}; + \`; + `, + output: ` + const Box = styled.div\` + padding: \${({ theme }) => 'var(--cui-spacings-peta)'}; + \`; + `, + errors: [{ messageId: 'replace' }], + }, + { + name: 'member expression', + code: ` + const borderRadius = theme.borderRadius.kilo; + `, + output: ` + const borderRadius = 'var(--cui-border-radius-kilo)'; + `, + errors: [{ messageId: 'replace' }], + }, + ], +}); diff --git a/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.ts b/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.ts new file mode 100644 index 0000000000..903b7f5e7f --- /dev/null +++ b/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.ts @@ -0,0 +1,186 @@ +/** + * Copyright 2023, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { schema } from '@sumup/design-tokens'; + +const createRule = ESLintUtils.RuleCreator( + (name) => + `https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui/${name}`, +); + +export const preferCustomProperties = createRule({ + name: 'prefer-custom-properties', + meta: { + type: 'suggestion', + schema: [], + fixable: 'code', + docs: { + description: + 'Custom properties prefixed with `--cui-` should be valid Circuit UI design tokens.', + recommended: 'error', + }, + messages: { + replace: + 'Use CSS custom properties instead of the Emotion.js theme. Replace "{{jsToken}}" with "{{cssVariable}}".', + refactor: 'Use CSS custom properties instead of the Emotion.js theme.', + }, + }, + defaultOptions: [], + create(context) { + function transformCamelToKebabCase(string: string) { + return string.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); + } + + function createCSSCustomProperty(segments: string[]): `--cui-${string}` { + const name = segments.map(transformCamelToKebabCase).join('-'); + return `--cui-${name}`; + } + + function isValidCustomProperty(customProperty: string) { + return schema.findIndex((token) => token.name === customProperty) !== -1; + } + + function reportColorToken(node: TSESTree.Node, identifiers: string[]) { + if (identifiers[1] === 'colors' && identifiers.length === 3) { + context.report({ + node, + messageId: 'refactor', + }); + } + } + + function flattenMemberExpression( + expression: TSESTree.Expression, + identifiers: string[] = [], + computed = false, + ): { identifiers: string[]; computed: boolean } { + if (expression.type === 'MemberExpression') { + return flattenMemberExpression( + expression.object, + expression.property.type === 'Identifier' + ? [expression.property.name, ...identifiers] + : identifiers, + computed || expression.computed, + ); + } + + if (expression.type !== 'Identifier') { + return { identifiers, computed: true }; + } + + return { identifiers: [expression.name, ...identifiers], computed }; + } + + function checkTaggedTemplateExpression( + node: TSESTree.TaggedTemplateExpression, + ) { + const { quasi } = node; + + quasi.expressions.forEach((expression) => { + const { identifiers, computed } = flattenMemberExpression(expression); + + const [theme, ...rest] = identifiers; + + // Ignore template literals that don't reference the `theme` + if (theme !== 'theme') { + return; + } + + // Computed expressions cannot be auto-fixed. + if (computed) { + context.report({ + node: expression, + messageId: 'refactor', + }); + return; + } + + const customProperty = createCSSCustomProperty(rest); + + if (!isValidCustomProperty(customProperty)) { + reportColorToken(expression, identifiers); + return; + } + + const jsToken = `\${${identifiers.join('.')}}`; + const cssVariable = `var(${customProperty})`; + + const text = context + .getSourceCode() + .getText(node) + .replace(jsToken, cssVariable); + + context.report({ + node: expression, + messageId: 'replace', + data: { jsToken, cssVariable }, + fix(fixer) { + return fixer.replaceText(node, text); + }, + }); + }); + } + + function checkMemberExpression(node: TSESTree.MemberExpression) { + if (node.parent?.type === 'TemplateLiteral') { + return; + } + + const { identifiers, computed } = flattenMemberExpression(node); + + const [theme, ...rest] = identifiers; + + // Ignore template literals that don't reference the `theme` + if (theme !== 'theme') { + return; + } + + // Computed expressions cannot be auto-fixed. + if (computed || node.parent?.type === 'BinaryExpression') { + context.report({ + node, + messageId: 'refactor', + }); + return; + } + + const customProperty = createCSSCustomProperty(rest); + + if (!isValidCustomProperty(customProperty)) { + reportColorToken(node, identifiers); + return; + } + + const jsToken = `\${${identifiers.join('.')}}`; + const cssVariable = `var(${customProperty})`; + + context.report({ + node, + messageId: 'replace', + data: { jsToken, cssVariable }, + fix(fixer) { + return fixer.replaceText(node, `'${cssVariable}'`); + }, + }); + } + + return { + 'TaggedTemplateExpression:has(Identifier[name="css"])': + checkTaggedTemplateExpression, + 'MemberExpression:has(Identifier[name="theme"])': checkMemberExpression, + }; + }, +}); diff --git a/packages/stylelint-plugin-circuit-ui/package.json b/packages/stylelint-plugin-circuit-ui/package.json index f475d341a8..9e00ff15d3 100644 --- a/packages/stylelint-plugin-circuit-ui/package.json +++ b/packages/stylelint-plugin-circuit-ui/package.json @@ -26,7 +26,7 @@ "build": "tsc" }, "devDependencies": { - "@sumup/design-tokens": "^5.3.0", + "@sumup/design-tokens": "^6.0.0-next", "@tsconfig/node18": "^2.0.0", "jest-preset-stylelint": "^6.1.0", "typescript": "^5.0.4" diff --git a/tsconfig.json b/tsconfig.json index 65cce99339..a7225805e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "noUnusedParameters": true, "outDir": "./dist/es", "resolveJsonModule": true, + "skipLibCheck": true, "strict": true, "target": "es2019" },