diff --git a/.changeset/brown-cobras-invite.md b/.changeset/brown-cobras-invite.md new file mode 100644 index 0000000000..470f8e0731 --- /dev/null +++ b/.changeset/brown-cobras-invite.md @@ -0,0 +1,5 @@ +--- +"@sumup-oss/circuit-ui": minor +--- + +Added a new `weight` prop to the Body component. Choose between the `regular` and `bold` font weights. diff --git a/.changeset/clever-pugs-sing.md b/.changeset/clever-pugs-sing.md new file mode 100644 index 0000000000..361654faa4 --- /dev/null +++ b/.changeset/clever-pugs-sing.md @@ -0,0 +1,5 @@ +--- +'@sumup-oss/circuit-ui': minor +--- + +Added a new Compact component for text in space-constraint contexts. diff --git a/.changeset/dry-cheetahs-tap.md b/.changeset/dry-cheetahs-tap.md new file mode 100644 index 0000000000..97e5310f19 --- /dev/null +++ b/.changeset/dry-cheetahs-tap.md @@ -0,0 +1,32 @@ +--- +'@sumup-oss/design-tokens': minor +--- + +Consolidated and renamed the `typography` tokens: + +| Old | New | +| --------------------------------------- | ----------------------------------- | +| `typography-title-one-font-size` | `typography-display-l-font-size` | +| `typography-title-one-line-height` | `typography-display-l-line-height` | +| `typography-title-two-font-size` | `typography-display-m-font-size` | +| `typography-title-two-line-height` | `typography-display-m-line-height` | +| `typography-title-three-font-size` | `typography-display-m-font-size` | +| `typography-title-three-line-height` | `typography-display-m-line-height` | +| `typography-title-four-font-size` | `typography-display-s-font-size` | +| `typography-title-four-line-height` | `typography-display-s-line-height` | +| `typography-headline-one-font-size` | `typography-headline-l-font-size` | +| `typography-headline-one-line-height` | `typography-headline-l-line-height` | +| `typography-headline-two-font-size` | `typography-headline-m-font-size` | +| `typography-headline-two-line-height` | `typography-headline-m-line-height` | +| `typography-headline-three-font-size` | `typography-headline-m-font-size` | +| `typography-headline-three-line-height` | `typography-headline-m-line-height` | +| `typography-headline-four-font-size` | `typography-headline-s-font-size` | +| `typography-headline-four-line-height` | `typography-headline-s-line-height` | +| `typography-sub-headline-font-size` | `typography-headline-s-font-size` | +| `typography-sub-headline-line-height` | `typography-headline-s-line-height` | +| `typography-body-large-font-size` | `typography-body-l-font-size` | +| `typography-body-large-line-height` | `typography-body-l-line-height` | +| `typography-body-one-font-size` | `typography-body-m-font-size` | +| `typography-body-one-line-height` | `typography-body-m-line-height` | +| `typography-body-two-font-size` | `typography-body-s-font-size` | +| `typography-body-two-line-height` | `typography-body-s-line-height` | diff --git a/.changeset/five-elephants-travel.md b/.changeset/five-elephants-travel.md new file mode 100644 index 0000000000..3173bbdd60 --- /dev/null +++ b/.changeset/five-elephants-travel.md @@ -0,0 +1,5 @@ +--- +"@sumup-oss/circuit-ui": minor +--- + +Added a new Numeral component for numeric content such as currency values. diff --git a/.changeset/fluffy-lobsters-sin.md b/.changeset/fluffy-lobsters-sin.md new file mode 100644 index 0000000000..9ff69f1b09 --- /dev/null +++ b/.changeset/fluffy-lobsters-sin.md @@ -0,0 +1,21 @@ +--- +'@sumup-oss/circuit-ui': minor +--- + +Consolidated and renamed the sizes of the Display (formerly Title), Headline, and Body components: + +**Display & Headline** + +| Old | New | +| ----- | --- | +| one | l | +| two | m | +| three | m | +| four | s | + +**Body** + +| Old | New | +| --- | --- | +| one | m | +| two | s | diff --git a/.changeset/friendly-falcons-turn.md b/.changeset/friendly-falcons-turn.md new file mode 100644 index 0000000000..b7f4989d5f --- /dev/null +++ b/.changeset/friendly-falcons-turn.md @@ -0,0 +1,6 @@ +--- +"@sumup-oss/stylelint-plugin-circuit-ui": minor +"@sumup-oss/eslint-plugin-circuit-ui": minor +--- + +Added `circuit-ui/no-deprecated-custom-properties` rule to flag uses of deprecated custom properties. diff --git a/.changeset/grumpy-coins-sip.md b/.changeset/grumpy-coins-sip.md new file mode 100644 index 0000000000..1b533cb6dc --- /dev/null +++ b/.changeset/grumpy-coins-sip.md @@ -0,0 +1,5 @@ +--- +'@sumup-oss/circuit-ui': minor +--- + +Deprecated the BodyLarge component. Use the Body component in size `l` instead. diff --git a/.changeset/plenty-chicken-deny.md b/.changeset/plenty-chicken-deny.md new file mode 100644 index 0000000000..6d577a56b7 --- /dev/null +++ b/.changeset/plenty-chicken-deny.md @@ -0,0 +1,5 @@ +--- +'@sumup-oss/circuit-ui': minor +--- + +Added an explicit foreground color to the Body component (`fg-normal`) to better support localized dark mode. Previously, the component inherited its color from its parent. diff --git a/.changeset/pretty-tigers-run.md b/.changeset/pretty-tigers-run.md new file mode 100644 index 0000000000..614d636818 --- /dev/null +++ b/.changeset/pretty-tigers-run.md @@ -0,0 +1,5 @@ +--- +"@sumup-oss/eslint-plugin-circuit-ui": major +--- + +Added a migration for the Display (formerly Title), Headline and Body components' `size` prop to the `circuit-ui/no-renamed-props` rule. diff --git a/.changeset/rich-phones-attend.md b/.changeset/rich-phones-attend.md new file mode 100644 index 0000000000..8dffc39e71 --- /dev/null +++ b/.changeset/rich-phones-attend.md @@ -0,0 +1,5 @@ +--- +'@sumup-oss/circuit-ui': minor +--- + +Deprecated the SubHeadline component. Use the Headline component in size `s` instead. diff --git a/.changeset/shiny-dragons-sip.md b/.changeset/shiny-dragons-sip.md new file mode 100644 index 0000000000..1ddfe2a5b8 --- /dev/null +++ b/.changeset/shiny-dragons-sip.md @@ -0,0 +1,5 @@ +--- +"@sumup-oss/circuit-ui": major +--- + +Renamed the Title component to Display for consistency with other platforms. diff --git a/.changeset/soft-drinks-accept.md b/.changeset/soft-drinks-accept.md new file mode 100644 index 0000000000..894a64a7dc --- /dev/null +++ b/.changeset/soft-drinks-accept.md @@ -0,0 +1,5 @@ +--- +'@sumup-oss/circuit-ui': minor +--- + +Deprecated the Body component's `variant` prop. Use the new `color` prop instead of the `alert`, `confirm` and `subtle` variants. Use the new `weight` prop instead of the `highlight` variant. Use custom CSS for the `quote` variant. diff --git a/.changeset/tiny-suits-smile.md b/.changeset/tiny-suits-smile.md new file mode 100644 index 0000000000..68e9856668 --- /dev/null +++ b/.changeset/tiny-suits-smile.md @@ -0,0 +1,5 @@ +--- +'@sumup-oss/circuit-ui': minor +--- + +Added a new `color` prop to the Body component. Choose any foreground color. diff --git a/.eslintrc.js b/.eslintrc.js index 5eda0a38f8..8633d64339 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,6 +17,7 @@ module.exports = require('@sumup-oss/foundry/eslint')({ }, rules: { '@sumup-oss/circuit-ui/no-invalid-custom-properties': 'error', + '@sumup-oss/circuit-ui/no-deprecated-custom-properties': 'error', '@sumup-oss/circuit-ui/prefer-custom-properties': 'warn', 'react/no-unknown-property': ['error', { ignore: ['css'] }], }, diff --git a/.storybook/components/Icons.tsx b/.storybook/components/Icons.tsx index 68d7d9d749..7ccff88ad9 100644 --- a/.storybook/components/Icons.tsx +++ b/.storybook/components/Icons.tsx @@ -191,7 +191,7 @@ export function Icons() { Deprecated diff --git a/.storybook/components/Theme.tsx b/.storybook/components/Theme.tsx index 83ddce233a..d861679f37 100644 --- a/.storybook/components/Theme.tsx +++ b/.storybook/components/Theme.tsx @@ -20,32 +20,33 @@ import { light, schema } from '@sumup-oss/design-tokens'; import { SumUpLogomark } from '@sumup-oss/icons'; import { Anchor, + Badge, Table, ToastProvider, useNotificationToast, type TableHeaderCell, type TableRow, } from '../../packages/circuit-ui/index.js'; +import { Tooltip } from '../../packages/circuit-ui/experimental.js'; type CustomPropertyName = `--cui-${string}`; type CustomPropertyValue = string; -type CustomProperty = [CustomPropertyName, CustomPropertyValue]; +type CustomProperty = { + name: CustomPropertyName; + value: CustomPropertyValue; + deprecation?: { replacement: CustomPropertyName }; +}; 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 filterCustomProperties(namespace: string, type?: string) { + return schema.filter((token) => { + const isNamespace = token.name.startsWith(`--cui-${namespace}`); + const isType = type ? token.type === type : true; + return isNamespace && isType; + }); } function getCustomPropertyValue(name: CustomPropertyName): CustomPropertyValue { @@ -73,13 +74,29 @@ function getRows( customProperties: CustomProperties, Preview?: PreviewComponent, ) { - return customProperties.map(([name, value]) => { + return customProperties.map(({ name, value, deprecation }) => { const row: TableRow = [ { children: (
{name} + {deprecation && ( + ( + + Deprecated + + )} + /> + )}
), }, @@ -111,9 +128,12 @@ export function CustomPropertiesTable({ const [customProperties, setCustomProperties] = useState(); useEffect(() => { - const names = filterCustomProperties(namespace, type); + const tokens = filterCustomProperties(namespace, type); setCustomProperties( - names.map((name) => [name, getCustomPropertyValue(name)]), + tokens.map((token) => ({ + ...token, + value: getCustomPropertyValue(token.name), + })), ); }, [namespace, type]); @@ -194,12 +214,28 @@ export function FontStack({ name }: PreviewProps) { export function FontWeight({ name }: PreviewProps) { return ( + // @ts-expect-error A CSS custom property is a valid font weight

Lorem ipsum

); } +export function Typography({ name }: PreviewProps) { + if (name.includes('font-size')) { + return ( +

Lorem ipsum

+ ); + } + if (name.includes('line-height')) { + return

Lorem ipsum

; + } + if (name.includes('letter-spacing')) { + return

Lorem ipsum

; + } + return null; +} + export function IconSize({ name }: PreviewProps) { return ( [ { children: {`theme.mq.${bp}`} }, - { children: {theme.mq[bp]} }, + { + children: {theme.mq[bp as keyof typeof theme.mq]}, + }, ])} /> diff --git a/.storybook/components/index.ts b/.storybook/components/index.ts index 859a9e10ab..bb2c325cfd 100644 --- a/.storybook/components/index.ts +++ b/.storybook/components/index.ts @@ -40,6 +40,7 @@ export { BorderWidth, FontStack, FontWeight, + Typography, Transition, MediaQueriesTable, } from './Theme.js'; diff --git a/.stylelintrc.js b/.stylelintrc.js index e89ad4c673..fb0e3c24c3 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,6 +1,12 @@ module.exports = require('@sumup-oss/foundry/stylelint')({ extends: ['stylelint-prettier/recommended', 'stylelint-config-css-modules'], + plugins: [ + // TODO: Remove once Foundry has been updated + '@sumup-oss/stylelint-plugin-circuit-ui', + ], rules: { + 'circuit-ui/no-invalid-custom-properties': true, + 'circuit-ui/no-deprecated-custom-properties': true, 'selector-class-pattern': [ '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', { diff --git a/docs/features/1-theme.mdx b/docs/features/1-theme.mdx index bf30171a85..e693dab845 100644 --- a/docs/features/1-theme.mdx +++ b/docs/features/1-theme.mdx @@ -11,9 +11,9 @@ import { BorderWidth, FontStack, FontWeight, + Typography, Transition, } from '../../.storybook/components'; -import { Headline, SubHeadline, Body } from '@sumup-oss/circuit-ui'; @@ -43,7 +43,7 @@ Circuit UI's [ESLint plugin](Packages/eslint-plugin-circuit-ui/Docs) includes r ## Spacings -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. +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 design tokens instead. @@ -61,7 +61,9 @@ Use spacings for gutters, margins, and paddings. Don't use it for border width, ## Typography -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 the `var(--cui-typography-*)` CSS custom properties directly in your styles. Instead, use the typography components [`Display`](Typography/Display/Docs), [`Headline`](Typography/Headline/Docs), and [`Body`](Typography/Body/Docs). + + ## Font stack diff --git a/packages/circuit-ui/components/Badge/Badge.module.css b/packages/circuit-ui/components/Badge/Badge.module.css index de3238cf59..c33329cd2c 100644 --- a/packages/circuit-ui/components/Badge/Badge.module.css +++ b/packages/circuit-ui/components/Badge/Badge.module.css @@ -1,9 +1,9 @@ .base { display: inline-block; padding: 2px var(--cui-spacings-byte); - font-size: var(--cui-typography-body-two-font-size); + font-size: var(--cui-typography-body-s-font-size); font-weight: var(--cui-font-weight-bold); - line-height: var(--cui-typography-body-two-line-height); + line-height: var(--cui-typography-body-s-line-height); text-align: center; letter-spacing: 0.25px; border-radius: var(--cui-border-radius-pill); diff --git a/packages/circuit-ui/components/Body/Body.mdx b/packages/circuit-ui/components/Body/Body.mdx index d6ff462d95..d16219d2aa 100644 --- a/packages/circuit-ui/components/Body/Body.mdx +++ b/packages/circuit-ui/components/Body/Body.mdx @@ -18,12 +18,26 @@ The Body component is used to present content to our users. ### Sizes -The Body component comes in two sizes. Use the default `one` size in most cases. Consider using the [BodyLarge component](Typography/BodyLarge) for large typography in specific cases. +The Body component comes in three sizes. Use the default `m` size in most cases. +### Weights + +The Body component comes in two weights. Use the default `regular` weight in most cases. + + + +### Colors + +The Body component accepts any foreground color. Use the default `normal` color in most cases. + + + ### Variants + + The Body component accepts five different variants—`highlight`, `quote`, `confirm`, `alert` and `subtle`—to tailor it according to the content we are presenting. Different variants will render different HTML elements by default: diff --git a/packages/circuit-ui/components/Body/Body.module.css b/packages/circuit-ui/components/Body/Body.module.css index 262306322e..c6a2f30a30 100644 --- a/packages/circuit-ui/components/Body/Body.module.css +++ b/packages/circuit-ui/components/Body/Body.module.css @@ -1,17 +1,73 @@ -.base { +/* Sizes */ + +.l { + font-size: var(--cui-typography-body-l-font-size); + line-height: var(--cui-typography-body-l-line-height); + letter-spacing: var(--cui-typography-body-l-letter-spacing); +} + +.m { + font-size: var(--cui-typography-body-m-font-size); + line-height: var(--cui-typography-body-m-line-height); + letter-spacing: var(--cui-typography-body-m-letter-spacing); +} + +.s { + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); + letter-spacing: var(--cui-typography-body-s-letter-spacing); +} + +/* Weights */ + +.regular { font-weight: var(--cui-font-weight-regular); } -/* Sizes */ +.bold { + font-weight: var(--cui-font-weight-bold); +} + +/* Colors */ + +.normal { + color: var(--cui-fg-normal); +} + +.subtle { + color: var(--cui-fg-subtle); +} -.one { - font-size: var(--cui-typography-body-one-font-size); - line-height: var(--cui-typography-body-one-line-height); +.placeholder { + color: var(--cui-fg-placeholder); } -.two { - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); +.on-strong { + color: var(--cui-fg-on-strong); +} + +.on-strong-subtle { + color: var(--cui-fg-on-strong-subtle); +} + +.accent { + color: var(--cui-fg-accent); +} + +.success { + color: var(--cui-fg-success); +} + +.warning { + color: var(--cui-fg-warning); +} + +.danger { + color: var(--cui-fg-danger); +} + +.promo { + color: var(--cui-fg-promo); } /* Variants */ @@ -35,7 +91,3 @@ blockquote { .alert { color: var(--cui-fg-danger); } - -.subtle { - color: var(--cui-fg-subtle); -} diff --git a/packages/circuit-ui/components/Body/Body.stories.tsx b/packages/circuit-ui/components/Body/Body.stories.tsx index eda2d599e2..f090c924b9 100644 --- a/packages/circuit-ui/components/Body/Body.stories.tsx +++ b/packages/circuit-ui/components/Body/Body.stories.tsx @@ -13,6 +13,8 @@ * limitations under the License. */ +import { BodyLarge } from '../BodyLarge/BodyLarge.js'; + import type { BodyProps } from './Body.js'; import { Body } from './index.js'; @@ -23,6 +25,7 @@ const content = export default { title: 'Typography/Body', component: Body, + subcomponents: { BodyLarge }, argTypes: { as: { control: 'text' }, }, @@ -30,12 +33,50 @@ export default { export const Base = (args: BodyProps) => {content}; -const sizes = ['one', 'two'] as const; +const sizes = ['l', 'm', 's'] as const; export const Sizes = (args: BodyProps) => - sizes.map((s) => ( - - This is a body {s}. {content} + sizes.map((size) => ( + + This is size {size}. {content} + + )); + +const weights = ['regular', 'bold'] as const; + +export const Weights = (args: BodyProps) => + weights.map((weight) => ( + + This is the {weight} weight. {content} + + )); + +const colors = [ + 'normal', + 'subtle', + 'placeholder', + 'on-strong', + 'on-strong-subtle', + 'accent', + 'success', + 'warning', + 'danger', + 'promo', +] as const; + +export const Colors = (args: BodyProps) => + colors.map((color) => ( + + This is the {color} color. {content} )); diff --git a/packages/circuit-ui/components/Body/Body.tsx b/packages/circuit-ui/components/Body/Body.tsx index af4669e4e9..02f9af185f 100644 --- a/packages/circuit-ui/components/Body/Body.tsx +++ b/packages/circuit-ui/components/Body/Body.tsx @@ -17,6 +17,7 @@ import { forwardRef, type HTMLAttributes } from 'react'; import type { AsPropType } from '../../types/prop-types.js'; import { clsx } from '../../styles/clsx.js'; +import { deprecate } from '../../util/logger.js'; import classes from './Body.module.css'; @@ -24,11 +25,42 @@ type Variant = 'highlight' | 'quote' | 'confirm' | 'alert' | 'subtle'; export interface BodyProps extends HTMLAttributes { /** - * Choose from 2 font sizes. Default `one`. + * Choose from 3 font sizes. Default `m`. */ - size?: 'one' | 'two'; + size?: + | 's' + | 'm' + | 'l' + /** + * @deprecated + */ + | 'one' + /** + * @deprecated + */ + | 'two'; /** - * Choose from style variants. + * Choose from two font weights. Default: `regular`. + */ + weight?: 'regular' | 'bold'; + /** + * Choose a foreground color. Default: `normal`. + */ + color?: + | 'normal' + | 'subtle' + | 'placeholder' + | 'on-strong' + | 'on-strong-subtle' + | 'accent' + | 'success' + | 'warning' + | 'danger' + | 'promo'; + /** + * @deprecated Use the new `color` prop instead of the `alert`, `confirm` and + * `subtle` variants. Use the new `weight` prop instead of the `highlight` + * variant. Use custom CSS for the `quote` variant. */ variant?: Variant; /** @@ -47,20 +79,71 @@ function getHTMLElement(variant?: Variant): AsPropType { return 'p'; } +const deprecatedSizeMap: Record = { + 'one': 'm', + 'two': 's', +}; + /** * The Body component is used to present the core textual content * to our users. */ export const Body = forwardRef( - ({ className, as, size = 'one', variant, ...props }, ref) => { + ( + { + className, + as, + size: legacySize = 'm', + weight = 'regular', + color = 'normal', + variant, + ...props + }, + ref, + ) => { const Element = as || getHTMLElement(variant); + + if (process.env.NODE_ENV !== 'production') { + if (variant) { + if (variant === 'highlight') { + deprecate( + 'Body', + 'The "highlight" variant has been deprecated. Use the new `weight` prop instead.', + ); + } else if (variant === 'quote') { + deprecate( + 'Body', + 'The "quote" variant has been deprecated. Use custom CSS instead.', + ); + } else { + deprecate( + 'Body', + `The "${variant}" variant has been deprecated. Use the new \`color\` prop instead.`, + ); + } + } + + if (legacySize in deprecatedSizeMap) { + deprecate( + 'Body', + `The "${legacySize}" size has been deprecated. Use the "${deprecatedSizeMap[legacySize]}" size instead.`, + ); + } + } + + const size = (deprecatedSizeMap[legacySize] || legacySize) as + | 'l' + | 'm' + | 's'; + return ( - -# BodyLarge - - - -The BodyLarge component is used to render content with large typography. Typically, large typography is intended for landing pages. In most cases, the [Body](Typography/Body) should be used instead. - - - - -## Component variations - -### Variants - -The BodyLarge accepts five different variants—`highlight`, `quote`, `confirm`, `alert` and `subtle`—to tailor it according to the content we are presenting. - -The `highlight` variant will render a `` element by default, while the `quote` variant will render a `blockquote`. - - - ---- - -## Accessibility - -All accessibility guidelines for the [Body component](Typography/Body) also apply to the BodyLarge component. diff --git a/packages/circuit-ui/components/BodyLarge/BodyLarge.module.css b/packages/circuit-ui/components/BodyLarge/BodyLarge.module.css deleted file mode 100644 index aabbf9e841..0000000000 --- a/packages/circuit-ui/components/BodyLarge/BodyLarge.module.css +++ /dev/null @@ -1,31 +0,0 @@ -.base { - font-size: var(--cui-typography-body-large-font-size); - font-weight: var(--cui-font-weight-regular); - line-height: var(--cui-typography-body-large-line-height); -} - -/* Variants */ - -.highlight, -strong { - font-weight: var(--cui-font-weight-bold); -} - -.quote, -blockquote { - padding-left: var(--cui-spacings-kilo); - font-style: italic; - border-left: var(--cui-border-width-mega) solid var(--cui-border-accent); -} - -.confirm { - color: var(--cui-fg-success); -} - -.alert { - color: var(--cui-fg-danger); -} - -.subtle { - color: var(--cui-fg-subtle); -} diff --git a/packages/circuit-ui/components/BodyLarge/BodyLarge.stories.tsx b/packages/circuit-ui/components/BodyLarge/BodyLarge.stories.tsx deleted file mode 100644 index 6106a374b5..0000000000 --- a/packages/circuit-ui/components/BodyLarge/BodyLarge.stories.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright 2021, 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 type { BodyLargeProps } from './BodyLarge.js'; - -import { BodyLarge } from './index.js'; - -const content = - 'An electronic circuit is composed of individual electronic components, such as resistors, transistors, capacitors, inductors and diodes, connected by conductive wires or traces through which electric current can flow.'; - -export default { - title: 'Typography/BodyLarge', - component: BodyLarge, - argTypes: { - as: { control: 'text' }, - }, -}; - -export const Base = (args: BodyLargeProps) => ( - {content} -); - -const variants = ['highlight', 'quote', 'confirm', 'alert', 'subtle'] as const; - -export const Variants = (args: BodyLargeProps) => - variants.map((variant) => ( - - This is a {variant} BodyLarge - - )); diff --git a/packages/circuit-ui/components/BodyLarge/BodyLarge.tsx b/packages/circuit-ui/components/BodyLarge/BodyLarge.tsx index 65516f2978..ec6dc6fb0a 100644 --- a/packages/circuit-ui/components/BodyLarge/BodyLarge.tsx +++ b/packages/circuit-ui/components/BodyLarge/BodyLarge.tsx @@ -13,54 +13,25 @@ * limitations under the License. */ -import { forwardRef, type HTMLAttributes, type Ref } from 'react'; +import { forwardRef } from 'react'; -import type { AsPropType } from '../../types/prop-types.js'; -import { clsx } from '../../styles/clsx.js'; - -import classes from './BodyLarge.module.css'; - -type Variant = 'highlight' | 'quote' | 'confirm' | 'alert' | 'subtle'; - -export interface BodyLargeProps extends HTMLAttributes { - /** - * Choose from style variants. - */ - variant?: Variant; - /** - * Render the text using any HTML element. - */ - as?: AsPropType; - /** - * The ref to the HTML DOM element. - */ - ref?: Ref; -} - -function getHTMLElement(variant?: Variant): AsPropType { - if (variant === 'highlight') { - return 'strong'; - } - if (variant === 'quote') { - return 'blockquote'; - } - return 'p'; -} +import { deprecate } from '../../util/logger.js'; +import { Body, type BodyProps } from '../Body/Body.js'; +export type BodyLargeProps = Omit; /** - * The BodyLarge component is used to present the core textual content - * to our users. + * @deprecated Use the Body component in size `l` instead. */ export const BodyLarge = forwardRef( - ({ className, as, variant, ...props }, ref) => { - const Element = as || getHTMLElement(variant); - return ( - - ); + (props, ref) => { + if (process.env.NODE_ENV !== 'production') { + deprecate( + 'BodyLarge', + 'The BodyLarge component has been deprecated. Use the Body component in size `l` instead.', + ); + } + + return ; }, ); diff --git a/packages/circuit-ui/components/Button/base.module.css b/packages/circuit-ui/components/Button/base.module.css index 86d967eb78..4320581beb 100644 --- a/packages/circuit-ui/components/Button/base.module.css +++ b/packages/circuit-ui/components/Button/base.module.css @@ -6,7 +6,7 @@ width: auto; height: auto; margin: 0; - font-size: var(--cui-typography-body-one-font-size); + font-size: var(--cui-typography-body-m-font-size); font-weight: var(--cui-font-weight-bold); text-align: center; text-decoration: none; @@ -165,8 +165,8 @@ --loader-gap: 3px; --loader-transform: scale(150%); - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); border-radius: var(--cui-border-radius-byte); } @@ -177,8 +177,8 @@ --loader-gap: 5px; --loader-transform: scale(133%); - font-size: var(--cui-typography-body-one-font-size); - line-height: var(--cui-typography-body-one-line-height); + font-size: var(--cui-typography-body-m-font-size); + line-height: var(--cui-typography-body-m-line-height); border-radius: var(--cui-border-radius-byte); } diff --git a/packages/circuit-ui/components/Calendar/Calendar.module.css b/packages/circuit-ui/components/Calendar/Calendar.module.css index 866cdcf99d..c0ab955545 100644 --- a/packages/circuit-ui/components/Calendar/Calendar.module.css +++ b/packages/circuit-ui/components/Calendar/Calendar.module.css @@ -80,9 +80,9 @@ align-items: center; justify-content: center; aspect-ratio: 1 / 1; - font-size: var(--cui-typography-body-one-font-size); + font-size: var(--cui-typography-body-m-font-size); font-weight: var(--cui-font-weight-bold); - line-height: var(--cui-typography-body-one-line-height); + line-height: var(--cui-typography-body-m-line-height); } .day { @@ -90,9 +90,9 @@ height: 100%; aspect-ratio: 1 / 1; padding: 0; - font-size: var(--cui-typography-body-one-font-size); + font-size: var(--cui-typography-body-m-font-size); font-variant-numeric: tabular-nums; - line-height: var(--cui-typography-body-one-line-height); + line-height: var(--cui-typography-body-m-line-height); color: var(--cui-fg-normal); touch-action: manipulation; cursor: pointer; diff --git a/packages/circuit-ui/components/Carousel/components/Status/Status.module.css b/packages/circuit-ui/components/Carousel/components/Status/Status.module.css index fe357465c3..d0e1b5726c 100644 --- a/packages/circuit-ui/components/Carousel/components/Status/Status.module.css +++ b/packages/circuit-ui/components/Carousel/components/Status/Status.module.css @@ -1,6 +1,6 @@ @media (max-width: 479px) { .base { - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); } } diff --git a/packages/circuit-ui/components/Checkbox/Checkbox.module.css b/packages/circuit-ui/components/Checkbox/Checkbox.module.css index 2d7231510b..1d489d1e1b 100644 --- a/packages/circuit-ui/components/Checkbox/Checkbox.module.css +++ b/packages/circuit-ui/components/Checkbox/Checkbox.module.css @@ -9,7 +9,7 @@ .base + .label::before { position: absolute; - top: calc(var(--cui-typography-body-one-line-height) / 2); + top: calc(var(--cui-typography-body-m-line-height) / 2); left: 0; box-sizing: border-box; display: block; diff --git a/packages/circuit-ui/components/Checkbox/Checkbox.stories.tsx b/packages/circuit-ui/components/Checkbox/Checkbox.stories.tsx index 7d40dc598d..c4229427ef 100644 --- a/packages/circuit-ui/components/Checkbox/Checkbox.stories.tsx +++ b/packages/circuit-ui/components/Checkbox/Checkbox.stories.tsx @@ -107,8 +107,8 @@ export const Indeterminate = (args: { style={{ display: 'block', marginBottom: 'var(--cui-spacings-bit)', - fontSize: 'var(--cui-typography-body-two-font-size)', - lineHeight: 'var(--cui-typography-body-two-line-height)', + fontSize: 'var(--cui-typography-body-s-font-size)', + lineHeight: 'var(--cui-typography-body-s-line-height)', }} > {label} diff --git a/packages/circuit-ui/components/Compact/Compact.mdx b/packages/circuit-ui/components/Compact/Compact.mdx new file mode 100644 index 0000000000..062bb652a2 --- /dev/null +++ b/packages/circuit-ui/components/Compact/Compact.mdx @@ -0,0 +1,123 @@ +import { Meta, Status, Props, Story } from '../../../../.storybook/components'; +import * as Stories from './Compact.stories'; + + + +# Compact + + + +The Compact component is used to present content to our users. + + + + +## Usage guidelines + +## Component variations + +### Sizes + +The Compact component comes in three sizes. Use the default `m` size in most cases. + + + +### Weights + +The Compact component comes in two weights. Use the default `regular` weight in most cases. + + + +### Colors + +The Compact component accepts any foreground color. Use the default `normal` color in most cases. + + + + +--- + +## Accessibility + +### Best practices + +#### Break text up into sections + +In order to make content easier to digest (especially on content-heavy pages such as articles or landing pages), break text up into sections. + +- Use semantic HTML elements and headings (use the [Headline](Typography/Headline) component) to build sections into your markup +- Add spacing between paragraphs and sections for sighted users (use the [`spacing()` style mixin](Features/Style-Mixins/Spacing)) + +This is beneficial to everyone, but critical for users with cognitive, language or learning disabilities (such as dyslexia or ADHD). + +#### Write simple copy + +Similarly to how breaking text up into sections helps users _parse_ content, writing concise and simple copy helps users _understand_ content. + +- Avoid the figurative use of words, or specialized words ([3.1.3: Unusual Words](https://www.w3.org/WAI/WCAG21/Understanding/unusual-words)) +- Allow users to access the expanded form of abbreviations ([3.1.4: Abbreviations](https://www.w3.org/WAI/WCAG21/Understanding/abbreviations)) +- Write content as clearly and simply as possible ([3.1.5: Reading Level](https://www.w3.org/WAI/WCAG21/Understanding/reading-level)) + +_Note: the success criteria referenced above are intended for AAA conformance. Though they are not requirements, they are a best practice and following them will improve the accessibility of our content._ + +#### Complement text with images + +Images, graphs, or other illustrations can help users contextualize and/or understand a piece of content. + +Visuals should usually complement text—and not replace it—unless an excellent description is provided as alternative text. + +#### Translate content + +Translate content intended for a linguistically diverse audience, and make it easy for users to change the page's language. + +Bear in mind that machine translation is often inaccurate: prefer professional translation in order to give the best possible experience to users from diverse linguistic backgrounds. + +#### Do not rely on color alone + +Color alone is not sufficient to differentiate text from the surrounding content ([1.4.1 Use of Color](https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html)). + +This is especially relevant when using the `confirm` and `alert` variants. + +For example, imagine a list of good and bad accessibility practices. It is not enough to use the `confirm` (green) variant for good practices and the `alert` (red) variant for bad practices. Instead, use additional visual cues such as icons (with an accessible label), or break up the list into two separate ones with explicit labeling. + +#### Make status messages accessible to screen readers using live regions + +When a change in content is not given focus (typically a message being added to the DOM to reflect a status update), the change needs to be announced to screen readers using a [live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) ([4.1.3 Status Messages](https://www.w3.org/WAI/WCAG21/Understanding/status-messages.html), [3.3.1 Error Identification](https://www.w3.org/WAI/WCAG21/Understanding/error-identification)). + +For example, a form submission fails and a message saying "This username is already taken." using `` appears. Without a live region, screen reader users would have no way of knowing that something happened and that they need to make changes to the form before submitting again. They will either have to step through the DOM to try and find the relevant error message, or they will give up and close the page. + +Live regions can be tricky to work with, therefore we recommend using existing components such as the [NotificationInline](Notifications/NotificationInline) or the [NotificationToast](Notifications/NotificationToast) instead of implementing them from scratch. + +### Resources + +#### Visualizing document structure + +Use a tool like [Wave](https://wave.webaim.org/) to extract structure from a page (in Wave, you'll find the page structure under the _Structure_ tab). Verify that your copy is accurately grouped and labeled by headings. + +#### Verify your UI without color + +Simulate vision deficiencies or desaturate your page, and verify that visual cues beyond color are helping users understand your copy. + +- [Simulate vision deficiencies using Firefox](https://developer.mozilla.org/en-US/docs/Tools/Accessibility_inspector/Simulation) +- [Simulate vision deficiencies using Chrome](https://developer.chrome.com/blog/new-in-devtools-83/#vision-deficiencies) +- Desaturate a page using [Wave](https://wave.webaim.org/) (under Contrast, "Desaturate page") + +#### Test your page using a screen reader + +[Test your page using a screen reader](https://webaim.org/articles/screenreader_testing/) like JAWS (Windows), NVDA (Windows, Linux) or VoiceOver (macOS). + +This is particularly valuable for highly dynamic content, for example using live regions. + +#### Further reading + +- [Writing for Web Accessibility](https://www.w3.org/WAI/tips/writing/) (w3.org) +- [Cognitive Disabilities](https://webaim.org/articles/cognitive/) (WebAIM) + +#### Related WCAG success criteria + +- 1.4.1: [Use of Color](https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html) +- 3.1.3: [Unusual Words](https://www.w3.org/WAI/WCAG21/Understanding/unusual-words) (AAA) +- 3.1.4: [Abbreviations](https://www.w3.org/WAI/WCAG21/Understanding/abbreviations) (AAA) +- 3.1.5: [Reading Level](https://www.w3.org/WAI/WCAG21/Understanding/reading-level) (AAA) +- 3.3.1: [Error Identification](https://www.w3.org/WAI/WCAG21/Understanding/error-identification) +- 4.1.3: [Status Messages](https://www.w3.org/WAI/WCAG21/Understanding/status-messages.html) diff --git a/packages/circuit-ui/components/Compact/Compact.module.css b/packages/circuit-ui/components/Compact/Compact.module.css new file mode 100644 index 0000000000..02ccf82f40 --- /dev/null +++ b/packages/circuit-ui/components/Compact/Compact.module.css @@ -0,0 +1,71 @@ +/* Sizes */ + +.l { + font-size: var(--cui-typography-compact-l-font-size); + line-height: var(--cui-typography-compact-l-line-height); + letter-spacing: var(--cui-typography-compact-l-letter-spacing); +} + +.m { + font-size: var(--cui-typography-compact-m-font-size); + line-height: var(--cui-typography-compact-m-line-height); + letter-spacing: var(--cui-typography-compact-m-letter-spacing); +} + +.s { + font-size: var(--cui-typography-compact-s-font-size); + line-height: var(--cui-typography-compact-s-line-height); + letter-spacing: var(--cui-typography-compact-s-letter-spacing); +} + +/* Weights */ + +.regular { + font-weight: var(--cui-font-weight-regular); +} + +.bold { + font-weight: var(--cui-font-weight-bold); +} + +/* Colors */ + +.normal { + color: var(--cui-fg-normal); +} + +.subtle { + color: var(--cui-fg-subtle); +} + +.placeholder { + color: var(--cui-fg-placeholder); +} + +.on-strong { + color: var(--cui-fg-on-strong); +} + +.on-strong-subtle { + color: var(--cui-fg-on-strong-subtle); +} + +.accent { + color: var(--cui-fg-accent); +} + +.success { + color: var(--cui-fg-success); +} + +.warning { + color: var(--cui-fg-warning); +} + +.danger { + color: var(--cui-fg-danger); +} + +.promo { + color: var(--cui-fg-promo); +} diff --git a/packages/circuit-ui/components/Compact/Compact.spec.tsx b/packages/circuit-ui/components/Compact/Compact.spec.tsx new file mode 100644 index 0000000000..e0c42e8aaf --- /dev/null +++ b/packages/circuit-ui/components/Compact/Compact.spec.tsx @@ -0,0 +1,52 @@ +/** + * Copyright 2024, 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, expect, it } from 'vitest'; +import { createRef } from 'react'; + +import { axe, render } from '../../util/test-utils.js'; + +import { Compact } from './Compact.js'; + +describe('Compact', () => { + it('should merge a custom class name with the default ones', () => { + const className = 'foo'; + const { container } = render( + Compact, + ); + const paragraph = container.querySelector('p'); + expect(paragraph?.className).toContain(className); + }); + + it('should forward a ref', () => { + const ref = createRef(); + const { container } = render(Compact); + const paragraph = container.querySelector('p'); + expect(ref.current).toBe(paragraph); + }); + + const elements = ['p', 'article', 'div'] as const; + it.each(elements)('should render as a "%s" element', (as) => { + const { container } = render({as} Compact); + const actual = container.querySelector(as); + expect(actual).toBeVisible(); + }); + + it('should meet accessibility guidelines', async () => { + const { container } = render(Compact); + const actual = await axe(container); + expect(actual).toHaveNoViolations(); + }); +}); diff --git a/packages/circuit-ui/components/Compact/Compact.stories.tsx b/packages/circuit-ui/components/Compact/Compact.stories.tsx new file mode 100644 index 0000000000..10b58260d0 --- /dev/null +++ b/packages/circuit-ui/components/Compact/Compact.stories.tsx @@ -0,0 +1,80 @@ +/** + * Copyright 2024, 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 type { CompactProps } from './Compact.js'; + +import { Compact } from './index.js'; + +const content = + 'An electronic circuit is composed of individual electronic components, such as resistors, transistors, capacitors, inductors and diodes, connected by conductive wires or traces through which electric current can flow.'; + +export default { + title: 'Typography/Compact', + component: Compact, + argTypes: { + as: { control: 'text' }, + }, +}; + +export const Base = (args: CompactProps) => ( + {content} +); + +const sizes = ['l', 'm', 's'] as const; + +export const Sizes = (args: CompactProps) => + sizes.map((size) => ( + + This is size {size}. {content} + + )); + +const weights = ['regular', 'bold'] as const; + +export const Weights = (args: CompactProps) => + weights.map((weight) => ( + + This is the {weight} weight. {content} + + )); + +const colors = [ + 'normal', + 'subtle', + 'placeholder', + 'on-strong', + 'on-strong-subtle', + 'accent', + 'success', + 'warning', + 'danger', + 'promo', +] as const; + +export const Colors = (args: CompactProps) => + colors.map((color) => ( + + This is the {color} color. {content} + + )); diff --git a/packages/circuit-ui/components/Compact/Compact.tsx b/packages/circuit-ui/components/Compact/Compact.tsx new file mode 100644 index 0000000000..c78a997a5e --- /dev/null +++ b/packages/circuit-ui/components/Compact/Compact.tsx @@ -0,0 +1,81 @@ +/** + * Copyright 2024, 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 { forwardRef, type HTMLAttributes } from 'react'; + +import type { AsPropType } from '../../types/prop-types.js'; +import { clsx } from '../../styles/clsx.js'; + +import classes from './Compact.module.css'; + +export interface CompactProps extends HTMLAttributes { + /** + * Choose from 3 font sizes. Default `m`. + */ + size?: 's' | 'm' | 'l'; + /** + * Choose from two font weights. Default: `regular`. + */ + weight?: 'regular' | 'bold'; + /** + * Choose a foreground color. Default: `normal`. + */ + color?: + | 'normal' + | 'subtle' + | 'placeholder' + | 'on-strong' + | 'on-strong-subtle' + | 'accent' + | 'success' + | 'warning' + | 'danger' + | 'promo'; + /** + * Render the text using any HTML element. + */ + as?: AsPropType; +} + +/** + * The Compact component is used to present the core textual content + * to our users. + */ +export const Compact = forwardRef( + ( + { + className, + as: Element = 'p', + size = 'm', + weight = 'regular', + color = 'normal', + ...props + }, + ref, + ) => ( + + ), +); + +Compact.displayName = 'Compact'; diff --git a/packages/circuit-ui/components/Compact/index.ts b/packages/circuit-ui/components/Compact/index.ts new file mode 100644 index 0000000000..53a03cc617 --- /dev/null +++ b/packages/circuit-ui/components/Compact/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2024, 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. + */ + +export { Compact } from './Compact.js'; + +export type { CompactProps } from './Compact.js'; diff --git a/packages/circuit-ui/components/Display/Display.mdx b/packages/circuit-ui/components/Display/Display.mdx new file mode 100644 index 0000000000..80723e3bfc --- /dev/null +++ b/packages/circuit-ui/components/Display/Display.mdx @@ -0,0 +1,27 @@ +import { Meta, Status, Props, Story } from '../../../../.storybook/components'; +import * as Stories from './Display.stories'; + + + +# Display + + + +The Display component is used to render headings with large typography. Typically, large typography is intended for landing pages. In most cases, the [Headline](Typography/Headline) should be used instead. + + + + +## Component variations + +### Sizes + +The Display component comes in three sizes. In most cases, use the [Headline component](Typography/Headline) component instead to render headings. + + + +--- + +## Accessibility + +All accessibility guidelines for the [Headline component](Typography/Headline) also apply to the Display component. diff --git a/packages/circuit-ui/components/Display/Display.module.css b/packages/circuit-ui/components/Display/Display.module.css new file mode 100644 index 0000000000..2e589a0762 --- /dev/null +++ b/packages/circuit-ui/components/Display/Display.module.css @@ -0,0 +1,25 @@ +.base { + font-weight: var(--cui-font-weight-bold); + color: var(--cui-fg-normal); + letter-spacing: -0.03em; +} + +/* Sizes */ + +.l { + font-size: var(--cui-typography-display-l-font-size); + line-height: var(--cui-typography-display-l-line-height); + letter-spacing: var(--cui-typography-display-l-letter-spacing); +} + +.m { + font-size: var(--cui-typography-display-m-font-size); + line-height: var(--cui-typography-display-m-line-height); + letter-spacing: var(--cui-typography-display-m-letter-spacing); +} + +.s { + font-size: var(--cui-typography-display-s-font-size); + line-height: var(--cui-typography-display-s-line-height); + letter-spacing: var(--cui-typography-display-s-letter-spacing); +} diff --git a/packages/circuit-ui/components/Title/Title.spec.tsx b/packages/circuit-ui/components/Display/Display.spec.tsx similarity index 82% rename from packages/circuit-ui/components/Title/Title.spec.tsx rename to packages/circuit-ui/components/Display/Display.spec.tsx index 984fbfd8d4..ecb2ccfbbb 100644 --- a/packages/circuit-ui/components/Title/Title.spec.tsx +++ b/packages/circuit-ui/components/Display/Display.spec.tsx @@ -18,15 +18,15 @@ import { createRef } from 'react'; import { render, axe } from '../../util/test-utils.js'; -import { Title } from './Title.js'; +import { Display } from './Display.jsx'; -describe('Title', () => { +describe('Display', () => { it('should merge a custom class name with the default ones', () => { const className = 'foo'; const { container } = render( - - Title - , + + Display + , ); const headline = container.querySelector('h2'); expect(headline?.className).toContain(className); @@ -35,16 +35,16 @@ describe('Title', () => { it('should forward a ref', () => { const ref = createRef(); const { container } = render( - - Title - , + + Display + , ); const headline = container.querySelector('h2'); expect(ref.current).toBe(headline); }); it('should meet accessibility guidelines', async () => { - const { container } = render(Title); + const { container } = render(Display); const actual = await axe(container); expect(actual).toHaveNoViolations(); }); diff --git a/packages/circuit-ui/components/Title/Title.stories.tsx b/packages/circuit-ui/components/Display/Display.stories.tsx similarity index 62% rename from packages/circuit-ui/components/Title/Title.stories.tsx rename to packages/circuit-ui/components/Display/Display.stories.tsx index b2f433928e..67fb43eb5f 100644 --- a/packages/circuit-ui/components/Title/Title.stories.tsx +++ b/packages/circuit-ui/components/Display/Display.stories.tsx @@ -13,28 +13,28 @@ * limitations under the License. */ -import { Title, type TitleProps } from './Title.js'; +import { Display, type DisplayProps } from './Display.jsx'; export default { - title: 'Typography/Title', - component: Title, + title: 'Typography/Display', + component: Display, }; -export const Base = (args: TitleProps) => ( - This is a Title +export const Base = (args: DisplayProps) => ( + This is a Display ); Base.args = { as: 'h1', }; -const sizes = ['one', 'two', 'three', 'four'] as const; +const sizes = ['l', 'm', 's'] as const; -export const Sizes = (args: TitleProps) => - sizes.map((s) => ( - - This is a Title {s} - +export const Sizes = (args: DisplayProps) => + sizes.map((size) => ( + + This is size {size} + )); Sizes.args = { diff --git a/packages/circuit-ui/components/Title/Title.tsx b/packages/circuit-ui/components/Display/Display.tsx similarity index 55% rename from packages/circuit-ui/components/Title/Title.tsx rename to packages/circuit-ui/components/Display/Display.tsx index 6bb00d088d..7b31f8527f 100644 --- a/packages/circuit-ui/components/Title/Title.tsx +++ b/packages/circuit-ui/components/Display/Display.tsx @@ -17,14 +17,34 @@ import { forwardRef, type HTMLAttributes } from 'react'; import { clsx } from '../../styles/clsx.js'; import { CircuitError } from '../../util/errors.js'; +import { deprecate } from '../../util/logger.js'; -import classes from './Title.module.css'; +import classes from './Display.module.css'; -export interface TitleProps extends HTMLAttributes { +export interface DisplayProps extends HTMLAttributes { /** - * A Circuit UI title size. Defaults to `one`. + * Choose from 3 font sizes. Defaults to `m`. */ - size?: 'one' | 'two' | 'three' | 'four'; + size?: + | 's' + | 'm' + | 'l' + /** + * @deprecated + */ + | 'one' + /** + * @deprecated + */ + | 'two' + /** + * @deprecated + */ + | 'three' + /** + * @deprecated + */ + | 'four'; /** * The HTML heading element to render. * Headings should be nested sequentially without skipping any levels. @@ -33,22 +53,43 @@ export interface TitleProps extends HTMLAttributes { as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; } +const deprecatedSizeMap: Record = { + 'one': 'l', + 'two': 'm', + 'three': 'm', + 'four': 's', +}; + /** * A flexible title component capable of rendering any HTML heading element. */ -export const Title = forwardRef( - ({ className, as, size = 'one', ...props }, ref) => { +export const Display = forwardRef( + ({ className, as, size: legacySize = 'm', ...props }, ref) => { if ( process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' && !process?.env?.UNSAFE_DISABLE_ELEMENT_ERRORS && !as ) { - throw new CircuitError('Title', 'The `as` prop is required.'); + throw new CircuitError('Display', 'The `as` prop is required.'); + } + + if (process.env.NODE_ENV !== 'production') { + if (legacySize in deprecatedSizeMap) { + deprecate( + 'Display', + `The "${legacySize}" size has been deprecated. Use the "${deprecatedSizeMap[legacySize]}" size instead.`, + ); + } } const Element = as || 'h1'; + const size = (deprecatedSizeMap[legacySize] || legacySize) as + | 'l' + | 'm' + | 's'; + return ( ( }, ); -Title.displayName = 'Title'; +Display.displayName = 'Display'; diff --git a/packages/circuit-ui/components/Title/index.ts b/packages/circuit-ui/components/Display/index.ts similarity index 86% rename from packages/circuit-ui/components/Title/index.ts rename to packages/circuit-ui/components/Display/index.ts index 347923192c..b85c320261 100644 --- a/packages/circuit-ui/components/Title/index.ts +++ b/packages/circuit-ui/components/Display/index.ts @@ -13,6 +13,6 @@ * limitations under the License. */ -export { Title } from './Title.js'; +export { Display } from './Display.jsx'; -export type { TitleProps } from './Title.js'; +export type { DisplayProps } from './Display.jsx'; diff --git a/packages/circuit-ui/components/Field/Field.module.css b/packages/circuit-ui/components/Field/Field.module.css index f74aeb7e08..058cf3f34b 100644 --- a/packages/circuit-ui/components/Field/Field.module.css +++ b/packages/circuit-ui/components/Field/Field.module.css @@ -8,8 +8,8 @@ .label, .legend { display: block; - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); } .label-text { @@ -33,8 +33,8 @@ .description { display: block; - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); color: var(--cui-fg-subtle); } @@ -46,8 +46,8 @@ .validation-hint { display: flex; margin-top: var(--cui-spacings-bit); - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); color: var(--cui-fg-subtle); transition: color var(--cui-transitions-default); } @@ -91,8 +91,7 @@ width: var(--cui-icon-sizes-kilo); height: var(--cui-icon-sizes-kilo); margin-top: calc( - (var(--cui-typography-body-two-line-height) - var(--cui-icon-sizes-kilo)) / - 2 + (var(--cui-typography-body-s-line-height) - var(--cui-icon-sizes-kilo)) / 2 ); margin-right: var(--cui-spacings-bit); } diff --git a/packages/circuit-ui/components/Headline/Headline.mdx b/packages/circuit-ui/components/Headline/Headline.mdx index 5a4679db2a..4de1756f33 100644 --- a/packages/circuit-ui/components/Headline/Headline.mdx +++ b/packages/circuit-ui/components/Headline/Headline.mdx @@ -16,9 +16,9 @@ The Headline component is used for describing the contents of a page or page sec ### Sizes -The Headline component comes in four sizes. +The Headline component comes in three sizes. -Use the `four` size for card headers and `three` for page titles in web applications. For specific use cases such as landing pages, consider using the [Title](Typography/Title) component. +Use the `m` size for card headers and `l` for page titles in web applications. Consider using the [Display](Typography/Display) component for specific use cases such as landing pages. diff --git a/packages/circuit-ui/components/Headline/Headline.module.css b/packages/circuit-ui/components/Headline/Headline.module.css index 16070244a3..a890f773bc 100644 --- a/packages/circuit-ui/components/Headline/Headline.module.css +++ b/packages/circuit-ui/components/Headline/Headline.module.css @@ -6,22 +6,20 @@ /* Sizes */ -.one { - font-size: var(--cui-typography-headline-one-font-size); - line-height: var(--cui-typography-headline-one-line-height); +.l { + font-size: var(--cui-typography-headline-l-font-size); + line-height: var(--cui-typography-headline-l-line-height); + letter-spacing: var(--cui-typography-headline-l-letter-spacing); } -.two { - font-size: var(--cui-typography-headline-two-font-size); - line-height: var(--cui-typography-headline-two-line-height); +.m { + font-size: var(--cui-typography-headline-m-font-size); + line-height: var(--cui-typography-headline-m-line-height); + letter-spacing: var(--cui-typography-headline-m-letter-spacing); } -.three { - font-size: var(--cui-typography-headline-three-font-size); - line-height: var(--cui-typography-headline-three-line-height); -} - -.four { - font-size: var(--cui-typography-headline-four-font-size); - line-height: var(--cui-typography-headline-four-line-height); +.s { + font-size: var(--cui-typography-headline-s-font-size); + line-height: var(--cui-typography-headline-s-line-height); + letter-spacing: var(--cui-typography-headline-s-letter-spacing); } diff --git a/packages/circuit-ui/components/Headline/Headline.stories.tsx b/packages/circuit-ui/components/Headline/Headline.stories.tsx index 644c1b241d..840ecf083a 100644 --- a/packages/circuit-ui/components/Headline/Headline.stories.tsx +++ b/packages/circuit-ui/components/Headline/Headline.stories.tsx @@ -28,12 +28,12 @@ Base.args = { as: 'h2', }; -const sizes = ['one', 'two', 'three', 'four'] as const; +const sizes = ['l', 'm', 's'] as const; export const Sizes = (args: HeadlineProps) => - sizes.map((s) => ( - - This is a headline {s} + sizes.map((size) => ( + + This is size {size} )); diff --git a/packages/circuit-ui/components/Headline/Headline.tsx b/packages/circuit-ui/components/Headline/Headline.tsx index 204c47ed83..a4b0e04064 100644 --- a/packages/circuit-ui/components/Headline/Headline.tsx +++ b/packages/circuit-ui/components/Headline/Headline.tsx @@ -17,14 +17,34 @@ import { forwardRef, type HTMLAttributes } from 'react'; import { clsx } from '../../styles/clsx.js'; import { CircuitError } from '../../util/errors.js'; +import { deprecate } from '../../util/logger.js'; import classes from './Headline.module.css'; export interface HeadlineProps extends HTMLAttributes { /** - * A Circuit UI headline size. Defaults to `one`. + * Choose from 3 font sizes. Defaults to `m`. */ - size?: 'one' | 'two' | 'three' | 'four'; + size?: + | 's' + | 'm' + | 'l' + /** + * @deprecated + */ + | 'one' + /** + * @deprecated + */ + | 'two' + /** + * @deprecated + */ + | 'three' + /** + * @deprecated + */ + | 'four'; /** * The HTML heading element to render. * Headings should be nested sequentially without skipping any levels. @@ -33,11 +53,18 @@ export interface HeadlineProps extends HTMLAttributes { as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; } +const deprecatedSizeMap: Record = { + 'one': 'l', + 'two': 'm', + 'three': 'm', + 'four': 's', +}; + /** * A flexible headline component capable of rendering any HTML heading element. */ export const Headline = forwardRef( - ({ className, as, size = 'one', ...props }, ref) => { + ({ className, as, size: legacySize = 'm', ...props }, ref) => { if ( process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' && @@ -47,8 +74,22 @@ export const Headline = forwardRef( throw new CircuitError('Headline', 'The `as` prop is required.'); } + if (process.env.NODE_ENV !== 'production') { + if (legacySize in deprecatedSizeMap) { + deprecate( + 'Headline', + `The "${legacySize}" size has been deprecated. Use the "${deprecatedSizeMap[legacySize]}" size instead.`, + ); + } + } + const Element = as || 'h2'; + const size = (deprecatedSizeMap[legacySize] || legacySize) as + | 'l' + | 'm' + | 's'; + return ( + +# Numeral + + + +The Numeral component is used to present numeric content such as currency values. + + + + +## Usage guidelines + +## Component variations + +### Sizes + +The Numeral component comes in three sizes. Use the default `m` size in most cases. + + + +### Weights + +The Numeral component comes in two weights. Use the default `regular` weight in most cases. + + + +### Colors + +The Numeral component accepts any foreground color. Use the default `normal` color in most cases. + + diff --git a/packages/circuit-ui/components/Numeral/Numeral.module.css b/packages/circuit-ui/components/Numeral/Numeral.module.css new file mode 100644 index 0000000000..7f4b54a4f0 --- /dev/null +++ b/packages/circuit-ui/components/Numeral/Numeral.module.css @@ -0,0 +1,75 @@ +.base { + font-variant-numeric: tabular-nums; +} + +/* Sizes */ + +.l { + font-size: var(--cui-typography-numeral-l-font-size); + line-height: var(--cui-typography-numeral-l-line-height); + letter-spacing: var(--cui-typography-numeral-l-letter-spacing); +} + +.m { + font-size: var(--cui-typography-numeral-m-font-size); + line-height: var(--cui-typography-numeral-m-line-height); + letter-spacing: var(--cui-typography-numeral-m-letter-spacing); +} + +.s { + font-size: var(--cui-typography-numeral-s-font-size); + line-height: var(--cui-typography-numeral-s-line-height); + letter-spacing: var(--cui-typography-numeral-s-letter-spacing); +} + +/* Weights */ + +.regular { + font-weight: var(--cui-font-weight-regular); +} + +.bold { + font-weight: var(--cui-font-weight-bold); +} + +/* Colors */ + +.normal { + color: var(--cui-fg-normal); +} + +.subtle { + color: var(--cui-fg-subtle); +} + +.placeholder { + color: var(--cui-fg-placeholder); +} + +.on-strong { + color: var(--cui-fg-on-strong); +} + +.on-strong-subtle { + color: var(--cui-fg-on-strong-subtle); +} + +.accent { + color: var(--cui-fg-accent); +} + +.success { + color: var(--cui-fg-success); +} + +.warning { + color: var(--cui-fg-warning); +} + +.danger { + color: var(--cui-fg-danger); +} + +.promo { + color: var(--cui-fg-promo); +} diff --git a/packages/circuit-ui/components/Numeral/Numeral.spec.tsx b/packages/circuit-ui/components/Numeral/Numeral.spec.tsx new file mode 100644 index 0000000000..4398eedfb7 --- /dev/null +++ b/packages/circuit-ui/components/Numeral/Numeral.spec.tsx @@ -0,0 +1,52 @@ +/** + * Copyright 2024, 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, expect, it } from 'vitest'; +import { createRef } from 'react'; + +import { axe, render } from '../../util/test-utils.js'; + +import { Numeral } from './Numeral.js'; + +describe('Numeral', () => { + it('should merge a custom class name with the default ones', () => { + const className = 'foo'; + const { container } = render( + Numeral, + ); + const paragraph = container.querySelector('p'); + expect(paragraph?.className).toContain(className); + }); + + it('should forward a ref', () => { + const ref = createRef(); + const { container } = render(Numeral); + const paragraph = container.querySelector('p'); + expect(ref.current).toBe(paragraph); + }); + + const elements = ['p', 'article', 'div'] as const; + it.each(elements)('should render as a "%s" element', (as) => { + const { container } = render({as} Numeral); + const actual = container.querySelector(as); + expect(actual).toBeVisible(); + }); + + it('should meet accessibility guidelines', async () => { + const { container } = render(Numeral); + const actual = await axe(container); + expect(actual).toHaveNoViolations(); + }); +}); diff --git a/packages/circuit-ui/components/Numeral/Numeral.stories.tsx b/packages/circuit-ui/components/Numeral/Numeral.stories.tsx new file mode 100644 index 0000000000..7bb74c488f --- /dev/null +++ b/packages/circuit-ui/components/Numeral/Numeral.stories.tsx @@ -0,0 +1,79 @@ +/** + * Copyright 2024, 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 type { NumeralProps } from './Numeral.js'; + +import { Numeral } from './index.js'; + +const content = '$ 1,009.95'; + +export default { + title: 'Typography/Numeral', + component: Numeral, + argTypes: { + as: { control: 'text' }, + }, +}; + +export const Base = (args: NumeralProps) => ( + {content} +); + +const sizes = ['l', 'm', 's'] as const; + +export const Sizes = (args: NumeralProps) => + sizes.map((size) => ( + + {content} in size {size} + + )); + +const weights = ['regular', 'bold'] as const; + +export const Weights = (args: NumeralProps) => + weights.map((weight) => ( + + {content} in {weight} weight + + )); + +const colors = [ + 'normal', + 'subtle', + 'placeholder', + 'on-strong', + 'on-strong-subtle', + 'accent', + 'success', + 'warning', + 'danger', + 'promo', +] as const; + +export const Colors = (args: NumeralProps) => + colors.map((color) => ( + + {content} in the {color} color. + + )); diff --git a/packages/circuit-ui/components/Numeral/Numeral.tsx b/packages/circuit-ui/components/Numeral/Numeral.tsx new file mode 100644 index 0000000000..e986c5b626 --- /dev/null +++ b/packages/circuit-ui/components/Numeral/Numeral.tsx @@ -0,0 +1,82 @@ +/** + * Copyright 2024, 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 { forwardRef, type HTMLAttributes } from 'react'; + +import type { AsPropType } from '../../types/prop-types.js'; +import { clsx } from '../../styles/clsx.js'; + +import classes from './Numeral.module.css'; + +export interface NumeralProps extends HTMLAttributes { + /** + * Choose from 3 font sizes. Default `m`. + */ + size?: 's' | 'm' | 'l'; + /** + * Choose from two font weights. Default: `regular`. + */ + weight?: 'regular' | 'bold'; + /** + * Choose a foreground color. Default: `normal`. + */ + color?: + | 'normal' + | 'subtle' + | 'placeholder' + | 'on-strong' + | 'on-strong-subtle' + | 'accent' + | 'success' + | 'warning' + | 'danger' + | 'promo'; + /** + * Render the text using any HTML element. + */ + as?: AsPropType; +} + +/** + * The Numeral component is used to present the core textual content + * to our users. + */ +export const Numeral = forwardRef( + ( + { + className, + as: Element = 'p', + size = 'm', + weight = 'regular', + color = 'normal', + ...props + }, + ref, + ) => ( + + ), +); + +Numeral.displayName = 'Numeral'; diff --git a/packages/circuit-ui/components/Numeral/index.ts b/packages/circuit-ui/components/Numeral/index.ts new file mode 100644 index 0000000000..6dd12b8c67 --- /dev/null +++ b/packages/circuit-ui/components/Numeral/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2024, 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. + */ + +export { Numeral } from './Numeral.js'; + +export type { NumeralProps } from './Numeral.js'; diff --git a/packages/circuit-ui/components/Popover/Popover.module.css b/packages/circuit-ui/components/Popover/Popover.module.css index aa078ee289..72b7ba7ccc 100644 --- a/packages/circuit-ui/components/Popover/Popover.module.css +++ b/packages/circuit-ui/components/Popover/Popover.module.css @@ -3,8 +3,8 @@ align-items: center; justify-content: flex-start; width: 100%; - font-size: var(--cui-typography-body-one-font-size); - line-height: var(--cui-typography-body-one-line-height); + font-size: var(--cui-typography-body-m-font-size); + line-height: var(--cui-typography-body-m-line-height); text-align: left; background: var(--cui-bg-elevated); } diff --git a/packages/circuit-ui/components/ProgressBar/ProgressBar.module.css b/packages/circuit-ui/components/ProgressBar/ProgressBar.module.css index 12fcdb17d5..5a81904c90 100644 --- a/packages/circuit-ui/components/ProgressBar/ProgressBar.module.css +++ b/packages/circuit-ui/components/ProgressBar/ProgressBar.module.css @@ -90,6 +90,6 @@ .label { flex-shrink: 0; margin-left: var(--cui-spacings-byte); - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); } diff --git a/packages/circuit-ui/components/RadioButton/RadioButton.module.css b/packages/circuit-ui/components/RadioButton/RadioButton.module.css index 7f73be6501..60fd3c5ef5 100644 --- a/packages/circuit-ui/components/RadioButton/RadioButton.module.css +++ b/packages/circuit-ui/components/RadioButton/RadioButton.module.css @@ -8,7 +8,7 @@ .label::before { position: absolute; - top: calc(var(--cui-typography-body-one-line-height) / 2); + top: calc(var(--cui-typography-body-m-line-height) / 2); left: 0; box-sizing: border-box; display: block; @@ -27,7 +27,7 @@ .label::after { position: absolute; - top: calc(var(--cui-typography-body-one-line-height) / 2); + top: calc(var(--cui-typography-body-m-line-height) / 2); left: var(--cui-spacings-bit); box-sizing: border-box; display: block; diff --git a/packages/circuit-ui/components/Select/Select.module.css b/packages/circuit-ui/components/Select/Select.module.css index 03df237805..59c84b6edf 100644 --- a/packages/circuit-ui/components/Select/Select.module.css +++ b/packages/circuit-ui/components/Select/Select.module.css @@ -14,8 +14,8 @@ padding-left: var(--cui-spacings-mega); margin: 0; overflow-x: hidden; - font-size: var(--cui-typography-body-one-font-size); - line-height: var(--cui-typography-body-one-line-height); + font-size: var(--cui-typography-body-m-font-size); + line-height: var(--cui-typography-body-m-line-height); color: var(--cui-fg-normal); text-overflow: ellipsis; white-space: nowrap; diff --git a/packages/circuit-ui/components/SideNavigation/components/DesktopNavigation/DesktopNavigation.tsx b/packages/circuit-ui/components/SideNavigation/components/DesktopNavigation/DesktopNavigation.tsx index a8ad6d7fda..9efa23c2ee 100644 --- a/packages/circuit-ui/components/SideNavigation/components/DesktopNavigation/DesktopNavigation.tsx +++ b/packages/circuit-ui/components/SideNavigation/components/DesktopNavigation/DesktopNavigation.tsx @@ -83,7 +83,7 @@ export function DesktopNavigation({ aria-label={secondaryNavigationLabel} > - + {activePrimaryLink?.label} diff --git a/packages/circuit-ui/components/SideNavigation/components/PrimaryLink/PrimaryLink.module.css b/packages/circuit-ui/components/SideNavigation/components/PrimaryLink/PrimaryLink.module.css index 7bc44e48c7..998f552b96 100644 --- a/packages/circuit-ui/components/SideNavigation/components/PrimaryLink/PrimaryLink.module.css +++ b/packages/circuit-ui/components/SideNavigation/components/PrimaryLink/PrimaryLink.module.css @@ -84,8 +84,8 @@ @media (max-width: 1279px) { .label { - font-size: var(--cui-typography-headline-two-font-size); - line-height: var(--cui-typography-headline-two-line-height); + font-size: var(--cui-typography-headline-m-font-size); + line-height: var(--cui-typography-headline-m-line-height); } } diff --git a/packages/circuit-ui/components/SideNavigation/components/SecondaryLinks/SecondaryLinks.tsx b/packages/circuit-ui/components/SideNavigation/components/SecondaryLinks/SecondaryLinks.tsx index d175413a45..442446e891 100644 --- a/packages/circuit-ui/components/SideNavigation/components/SecondaryLinks/SecondaryLinks.tsx +++ b/packages/circuit-ui/components/SideNavigation/components/SecondaryLinks/SecondaryLinks.tsx @@ -24,7 +24,7 @@ import { useFocusList, type FocusProps, } from '../../../../hooks/useFocusList/index.js'; -import { SubHeadline } from '../../../SubHeadline/index.js'; +import { Headline } from '../../../Headline/index.js'; import { Body } from '../../../Body/index.js'; import { Badge } from '../../../Badge/index.js'; import { useComponents } from '../../../ComponentsContext/index.js'; @@ -81,7 +81,9 @@ function SecondaryGroup({
  • {label && ( - {label} + + {label} + )}
      diff --git a/packages/circuit-ui/components/SubHeadline/SubHeadline.mdx b/packages/circuit-ui/components/SubHeadline/SubHeadline.mdx index 0909f36672..ebf2627ab0 100644 --- a/packages/circuit-ui/components/SubHeadline/SubHeadline.mdx +++ b/packages/circuit-ui/components/SubHeadline/SubHeadline.mdx @@ -5,7 +5,9 @@ import * as Stories from './SubHeadline.stories'; # SubHeadline - + +Use the Headline component in size `s` instead. + The SubHeadline component helps break up larger related chunks of content in the same section. It is typically used to separate subsections within a card. diff --git a/packages/circuit-ui/components/SubHeadline/SubHeadline.module.css b/packages/circuit-ui/components/SubHeadline/SubHeadline.module.css deleted file mode 100644 index 1c11dee923..0000000000 --- a/packages/circuit-ui/components/SubHeadline/SubHeadline.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.base { - font-size: var(--cui-typography-sub-headline-font-size); - font-weight: var(--cui-font-weight-bold); - line-height: var(--cui-typography-sub-headline-line-height); - color: var(--cui-fg-normal); - text-transform: uppercase; -} diff --git a/packages/circuit-ui/components/SubHeadline/SubHeadline.tsx b/packages/circuit-ui/components/SubHeadline/SubHeadline.tsx index a7832923d5..fc5ebeeef8 100644 --- a/packages/circuit-ui/components/SubHeadline/SubHeadline.tsx +++ b/packages/circuit-ui/components/SubHeadline/SubHeadline.tsx @@ -15,10 +15,8 @@ import { forwardRef, type HTMLAttributes } from 'react'; -import { clsx } from '../../styles/clsx.js'; -import { CircuitError } from '../../util/errors.js'; - -import classes from './SubHeadline.module.css'; +import { deprecate } from '../../util/logger.js'; +import { Headline } from '../Headline/Headline.js'; export interface SubHeadlineProps extends HTMLAttributes { /** @@ -30,25 +28,18 @@ export interface SubHeadlineProps extends HTMLAttributes { } /** - * A flexible SubHeadline component capable of rendering using any HTML heading - * element, except h1. + * @deprecated Use the Headline component in size `s` instead. */ export const SubHeadline = forwardRef( - ({ className, as, ...props }, ref) => { - if ( - process.env.NODE_ENV !== 'production' && - process.env.NODE_ENV !== 'test' && - !process?.env?.UNSAFE_DISABLE_ELEMENT_ERRORS && - !as - ) { - throw new CircuitError('SubHeadline', 'The `as` prop is required.'); + (props, ref) => { + if (process.env.NODE_ENV !== 'production') { + deprecate( + 'SubHeadline', + 'The SubHeadline component has been deprecated. Use the Headline component in size `s` instead.', + ); } - const Element = as || 'h2'; - - return ( - - ); + return ; }, ); diff --git a/packages/circuit-ui/components/Table/components/TableCell/TableCell.module.css b/packages/circuit-ui/components/Table/components/TableCell/TableCell.module.css index 98328356bb..5dc46c9b08 100644 --- a/packages/circuit-ui/components/Table/components/TableCell/TableCell.module.css +++ b/packages/circuit-ui/components/Table/components/TableCell/TableCell.module.css @@ -28,8 +28,8 @@ .condensed { padding: var(--cui-spacings-kilo) var(--cui-spacings-mega) var(--cui-spacings-kilo) var(--cui-spacings-giga); - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); } .presentation { @@ -47,17 +47,17 @@ .presentation.header { padding: var(--cui-spacings-byte) var(--cui-spacings-giga); - font-size: var(--cui-typography-body-two-font-size); + font-size: var(--cui-typography-body-s-font-size); font-weight: var(--cui-font-weight-bold); - line-height: var(--cui-typography-body-two-line-height); + line-height: var(--cui-typography-body-s-line-height); white-space: nowrap; } .condensed.presentation { padding: var(--cui-spacings-kilo) var(--cui-spacings-mega) var(--cui-spacings-kilo) var(--cui-spacings-giga); - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); } .condensed.presentation.header { diff --git a/packages/circuit-ui/components/Table/components/TableHeader/TableHeader.module.css b/packages/circuit-ui/components/Table/components/TableHeader/TableHeader.module.css index 5470c2316c..6fbce52f76 100644 --- a/packages/circuit-ui/components/Table/components/TableHeader/TableHeader.module.css +++ b/packages/circuit-ui/components/Table/components/TableHeader/TableHeader.module.css @@ -9,9 +9,9 @@ .base[scope="col"] { padding: var(--cui-spacings-byte) var(--cui-spacings-giga); - font-size: var(--cui-typography-body-two-font-size); + font-size: var(--cui-typography-body-s-font-size); font-weight: var(--cui-font-weight-bold); - line-height: var(--cui-typography-body-two-line-height); + line-height: var(--cui-typography-body-s-line-height); color: var(--cui-fg-subtle); white-space: nowrap; vertical-align: middle; @@ -44,8 +44,8 @@ .condensed { padding: var(--cui-spacings-kilo) var(--cui-spacings-mega) var(--cui-spacings-kilo) var(--cui-spacings-giga); - font-size: var(--cui-typography-body-two-font-size); - line-height: var(--cui-typography-body-two-line-height); + font-size: var(--cui-typography-body-s-font-size); + line-height: var(--cui-typography-body-s-line-height); vertical-align: middle; } diff --git a/packages/circuit-ui/components/Tabs/components/Tab/Tab.module.css b/packages/circuit-ui/components/Tabs/components/Tab/Tab.module.css index 45421d83de..63406bc225 100644 --- a/packages/circuit-ui/components/Tabs/components/Tab/Tab.module.css +++ b/packages/circuit-ui/components/Tabs/components/Tab/Tab.module.css @@ -6,8 +6,8 @@ float: left; height: 100%; padding: var(--cui-spacings-kilo) var(--cui-spacings-tera); - font-size: var(--cui-typography-body-one-font-size); - line-height: var(--cui-typography-body-one-line-height); + font-size: var(--cui-typography-body-m-font-size); + line-height: var(--cui-typography-body-m-line-height); color: var(--cui-fg-subtle); text-decoration: none; white-space: nowrap; diff --git a/packages/circuit-ui/components/Tag/Tag.module.css b/packages/circuit-ui/components/Tag/Tag.module.css index be2a2c1625..445fb1c225 100644 --- a/packages/circuit-ui/components/Tag/Tag.module.css +++ b/packages/circuit-ui/components/Tag/Tag.module.css @@ -10,8 +10,8 @@ align-items: center; padding: calc(var(--cui-spacings-bit) - 1px) var(--cui-spacings-kilo); margin: 0; - font-size: var(--cui-typography-body-one-font-size); - line-height: var(--cui-typography-body-one-line-height); + font-size: var(--cui-typography-body-m-font-size); + line-height: var(--cui-typography-body-m-line-height); word-break: break-word; cursor: default; background-color: var(--cui-bg-normal); diff --git a/packages/circuit-ui/components/Title/Title.mdx b/packages/circuit-ui/components/Title/Title.mdx deleted file mode 100644 index 4f183200c3..0000000000 --- a/packages/circuit-ui/components/Title/Title.mdx +++ /dev/null @@ -1,27 +0,0 @@ -import { Meta, Status, Props, Story } from '../../../../.storybook/components'; -import * as Stories from './Title.stories'; - - - -# Title - - - -The Title component is used to render headings with large typography. Typically, large typography is intended for landing pages. In most cases, the [Headline](Typography/Headline) should be used instead. - - - - -## Component variations - -### Sizes - -The Title component comes in four sizes. In most cases, use the [Headline component](Typography/Headline) component instead to render headings. - - - ---- - -## Accessibility - -All accessibility guidelines for the [Headline component](Typography/Headline) also apply to the Title component. diff --git a/packages/circuit-ui/components/Title/Title.module.css b/packages/circuit-ui/components/Title/Title.module.css deleted file mode 100644 index e2edae4e40..0000000000 --- a/packages/circuit-ui/components/Title/Title.module.css +++ /dev/null @@ -1,27 +0,0 @@ -.base { - font-weight: var(--cui-font-weight-bold); - color: var(--cui-fg-normal); - letter-spacing: -0.03em; -} - -/* Sizes */ - -.one { - font-size: var(--cui-typography-title-one-font-size); - line-height: var(--cui-typography-title-one-line-height); -} - -.two { - font-size: var(--cui-typography-title-two-font-size); - line-height: var(--cui-typography-title-two-line-height); -} - -.three { - font-size: var(--cui-typography-title-three-font-size); - line-height: var(--cui-typography-title-three-line-height); -} - -.four { - font-size: var(--cui-typography-title-four-font-size); - line-height: var(--cui-typography-title-four-line-height); -} diff --git a/packages/circuit-ui/components/Toggle/Toggle.module.css b/packages/circuit-ui/components/Toggle/Toggle.module.css index c9c7a7e0d8..5873d1a79e 100644 --- a/packages/circuit-ui/components/Toggle/Toggle.module.css +++ b/packages/circuit-ui/components/Toggle/Toggle.module.css @@ -96,9 +96,9 @@ .label { display: block; margin-left: var(--cui-spacings-kilo); - font-size: var(--cui-typography-body-one-font-size); + font-size: var(--cui-typography-body-m-font-size); font-weight: var(--cui-font-weight-regular); - line-height: var(--cui-typography-body-one-line-height); + line-height: var(--cui-typography-body-m-line-height); cursor: pointer; } diff --git a/packages/circuit-ui/components/Tooltip/Tooltip.module.css b/packages/circuit-ui/components/Tooltip/Tooltip.module.css index b9f2ae744f..420efc0083 100644 --- a/packages/circuit-ui/components/Tooltip/Tooltip.module.css +++ b/packages/circuit-ui/components/Tooltip/Tooltip.module.css @@ -73,9 +73,9 @@ .content { padding: var(--cui-spacings-byte) var(--cui-spacings-kilo); - font-size: var(--cui-typography-body-two-font-size); + font-size: var(--cui-typography-body-s-font-size); font-weight: var(--cui-font-weight-regular); - line-height: var(--cui-typography-body-two-line-height); + line-height: var(--cui-typography-body-s-line-height); color: var(--cui-fg-normal); background-color: var(--cui-bg-elevated); border: var(--cui-border-width-kilo) solid var(--cui-border-subtle); diff --git a/packages/circuit-ui/index.ts b/packages/circuit-ui/index.ts index 8263f8e5e1..be4c7a7e41 100644 --- a/packages/circuit-ui/index.ts +++ b/packages/circuit-ui/index.ts @@ -21,14 +21,18 @@ export { clsx } from './styles/clsx.js'; // Typography export { Headline } from './components/Headline/index.js'; export type { HeadlineProps } from './components/Headline/index.js'; -export { Title } from './components/Title/index.js'; -export type { TitleProps } from './components/Title/index.js'; +export { Display } from './components/Display/index.js'; +export type { DisplayProps } from './components/Display/index.js'; export { SubHeadline } from './components/SubHeadline/index.js'; export type { SubHeadlineProps } from './components/SubHeadline/index.js'; export { Body } from './components/Body/index.js'; export type { BodyProps } from './components/Body/index.js'; export { BodyLarge } from './components/BodyLarge/index.js'; export type { BodyLargeProps } from './components/BodyLarge/index.js'; +export { Compact } from './components/Compact/index.js'; +export type { CompactProps } from './components/Compact/index.js'; +export { Numeral } from './components/Numeral/index.js'; +export type { NumeralProps } from './components/Numeral/index.js'; export { Anchor } from './components/Anchor/index.js'; export type { AnchorProps } from './components/Anchor/index.js'; export { List } from './components/List/index.js'; diff --git a/packages/circuit-ui/styles/base.css b/packages/circuit-ui/styles/base.css index 21e82e8317..1a00db7611 100644 --- a/packages/circuit-ui/styles/base.css +++ b/packages/circuit-ui/styles/base.css @@ -183,8 +183,8 @@ html { } body { - font-size: var(--cui-typography-body-one-font-size); - line-height: var(--cui-typography-body-one-line-height); + font-size: var(--cui-typography-body-m-font-size); + line-height: var(--cui-typography-body-m-line-height); color: var(--cui-fg-normal); background-color: var(--cui-bg-normal); } diff --git a/packages/design-tokens/themes/schema.ts b/packages/design-tokens/themes/schema.ts index 2600fb99ad..bf08483dac 100644 --- a/packages/design-tokens/themes/schema.ts +++ b/packages/design-tokens/themes/schema.ts @@ -205,30 +205,221 @@ export const schema = [ { 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' }, + { name: '--cui-typography-display-l-font-size', type: 'dimension' }, + { name: '--cui-typography-display-l-line-height', type: 'dimension' }, + { name: '--cui-typography-display-l-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-display-m-font-size', type: 'dimension' }, + { name: '--cui-typography-display-m-line-height', type: 'dimension' }, + { name: '--cui-typography-display-m-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-display-s-font-size', type: 'dimension' }, + { name: '--cui-typography-display-s-line-height', type: 'dimension' }, + { name: '--cui-typography-display-s-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-headline-l-font-size', type: 'dimension' }, + { name: '--cui-typography-headline-l-line-height', type: 'dimension' }, + { name: '--cui-typography-headline-l-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-headline-m-font-size', type: 'dimension' }, + { name: '--cui-typography-headline-m-line-height', type: 'dimension' }, + { name: '--cui-typography-headline-m-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-headline-s-font-size', type: 'dimension' }, + { name: '--cui-typography-headline-s-line-height', type: 'dimension' }, + { name: '--cui-typography-headline-s-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-body-l-font-size', type: 'dimension' }, + { name: '--cui-typography-body-l-line-height', type: 'dimension' }, + { name: '--cui-typography-body-l-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-body-m-font-size', type: 'dimension' }, + { name: '--cui-typography-body-m-line-height', type: 'dimension' }, + { name: '--cui-typography-body-m-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-body-s-font-size', type: 'dimension' }, + { name: '--cui-typography-body-s-line-height', type: 'dimension' }, + { name: '--cui-typography-body-s-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-compact-l-font-size', type: 'dimension' }, + { name: '--cui-typography-compact-l-line-height', type: 'dimension' }, + { name: '--cui-typography-compact-l-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-compact-m-font-size', type: 'dimension' }, + { name: '--cui-typography-compact-m-line-height', type: 'dimension' }, + { name: '--cui-typography-compact-m-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-compact-s-font-size', type: 'dimension' }, + { name: '--cui-typography-compact-s-line-height', type: 'dimension' }, + { name: '--cui-typography-compact-s-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-numeral-l-font-size', type: 'dimension' }, + { name: '--cui-typography-numeral-l-line-height', type: 'dimension' }, + { name: '--cui-typography-numeral-l-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-numeral-m-font-size', type: 'dimension' }, + { name: '--cui-typography-numeral-m-line-height', type: 'dimension' }, + { name: '--cui-typography-numeral-m-letter-spacing', type: 'dimension' }, + { name: '--cui-typography-numeral-s-font-size', type: 'dimension' }, + { name: '--cui-typography-numeral-s-line-height', type: 'dimension' }, + { name: '--cui-typography-numeral-s-letter-spacing', type: 'dimension' }, + /* eslint-disable @sumup-oss/circuit-ui/no-deprecated-custom-properties */ + { + name: '--cui-typography-headline-one-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-l-font-size', + }, + }, + { + name: '--cui-typography-headline-one-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-l-line-height', + }, + }, + { + name: '--cui-typography-headline-two-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-m-font-size', + }, + }, + { + name: '--cui-typography-headline-two-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-m-line-height', + }, + }, + { + name: '--cui-typography-headline-three-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-m-font-size', + }, + }, + { + name: '--cui-typography-headline-three-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-m-line-height', + }, + }, + { + name: '--cui-typography-headline-four-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-s-font-size', + }, + }, + { + name: '--cui-typography-headline-four-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-s-line-height', + }, + }, + { + name: '--cui-typography-title-one-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-display-l-font-size', + }, + }, + { + name: '--cui-typography-title-one-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-display-l-line-height', + }, + }, + { + name: '--cui-typography-title-two-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-display-m-font-size', + }, + }, + { + name: '--cui-typography-title-two-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-display-m-line-height', + }, + }, + { + name: '--cui-typography-title-three-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-display-m-font-size', + }, + }, + { + name: '--cui-typography-title-three-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-display-m-line-height', + }, + }, + { + name: '--cui-typography-title-four-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-display-s-font-size', + }, + }, + { + name: '--cui-typography-title-four-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-display-s-line-height', + }, + }, + { + name: '--cui-typography-sub-headline-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-s-font-size', + }, + }, + { + name: '--cui-typography-sub-headline-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-headline-s-line-height', + }, + }, + { + name: '--cui-typography-body-one-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-body-m-font-size', + }, + }, + { + name: '--cui-typography-body-one-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-body-m-line-height', + }, + }, + { + name: '--cui-typography-body-two-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-body-s-font-size', + }, + }, + { + name: '--cui-typography-body-two-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-body-s-line-height', + }, + }, + { + name: '--cui-typography-body-large-font-size', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-body-l-font-size', + }, + }, + { + name: '--cui-typography-body-large-line-height', + type: 'dimension', + deprecation: { + replacement: '--cui-typography-body-l-line-height', + }, + }, + /* eslint-disable @sumup-oss/circuit-ui/no-deprecated-custom-properties */ /* Z-indices */ { name: '--cui-z-index-default', type: 'number' }, { name: '--cui-z-index-absolute', type: 'number' }, @@ -240,4 +431,8 @@ export const schema = [ { 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 }[]; +] satisfies { + name: TokenName; + type: TokenType; + deprecation?: { replacement: TokenName }; +}[]; diff --git a/packages/design-tokens/themes/shared.ts b/packages/design-tokens/themes/shared.ts index cb524c3dc6..81fe9b2fcf 100644 --- a/packages/design-tokens/themes/shared.ts +++ b/packages/design-tokens/themes/shared.ts @@ -161,6 +161,232 @@ export const shared = [ type: 'duration', }, /* Typography */ + { + name: '--cui-typography-display-l-font-size', + value: '4rem', + type: 'dimension', + }, + { + name: '--cui-typography-display-l-line-height', + value: '4.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-display-l-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-display-m-font-size', + value: '3rem', + type: 'dimension', + }, + { + name: '--cui-typography-display-m-line-height', + value: '3.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-display-m-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-display-s-font-size', + value: '2.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-display-s-line-height', + value: '2.875rem', + type: 'dimension', + }, + { + name: '--cui-typography-display-s-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-headline-l-font-size', + value: '2rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-l-line-height', + value: '2.25rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-l-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-headline-m-font-size', + value: '1.375rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-m-line-height', + value: '1.625rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-m-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-headline-s-font-size', + value: '1.125rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-s-line-height', + value: '1.375rem', + type: 'dimension', + }, + { + name: '--cui-typography-headline-s-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-body-l-font-size', + value: '1.25rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-l-line-height', + value: '1.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-l-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-body-m-font-size', + value: '1rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-m-line-height', + value: '1.375rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-m-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-body-s-font-size', + value: '0.875rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-s-line-height', + value: '1.24rem', + type: 'dimension', + }, + { + name: '--cui-typography-body-s-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-compact-l-font-size', + value: '1.125rem', + type: 'dimension', + }, + { + name: '--cui-typography-compact-l-line-height', + value: '1.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-compact-l-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-compact-m-font-size', + value: '0.9375rem', + type: 'dimension', + }, + { + name: '--cui-typography-compact-m-line-height', + value: '1.0625rem', + type: 'dimension', + }, + { + name: '--cui-typography-compact-m-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-compact-s-font-size', + value: '0.8125rem', + type: 'dimension', + }, + { + name: '--cui-typography-compact-s-line-height', + value: '0.9375rem', + type: 'dimension', + }, + { + name: '--cui-typography-compact-s-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-numeral-l-font-size', + value: '3rem', + type: 'dimension', + }, + { + name: '--cui-typography-numeral-l-line-height', + value: '3.375rem', + type: 'dimension', + }, + { + name: '--cui-typography-numeral-l-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-numeral-m-font-size', + value: '1.5rem', + type: 'dimension', + }, + { + name: '--cui-typography-numeral-m-line-height', + value: '1.75rem', + type: 'dimension', + }, + { + name: '--cui-typography-numeral-m-letter-spacing', + value: '0px', + type: 'dimension', + }, + { + name: '--cui-typography-numeral-s-font-size', + value: '1rem', + type: 'dimension', + }, + { + name: '--cui-typography-numeral-s-line-height', + value: '1rem', + type: 'dimension', + }, + { + name: '--cui-typography-numeral-s-letter-spacing', + value: '0px', + type: 'dimension', + }, + /* eslint-disable @sumup-oss/circuit-ui/no-deprecated-custom-properties */ { name: '--cui-typography-headline-one-font-size', value: '2rem', @@ -173,22 +399,22 @@ export const shared = [ }, { name: '--cui-typography-headline-two-font-size', - value: '1.5rem', + value: '1.375rem', type: 'dimension', }, { name: '--cui-typography-headline-two-line-height', - value: '1.75rem', + value: '1.625rem', type: 'dimension', }, { name: '--cui-typography-headline-three-font-size', - value: '1.25rem', + value: '1.375rem', type: 'dimension', }, { name: '--cui-typography-headline-three-line-height', - value: '1.5rem', + value: '1.625rem', type: 'dimension', }, { @@ -198,57 +424,57 @@ export const shared = [ }, { name: '--cui-typography-headline-four-line-height', - value: '1.5rem', + value: '1.375rem', type: 'dimension', }, { name: '--cui-typography-title-one-font-size', - value: '7.5rem', + value: '4rem', type: 'dimension', }, { name: '--cui-typography-title-one-line-height', - value: '7.5rem', + value: '4.5rem', type: 'dimension', }, { name: '--cui-typography-title-two-font-size', - value: '6rem', + value: '3rem', type: 'dimension', }, { name: '--cui-typography-title-two-line-height', - value: '6rem', + value: '3.5rem', type: 'dimension', }, { name: '--cui-typography-title-three-font-size', - value: '4rem', + value: '3rem', type: 'dimension', }, { name: '--cui-typography-title-three-line-height', - value: '4rem', + value: '3.5rem', type: 'dimension', }, { name: '--cui-typography-title-four-font-size', - value: '3.5rem', + value: '2.5rem', type: 'dimension', }, { name: '--cui-typography-title-four-line-height', - value: '3.5rem', + value: '2.875rem', type: 'dimension', }, { name: '--cui-typography-sub-headline-font-size', - value: '0.875rem', + value: '1.125rem', type: 'dimension', }, { name: '--cui-typography-sub-headline-line-height', - value: '1.25rem', + value: '1.375rem', type: 'dimension', }, { @@ -258,7 +484,7 @@ export const shared = [ }, { name: '--cui-typography-body-one-line-height', - value: '1.5rem', + value: '1.375rem', type: 'dimension', }, { @@ -278,9 +504,10 @@ export const shared = [ }, { name: '--cui-typography-body-large-line-height', - value: '1.75rem', + value: '1.5rem', type: 'dimension', }, + /* eslint-enable @sumup-oss/circuit-ui/no-deprecated-custom-properties */ /* Z-indices */ { name: '--cui-z-index-default', diff --git a/packages/eslint-plugin-circuit-ui/component-lifecycle-imports/index.ts b/packages/eslint-plugin-circuit-ui/component-lifecycle-imports/index.ts index 3d5fec786f..909f6f48e6 100644 --- a/packages/eslint-plugin-circuit-ui/component-lifecycle-imports/index.ts +++ b/packages/eslint-plugin-circuit-ui/component-lifecycle-imports/index.ts @@ -132,8 +132,7 @@ export const componentLifecycleImports = createRule({ fixes.push( fixer.replaceText( node, - context - .getSourceCode() + context.sourceCode .getText(node) .replace(importSpecifier, '') .replace(' ,', ''), diff --git a/packages/eslint-plugin-circuit-ui/index.ts b/packages/eslint-plugin-circuit-ui/index.ts index 9afd975cd3..15d218eac1 100644 --- a/packages/eslint-plugin-circuit-ui/index.ts +++ b/packages/eslint-plugin-circuit-ui/index.ts @@ -15,6 +15,7 @@ import { componentLifecycleImports } from './component-lifecycle-imports'; import { noInvalidCustomProperties } from './no-invalid-custom-properties'; +import { noDeprecatedCustomProperties } from './no-deprecated-custom-properties'; import { noDeprecatedComponents } from './no-deprecated-components'; import { noDeprecatedProps } from './no-deprecated-props'; import { noRenamedProps } from './no-renamed-props'; @@ -26,6 +27,7 @@ import { renamedPackageScope } from './renamed-package-scope'; export const rules = { 'component-lifecycle-imports': componentLifecycleImports, 'no-invalid-custom-properties': noInvalidCustomProperties, + 'no-deprecated-custom-properties': noDeprecatedCustomProperties, 'no-deprecated-components': noDeprecatedComponents, 'no-deprecated-props': noDeprecatedProps, 'no-renamed-props': noRenamedProps, diff --git a/packages/eslint-plugin-circuit-ui/no-deprecated-components/index.ts b/packages/eslint-plugin-circuit-ui/no-deprecated-components/index.ts index 1fb4c815f1..d49163e463 100644 --- a/packages/eslint-plugin-circuit-ui/no-deprecated-components/index.ts +++ b/packages/eslint-plugin-circuit-ui/no-deprecated-components/index.ts @@ -31,6 +31,14 @@ const components = [ name: 'Selector', alternative: 'Use the SelectorGroup component instead.', }, + { + name: 'SubHeadline', + alternative: 'Use the Headline component in size `s` instead.', + }, + { + name: 'Title', + alternative: 'Use the new Display component instead.', + }, ]; export const noDeprecatedComponents = createRule({ diff --git a/packages/eslint-plugin-circuit-ui/no-deprecated-custom-properties/README.md b/packages/eslint-plugin-circuit-ui/no-deprecated-custom-properties/README.md new file mode 100644 index 0000000000..7087c49063 --- /dev/null +++ b/packages/eslint-plugin-circuit-ui/no-deprecated-custom-properties/README.md @@ -0,0 +1,37 @@ +# Replace deprecated CSS custom properties (`no-deprecated-custom-properties`) + +Occasionally, CSS custom properties are removed or renamed. This rule flags uses of deprecated custom properties. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```css +.class { + font-size: var(--cui-typography-headline-one-font-size); + line-height: var(--cui-typography-headline-one-line-height); +} +``` + +Examples of **correct** code for this rule: + +```css +.class { + font-size: var(--cui-typography-headline-l-font-size); + line-height: var(--cui-typography-headline-l-line-height); +} +``` + +### 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 +- [Migration guide](https://github.com/sumup-oss/circuit-ui/blob/main/MIGRATION.md) +- [Design token release notes](https://github.com/sumup-oss/circuit-ui/blob/main/packages/design-tokens/CHANGELOG.md) diff --git a/packages/eslint-plugin-circuit-ui/no-deprecated-custom-properties/index.spec.ts b/packages/eslint-plugin-circuit-ui/no-deprecated-custom-properties/index.spec.ts new file mode 100644 index 0000000000..e154e65adc --- /dev/null +++ b/packages/eslint-plugin-circuit-ui/no-deprecated-custom-properties/index.spec.ts @@ -0,0 +1,131 @@ +/** + * 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-oss/circuit-ui/no-deprecated-custom-properties */ + +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import { noDeprecatedCustomProperties } from '.'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-call +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, +}); + +// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access +ruleTester.run( + 'no-deprecated-custom-properties', + noDeprecatedCustomProperties, + { + valid: [ + { + name: 'custom properties in a JS object', + code: ` + const typography = { + fontSize: "var(--cui-typography-headline-l-font-size)", + lineHeight: "var(--cui-typography-headline-l-line-height)", + } + `, + }, + { + name: 'custom properties in a tagged template literal', + code: ` + const styles = css\` + font-size: var(--cui-typography-headline-l-font-size); + line-height: var(--cui-typography-headline-l-line-height); + \`; + `, + }, + { + name: 'custom properties in inline styles', + code: ` + function Component() { + return ( +

      + Success +

      + ); + } + `, + }, + ], + invalid: [ + { + name: 'custom properties in a JS object', + code: ` + const typography = { + fontSize: "var(--cui-typography-headline-one-font-size)", + lineHeight: "var(--cui-typography-headline-one-line-height)", + } + `, + errors: [ + { + messageId: 'deprecated', + }, + { + messageId: 'deprecated', + }, + ], + }, + { + name: 'custom properties in a tagged template literal', + code: ` + const styles = css\` + font-size: var(--cui-typography-headline-one-font-size); + line-height: var(--cui-typography-headline-one-line-height); + \`; + `, + errors: [ + { + messageId: 'deprecated', + }, + { + messageId: 'deprecated', + }, + ], + }, + { + name: 'custom properties in inline styles', + code: ` + function Component() { + return ( +

      + Success +

      + ); + } + `, + errors: [ + { + messageId: 'deprecated', + }, + { + messageId: 'deprecated', + }, + ], + }, + ], + }, +); diff --git a/packages/eslint-plugin-circuit-ui/no-deprecated-custom-properties/index.ts b/packages/eslint-plugin-circuit-ui/no-deprecated-custom-properties/index.ts new file mode 100644 index 0000000000..e3a8bdc3f6 --- /dev/null +++ b/packages/eslint-plugin-circuit-ui/no-deprecated-custom-properties/index.ts @@ -0,0 +1,84 @@ +/** + * 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 } from '@typescript-eslint/utils'; +import { schema } from '@sumup-oss/design-tokens'; + +const DEPRECATED_CUSTOM_PROPERTIES = schema.filter(({ deprecation }) => + Boolean(deprecation), +); +const REGEX_STRING = DEPRECATED_CUSTOM_PROPERTIES.map(({ name }) => name).join( + '|', +); + +/* eslint-disable */ + +const createRule = ESLintUtils.RuleCreator( + (name) => + `https://github.com/sumup-oss/circuit-ui/tree/main/packages/eslint-plugin-circuit-ui/${name}`, +); + +export const noDeprecatedCustomProperties = createRule({ + name: 'no-deprecated-custom-properties', + meta: { + type: 'suggestion', + schema: [], + docs: { + description: 'Deprecated custom properties should be removed or replaced', + recommended: 'strict', + }, + messages: { + deprecated: + 'The `{{name}}` custom property has been deprecated. Use the ` {{replacement}}` custom property instead.', + }, + }, + defaultOptions: [], + create(context) { + return { + // Inspired by `no-tabs`: https://github.com/eslint/eslint/blob/b98fdd413a3b07b262bfce6f704c1c1bb8582770/lib/rules/no-tabs.js + Program(node) { + context.sourceCode.getLines().forEach((line, index) => { + const regex = new RegExp(REGEX_STRING, 'g'); + let match: RegExpExecArray | null; + // biome-ignore lint/suspicious/noAssignInExpressions: + while ((match = regex.exec(line)) !== null) { + const name = match[0]; + const { replacement } = DEPRECATED_CUSTOM_PROPERTIES.find( + (token) => token.name === name, + )!.deprecation!; + context.report({ + node, + loc: { + start: { + line: index + 1, + column: match.index, + }, + end: { + line: index + 1, + column: match.index + match[0].length, + }, + }, + messageId: 'deprecated', + data: { + name, + replacement, + }, + }); + } + }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin-circuit-ui/no-deprecated-props/index.ts b/packages/eslint-plugin-circuit-ui/no-deprecated-props/index.ts index 661a80551d..793ce6b7ae 100644 --- a/packages/eslint-plugin-circuit-ui/no-deprecated-props/index.ts +++ b/packages/eslint-plugin-circuit-ui/no-deprecated-props/index.ts @@ -65,6 +65,12 @@ const mappings: Config[] = [ props: ['variant'], alternative: '', }, + { + components: ['Body'], + props: ['variant'], + alternative: + 'Use the new `color` prop instead of the `alert`, `confirm` and `subtle` variants. Use the new `weight` prop instead of the `highlight` variant. Use custom CSS for the `quote` variant.', + }, ]; export const noDeprecatedProps = createRule({ 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 dcf2b0465f..81ae81152f 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 @@ -42,16 +42,15 @@ export const noInvalidCustomProperties = createRule({ recommended: 'recommended', }, messages: { - invalid: "'{{name}}' is not a valid Circuit UI design token.", + invalid: '`{{name}}` is not a valid Circuit UI design token.', }, }, defaultOptions: [], create(context) { - const sourceCode = context.getSourceCode(); return { // Inspired by `no-tabs`: https://github.com/eslint/eslint/blob/b98fdd413a3b07b262bfce6f704c1c1bb8582770/lib/rules/no-tabs.js Program(node) { - sourceCode.getLines().forEach((line, index) => { + context.sourceCode.getLines().forEach((line, index) => { const regex = new RegExp(REGEX_STRING, 'g'); let match: RegExpExecArray | null; // biome-ignore lint/suspicious/noAssignInExpressions: diff --git a/packages/eslint-plugin-circuit-ui/no-renamed-props/README.md b/packages/eslint-plugin-circuit-ui/no-renamed-props/README.md index 7159438dea..c2c580d912 100644 --- a/packages/eslint-plugin-circuit-ui/no-renamed-props/README.md +++ b/packages/eslint-plugin-circuit-ui/no-renamed-props/README.md @@ -16,6 +16,17 @@ Note that the rule can only lint direct uses of a component. Wrapped instances s Examples of **incorrect** code for this rule: ```tsx +// Since Circuit UI v9 +function Component() { + return ( +
      + + + +
      + ); +} + // Since Circuit UI v7.5 function Component() { return ( @@ -53,6 +64,17 @@ function Component() { Examples of **correct** code for this rule: ```tsx +// Since Circuit UI v9 +function Component() { + return ( +
      + + + +
      + ); +} + // Since Circuit UI v7.5 function Component() { return ( diff --git a/packages/eslint-plugin-circuit-ui/no-renamed-props/index.spec.ts b/packages/eslint-plugin-circuit-ui/no-renamed-props/index.spec.ts index 6e28b59127..33247a1ac8 100644 --- a/packages/eslint-plugin-circuit-ui/no-renamed-props/index.spec.ts +++ b/packages/eslint-plugin-circuit-ui/no-renamed-props/index.spec.ts @@ -77,6 +77,24 @@ ruleTester.run('no-renamed-props', noRenamedProps, { } `, }, + { + name: 'matched Body component without the variant prop', + code: ` + function Component() { + return Lorem ipsum + } + `, + }, + { + name: 'matched Body component with variant="quote"', + code: ` + function Component() { + return ( + Lorem ipsum + ) + } + `, + }, ], invalid: [ { @@ -312,5 +330,35 @@ ruleTester.run('no-renamed-props', noRenamedProps, { { messageId: 'propName' }, ], }, + { + name: 'matched Body component with the old prop value', + code: ` + function ComponentA() { + return ( + Lorem ipsum + ) + } + + function ComponentB() { + return ( + Lorem ipsum + ) + } + `, + output: ` + function ComponentA() { + return ( + Lorem ipsum + ) + } + + function ComponentB() { + return ( + Lorem ipsum + ) + } + `, + errors: [{ messageId: 'bodyVariant' }, { messageId: 'bodyVariant' }], + }, ], }); diff --git a/packages/eslint-plugin-circuit-ui/no-renamed-props/index.ts b/packages/eslint-plugin-circuit-ui/no-renamed-props/index.ts index b500a6e2b7..b36978bcc3 100644 --- a/packages/eslint-plugin-circuit-ui/no-renamed-props/index.ts +++ b/packages/eslint-plugin-circuit-ui/no-renamed-props/index.ts @@ -55,7 +55,10 @@ type CustomConfig = { hook?: string; transform: ( node: TSESTree.JSXElement, - context: TSESLint.RuleContext<'propName' | 'propValue', never[]>, + context: TSESLint.RuleContext< + 'propName' | 'propValue' | 'bodyVariant', + never[] + >, ) => void; }; @@ -292,6 +295,104 @@ const configs: Config[] = [ }); }, }, + { + type: 'values', + component: 'Title', + prop: 'size', + values: { + one: 'l', + two: 'm', + three: 'm', + four: 's', + }, + }, + { + type: 'values', + component: 'Display', + prop: 'size', + values: { + one: 'l', + two: 'm', + three: 'm', + four: 's', + }, + }, + { + type: 'values', + component: 'Headline', + prop: 'size', + values: { + one: 'l', + two: 'm', + three: 'm', + four: 's', + }, + }, + { + type: 'values', + component: 'Body', + prop: 'size', + values: { + one: 'm', + two: 's', + }, + }, + { + type: 'custom', + component: 'Body', + // variant → weight or color + transform: (node, context) => { + const component = 'IconButton'; + + node.openingElement.attributes.forEach((attribute) => { + if ( + attribute.type !== 'JSXAttribute' || + attribute.name.type !== 'JSXIdentifier' || + attribute.name.name !== 'variant' + ) { + return; + } + + const current = getAttributeValue(attribute); + + if (current === 'highlight') { + const replacement = `weight="bold"`; + const weightAttribute = findAttribute(node, 'weight'); + context.report({ + node: attribute, + messageId: 'bodyVariant', + data: { component, current, replacement }, + fix: weightAttribute + ? undefined + : (fixer) => { + return fixer.replaceText(attribute, replacement); + }, + }); + return; + } + + if (current && ['alert', 'confirm', 'subtle'].includes(current)) { + const replacementMap: Record = { + 'alert': `color="danger"`, + 'confirm': `color="success"`, + 'subtle': `color="subtle"`, + }; + const replacement = replacementMap[current]; + const colorAttribute = findAttribute(node, 'color'); + context.report({ + node: attribute, + messageId: 'bodyVariant', + data: { component, current, replacement }, + fix: colorAttribute + ? undefined + : (fixer) => { + return fixer.replaceText(attribute, replacement); + }, + }); + } + }); + }, + }, ]; export const noRenamedProps = createRule({ @@ -309,6 +410,8 @@ export const noRenamedProps = createRule({ "The {{component}}'s `{{current}}` prop has been renamed to `{{replacement}}`.", propValue: "The {{component}}'s `{{prop}}` prop values have been renamed. Replace `{{current}}` with `{{replacement}}`.", + bodyVariant: + 'The {{component}}\'s `variant` prop has been deprecated. Replace `variant="{{current}}"` with `{{replacement}}`.', }, }, defaultOptions: [], diff --git a/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.ts b/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.ts index 116d5f6267..a8c2090b0f 100644 --- a/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.ts +++ b/packages/eslint-plugin-circuit-ui/prefer-custom-properties/index.ts @@ -36,7 +36,7 @@ export const preferCustomProperties = createRule({ }, messages: { replace: - "Use CSS custom properties instead of the Emotion.js theme. Replace '{{jsToken}}' with '{{cssVariable}}'.", + '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.', }, }, @@ -120,8 +120,7 @@ export const preferCustomProperties = createRule({ const jsToken = `\${${identifiers.join('.')}}`; const cssVariable = `var(${customProperty})`; - const text = context - .getSourceCode() + const text = context.sourceCode .getText(node) .replace(jsToken, cssVariable); diff --git a/packages/stylelint-plugin-circuit-ui/index.ts b/packages/stylelint-plugin-circuit-ui/index.ts index 3b0547d230..b365308eca 100644 --- a/packages/stylelint-plugin-circuit-ui/index.ts +++ b/packages/stylelint-plugin-circuit-ui/index.ts @@ -14,5 +14,6 @@ */ import { noInvalidCustomProperties } from './no-invalid-custom-properties/index.js'; +import { noDeprecatedCustomProperties } from './no-deprecated-custom-properties/index.js'; -export default [noInvalidCustomProperties]; +export default [noInvalidCustomProperties, noDeprecatedCustomProperties]; diff --git a/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/README.md b/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/README.md new file mode 100644 index 0000000000..8d9138c11c --- /dev/null +++ b/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/README.md @@ -0,0 +1,31 @@ +# Do not use deprecated Circuit UI custom properties (`no-deprecated-custom-properties`) + +Occasionally, CSS custom properties are removed or renamed. This rule flags uses of deprecated custom properties. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```css +color: var(--cui-typography-headline-one-font-size); +``` + +Examples of **correct** code for this rule: + +```css +color: var(--cui-typography-headline-l-font-size); +``` + +### 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 +- [Migration guide](https://github.com/sumup-oss/circuit-ui/blob/main/MIGRATION.md) +- [Design token release notes](https://github.com/sumup-oss/circuit-ui/blob/main/packages/design-tokens/CHANGELOG.md) diff --git a/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/index.spec.ts b/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/index.spec.ts new file mode 100644 index 0000000000..146bd41a8d --- /dev/null +++ b/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/index.spec.ts @@ -0,0 +1,113 @@ +/** + * 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-oss/circuit-ui/no-deprecated-custom-properties */ + +import { testRule } from '../setupTests.js'; + +import { noDeprecatedCustomProperties, ruleName, messages } from './index.js'; + +testRule({ + plugins: [noDeprecatedCustomProperties], + ruleName, + config: true, + fix: false, + + accept: [ + { + code: `.class { + font-size: var(--cui-typography-headline-l-font-size); + }`, + description: 'Allow valid custom properties', + }, + { + code: `.class { + margin-bottom: calc(var(--cui-spacings-bit) - var(--cui-typography-headline-l-line-height)); + }`, + description: 'Allow valid custom properties in complex style rules', + }, + ], + + reject: [ + { + code: `.class { + font-size: var(--cui-typography-headline-one-font-size); + }`, + fixed: `.class { + font-size: var(--cui-typography-headline-l-font-size); + }`, + description: 'Disallow deprecated custom properties', + message: messages.deprecated( + '--cui-typography-headline-one-font-size', + '--cui-typography-headline-l-font-size', + ), + line: 2, + column: 9, + endLine: 2, + endColumn: 65, + }, + { + code: `.class { + margin-bottom: calc(var(--cui-spacings-bit) - var(--cui-typography-headline-one-line-height)); + }`, + fixed: `.class { + margin-bottom: calc(var(--cui-spacings-bit) - var(--cui-typography-headline-l-line-height)); + }`, + description: + 'Disallow deprecated custom properties in complex style rules', + message: messages.deprecated( + '--cui-typography-headline-one-line-height', + '--cui-typography-headline-l-line-height', + ), + line: 2, + column: 9, + endLine: 2, + endColumn: 103, + }, + { + code: `.class { + margin-bottom: calc(var(--cui-spacings-bit) - var(--cui-typography-headline-one-line-height) - var(--cui-typography-headline-two-line-height)); + }`, + fixed: `.class { + margin-bottom: calc(var(--cui-spacings-bit) - var(--cui-typography-headline-l-line-height) - var(--cui-typography-headline-m-line-height)); + }`, + description: + 'Disallow multiple deprecated custom properties in complex style rules', + warnings: [ + { + message: messages.deprecated( + '--cui-typography-headline-one-line-height', + '--cui-typography-headline-l-line-height', + ), + line: 2, + column: 9, + endLine: 2, + endColumn: 152, + }, + { + message: messages.deprecated( + '--cui-typography-headline-two-line-height', + '--cui-typography-headline-m-line-height', + ), + line: 2, + column: 9, + endLine: 2, + endColumn: 152, + }, + ], + }, + ], +}); diff --git a/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/index.ts b/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/index.ts new file mode 100644 index 0000000000..39b315a6f7 --- /dev/null +++ b/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/index.ts @@ -0,0 +1,75 @@ +/** + * 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 stylelint, { type Rule } from 'stylelint'; +import { schema } from '@sumup-oss/design-tokens'; + +const DEPRECATED_CUSTOM_PROPERTIES = schema.filter(({ deprecation }) => + Boolean(deprecation), +); +const REGEX_STRING = DEPRECATED_CUSTOM_PROPERTIES.map(({ name }) => name).join( + '|', +); + +export const ruleName = 'circuit-ui/no-deprecated-custom-properties'; + +export const meta = { + url: 'https://github.com/sumup-oss/circuit-ui/tree/main/packages/stylelint-plugin-circuit-ui/no-deprecated-custom-properties/README.md', + fixable: true, +}; + +export const messages = stylelint.utils.ruleMessages(ruleName, { + deprecated: (name: string, replacement: string) => + `The \`${name}\` custom property has been deprecated. Use the \`${replacement}\` custom property instead.`, +}); + +const rule: Rule = (enabled, _options, context) => (root, result) => { + if (!enabled || DEPRECATED_CUSTOM_PROPERTIES.length === 0) { + return; + } + + root.walkDecls((decl) => { + const regex = new RegExp(REGEX_STRING, 'g'); + let match: RegExpExecArray | null; + // biome-ignore lint/suspicious/noAssignInExpressions: + while ((match = regex.exec(decl.value)) !== null) { + const name = match[0]; + // biome-ignore lint/style/noNonNullAssertion: + const { replacement } = DEPRECATED_CUSTOM_PROPERTIES.find( + (token) => token.name === name, + )!.deprecation!; + + if (context?.fix) { + decl.value = decl.value.replace(name, replacement); + } + + stylelint.utils.report({ + message: messages.deprecated(name, replacement), + node: decl, + result, + ruleName, + }); + } + }); +}; + +rule.ruleName = ruleName; +rule.meta = meta; +rule.messages = messages; + +export const noDeprecatedCustomProperties = stylelint.createPlugin( + ruleName, + rule, +); diff --git a/templates/astro/src/pages/index.astro b/templates/astro/src/pages/index.astro index 30fd0d931c..4326b46f3d 100644 --- a/templates/astro/src/pages/index.astro +++ b/templates/astro/src/pages/index.astro @@ -1,5 +1,5 @@ --- -import { Title, BodyLarge } from '@sumup-oss/circuit-ui'; +import { Display, BodyLarge } from '@sumup-oss/circuit-ui'; import Root from '../layouts/Root.astro'; import DocCard from '../components/DocCard.astro'; @@ -7,9 +7,9 @@ const title = 'Welcome to Circuit UI + Astro'; --- - + <Display as="h1" size="three"> {title} - + Get started by editing src/pages/index.astro diff --git a/templates/nextjs/template/app/page.tsx b/templates/nextjs/template/app/page.tsx index 2654164ef6..3fe28a978d 100644 --- a/templates/nextjs/template/app/page.tsx +++ b/templates/nextjs/template/app/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next'; -import { Title, BodyLarge } from '@sumup-oss/circuit-ui'; +import { Display, BodyLarge } from '@sumup-oss/circuit-ui'; import { SumUpLogo } from '@sumup-oss/icons'; import { DocCard } from '../components/DocCard'; @@ -14,9 +14,9 @@ export default function Page() { return (
      - + <Display as="h1" size="three"> {metadata.title as string} - + Get started by editing app/page.tsx diff --git a/templates/remix/app/routes/_index/route.tsx b/templates/remix/app/routes/_index/route.tsx index 6eb181a587..2399a74711 100644 --- a/templates/remix/app/routes/_index/route.tsx +++ b/templates/remix/app/routes/_index/route.tsx @@ -1,5 +1,5 @@ import type { MetaFunction } from '@remix-run/node'; -import { Title, BodyLarge } from '@sumup-oss/circuit-ui'; +import { Display, BodyLarge } from '@sumup-oss/circuit-ui'; import { DocCard } from '../../components/DocCard/index.js'; @@ -18,9 +18,9 @@ export const meta: MetaFunction = () => [ export default function Index() { return ( <> - + <Display as="h1" size="three"> {title} - + Get started by editing app/routes/_index/route.tsx