diff --git a/src/avatar/avatar.stories.tsx b/src/avatar/avatar.stories.tsx index cd88227a7..4561d4787 100644 --- a/src/avatar/avatar.stories.tsx +++ b/src/avatar/avatar.stories.tsx @@ -135,9 +135,9 @@ function StorySection({ return ( - {title} + {title} {description ? ( - + {description} ) : null} @@ -152,7 +152,7 @@ function AvatarExample({ label, children }: { label: string; children: React.Rea {children} - + {label} diff --git a/src/base-field/base-field.tsx b/src/base-field/base-field.tsx index 08c253d63..27c66986a 100644 --- a/src/base-field/base-field.tsx +++ b/src/base-field/base-field.tsx @@ -30,7 +30,7 @@ function fieldToneToTextTone(tone: FieldTone) { function FieldMessage({ id, children, tone }: FieldMessageProps) { return ( - + } tone={fieldToneToTextTone(tone)} variant="callout-2" id={id}> {tone === 'loading' ? ( + {children} ) @@ -334,9 +334,8 @@ function BaseField({ alignItems="flexEnd" > } > {label ? ( {label} diff --git a/src/box/box.stories.tsx b/src/box/box.stories.tsx index 8e8399d35..263481975 100644 --- a/src/box/box.stories.tsx +++ b/src/box/box.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { Heading } from '../heading' import { Inline } from '../inline' import { Stack } from '../stack' import { Text } from '../text' @@ -148,7 +147,9 @@ function PaddedBox({ prop, value }: { prop: keyof BoxPaddingProps; value: Space export function PaddingStory({ padding }: { padding: Space }) { return ( - The transparent bordered box has padding + }> + The transparent bordered box has padding + @@ -214,7 +215,9 @@ function MarginBox({ prop, value }: { prop: keyof BoxMarginProps; value: Space } export function MarginStory({ margin }: { margin: Space }) { return ( - The shaded box has margin + }> + The shaded box has margin + When margin is negative, you will see the outer bordered box appear to be inside the shaded box. @@ -272,7 +275,9 @@ export function OverlayScrollStory() { background="aside" > - Scrollable Content with Overlay Scroll + }> + Scrollable Content with Overlay Scroll + This Box component demonstrates the overlay scroll functionality. The scrollbar is hidden by default and appears on hover. @@ -280,7 +285,7 @@ export function OverlayScrollStory() { {Array.from({ length: 20 }, (_, i) => ( Content item {i + 1} - + This is some additional content to make the item taller and ensure scrolling is needed. diff --git a/src/button/button.mdx b/src/button/button.mdx index f8da6a30e..3c2a1427e 100644 --- a/src/button/button.mdx +++ b/src/button/button.mdx @@ -4,7 +4,6 @@ import { Box } from '../box' import { Inline } from '../inline' import { Stack } from '../stack' import { Text } from '../text' -import { Heading } from '../heading' import { Button } from './button' import * as ButtonStories from './button.stories' diff --git a/src/button/button.stories.jsx b/src/button/button.stories.jsx index 9597ef6a9..e4e890ec0 100644 --- a/src/button/button.stories.jsx +++ b/src/button/button.stories.jsx @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react' import { withDarkTheme } from '../../.storybook/dark-theme' import { Box } from '../box' -import { Heading } from '../heading' import { Inline } from '../inline' import { Stack } from '../stack' import { Text } from '../text' @@ -53,7 +52,9 @@ function FullWidthTemplate({ label, ...otherProps }) { } return ( - Full-width buttons and label alignment + }> + Full-width buttons and label alignment + When buttons have `width` other than the default `auto` they can also customize how the label is aligned horizontally. @@ -87,7 +88,9 @@ function PlaygroundTemplate({ label, ...props }) { } return ( - Click on the buttons to see the loading state + }> + Click on the buttons to see the loading state + diff --git a/src/button/icon-button.mdx b/src/button/icon-button.mdx index ffbd23e74..f1698ee20 100644 --- a/src/button/icon-button.mdx +++ b/src/button/icon-button.mdx @@ -4,7 +4,6 @@ import { Box } from '../box' import { Inline } from '../inline' import { Stack } from '../stack' import { Text } from '../text' -import { Heading } from '../heading' import { IconButton } from '../button' import * as IconButtonStories from './icon-button.stories' diff --git a/src/button/icon-button.stories.jsx b/src/button/icon-button.stories.jsx index 0e06cecaf..b54c7065b 100644 --- a/src/button/icon-button.stories.jsx +++ b/src/button/icon-button.stories.jsx @@ -4,7 +4,6 @@ import { useEffect, useState } from 'react' import { withDarkTheme } from '../../.storybook/dark-theme' import { Box } from '../box' import { IconButton } from '../button' -import { Heading } from '../heading' import { Inline } from '../inline' import { Stack } from '../stack' import { Text } from '../text' @@ -35,7 +34,9 @@ function LoadingButton(props) { function PlaygroundTemplate({ label, ...props }) { return ( - Click on the buttons to see the loading state + }> + Click on the buttons to see the loading state + diff --git a/src/expansion-panel/expansion-panel.stories.tsx b/src/expansion-panel/expansion-panel.stories.tsx index a42df5c2e..d3df904e9 100644 --- a/src/expansion-panel/expansion-panel.stories.tsx +++ b/src/expansion-panel/expansion-panel.stories.tsx @@ -26,7 +26,7 @@ export const IconToggle = { alignItems="center" justifyContent="spaceBetween" > - + Fruit diff --git a/src/heading/heading.module.css b/src/heading/heading.module.css deleted file mode 100644 index 2ca4387a9..000000000 --- a/src/heading/heading.module.css +++ /dev/null @@ -1,114 +0,0 @@ -.heading { - color: var(--product-library-display-primary-idle-tint); - font-weight: var(--reactist-font-weight-strong); - font-family: var(--reactist-font-family); -} - -.weight-medium { - font-weight: var(--reactist-font-weight-medium); -} - -.weight-light { - font-weight: var(--reactist-font-weight-regular); -} - -/* tone */ - -.tone-secondary { - color: var(--product-library-display-secondary-idle-tint); -} -.tone-danger { - color: rgb(209, 69, 59); -} - -/* font size */ - -h1.heading { - font-size: var(--reactist-font-size-header); -} -h1.size-largest { - font-size: var(--reactist-font-size-header-xlarge); -} -h1.size-larger { - font-size: var(--reactist-font-size-header-large); -} -h1.size-smaller { - font-size: var(--reactist-font-size-subtitle); -} - -h2.heading { - font-size: var(--reactist-font-size-subtitle); -} -h2.size-largest { - font-size: var(--reactist-font-size-header-large); -} -h2.size-larger { - font-size: var(--reactist-font-size-header); -} -h2.size-smaller { - font-size: var(--reactist-font-size-body); -} - -h3.heading { - font-size: var(--reactist-font-size-body); -} -h3.size-largest { - font-size: var(--reactist-font-size-header); -} -h3.size-larger { - font-size: var(--reactist-font-size-subtitle); -} -h3.size-smaller { - font-size: var(--reactist-font-size-caption); -} - -h4.heading, -h5.heading, -h6.heading { - /* - * unlike at higher levels, this one is kept as the same size as h3's - * you can make it two levels larger visually, but making it smaller has no effect - */ - font-size: var(--reactist-font-size-body); -} - -h4.size-largest, -h5.size-largest, -h6.size-largest { - font-size: var(--reactist-font-size-header); -} - -h4.size-larger, -h5.size-larger, -h6.size-larger { - font-size: var(--reactist-font-size-subtitle); -} - -/* h4/h5/h6 can't be made smaller, maybe we reconsider this? */ - -/* truncated text */ - -.lineClampMultipleLines { - display: -webkit-box; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.lineClamp-1 { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -.lineClamp-2 { - -webkit-line-clamp: 2; -} -.lineClamp-3 { - -webkit-line-clamp: 3; -} -.lineClamp-4 { - -webkit-line-clamp: 4; -} -.lineClamp-5 { - -webkit-line-clamp: 5; -} diff --git a/src/heading/heading.stories.tsx b/src/heading/heading.stories.tsx deleted file mode 100644 index 3ecf79e4e..000000000 --- a/src/heading/heading.stories.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import * as React from 'react' - -import { Stack } from '../stack' -import { ResponsiveWidthRef, select, selectWithNone } from '../utils/storybook-helper' - -import { Heading } from './heading' - -export default { - title: '🔤 Typography/Heading', - component: Heading, - parameters: { - badges: ['accessible'], - figma: { - path: 'Global › Text Styles › SF *FOR WEB* › Header 1', - url: 'https://www.figma.com/design/xo9yAsH8PQUpi0eTJh9pmR/Product-Library---Global?node-id=2524-3589', - }, - }, -} - -export function HeadingStory() { - return ( -
- - - - Heading level 1, largest - - - Heading level 1, larger - - Heading level 1 - - Heading level 1, smaller - - - - - - Heading level 2, largest - - - Heading level 2, larger - - Heading level 2 - - Heading level 2, smaller - - - - - - Heading level 3, largest - - - Heading level 3, larger - - Heading level 3 - - Heading level 3, smaller - - - - - - Heading level 4 / 5 / 6, largest - - - Heading level 4 / 5 / 6, larger - - Heading level 4 / 5 / 6 - - -
- ) -} - -HeadingStory.parameters = { - chromatic: { disableSnapshot: false }, -} - -export function TruncatedHeadingStory() { - return ( -
- - This is a long title which we will use demonstrate truncating content. When this - overflows and begins to drop to a new line, its overflowing content will be replaced - by ellipses. - - - Now we have a subtitle which we will use to demostrate truncating to multiple lines. - Sometimes we need to provide more context yet still remain skimmable to users, and - subtitles are a good way to do this. As it’s much longer now we can allow a - second line to be displayed before truncating it at two lines. - -
- ) -} - -TruncatedHeadingStory.parameters = { - chromatic: { disableSnapshot: false }, -} - -export function ResponsiveHeadingStory(props: React.ComponentProps) { - return ( - <> - - - - ) -} - -ResponsiveHeadingStory.args = { - level: '1', - weight: 'regular', - tone: 'normal', - children: 'Lorem ipsum dolor, sit amet consectetur adipisicing elit', -} - -ResponsiveHeadingStory.argTypes = { - level: select(['1', '2', '3', '4', '5', '6']), - size: selectWithNone(['largest', 'larger', 'smaller']), - weight: select(['regular', 'light']), - lineClamp: selectWithNone([1, 2, 3, 4, 5]), - tone: select(['normal', 'secondary', 'danger']), - align: { control: false }, - children: { - control: { type: 'text' }, - }, -} - -export function HeadingPlaygroundStory(props: React.ComponentProps) { - return ( -
- -
- ) -} - -HeadingPlaygroundStory.args = { - level: '1', - weight: 'regular', - tone: 'normal', - children: 'Lorem ipsum dolor, sit amet consectetur adipisicing elit', -} - -HeadingPlaygroundStory.argTypes = { - level: select(['1', '2', '3', '4', '5', '6']), - size: selectWithNone(['largest', 'larger', 'smaller']), - weight: select(['regular', 'medium', 'light']), - lineClamp: selectWithNone([1, 2, 3, 4, 5]), - tone: select(['normal', 'secondary', 'danger']), - align: selectWithNone(['start', 'center', 'end', 'justify']), - children: { - control: { type: 'text' }, - }, -} diff --git a/src/heading/heading.test.tsx b/src/heading/heading.test.tsx deleted file mode 100644 index 2fd9f9b6f..000000000 --- a/src/heading/heading.test.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import * as React from 'react' - -import { render, screen } from '@testing-library/react' -import { axe } from 'jest-axe' - -import { Heading } from './heading' - -describe('Heading', () => { - it('does not acknowledge the className prop, but exceptionallySetClassName instead', () => { - render( - - Heading - , - ) - expect(screen.getByTestId('heading-element')).toHaveClass('right') - expect(screen.getByTestId('heading-element')).not.toHaveClass('wrong') - }) - - it('renders the expected heading tag name based on the level', () => { - const { rerender } = render( - - Heading - , - ) - expect(screen.getByTestId('heading-element').tagName).toBe('H1') - - for (const level of [2, 3, 4, 5, 6] as const) { - rerender( - - Heading - , - ) - expect(screen.getByTestId('heading-element').tagName).toBe(`H${level}`) - } - }) - - it('renders its children as its content', () => { - render( - - Hello world - , - ) - expect(screen.getByTestId('heading-element').innerHTML).toMatchInlineSnapshot( - `"Hello world"`, - ) - }) - - describe('size="…"', () => { - it('adds the appropriate class names', () => { - const { rerender } = render( - - Heading - , - ) - const textElement = screen.getByTestId('heading-element') - expect(textElement).not.toHaveClass('size-smaller') - expect(textElement).not.toHaveClass('size-larger') - expect(textElement).not.toHaveClass('size-largest') - - for (const size of ['smaller', 'larger', 'largest'] as const) { - rerender( - - Heading - , - ) - expect(textElement).toHaveClass(`size-${size}`) - } - }) - }) - - describe('weight="…"', () => { - it('adds the appropriate class names', () => { - const { rerender } = render( - - Heading - , - ) - const textElement = screen.getByTestId('heading-element') - expect(textElement).not.toHaveClass('weight-regular') - expect(textElement).not.toHaveClass('weight-light') - - rerender( - - Heading - , - ) - expect(textElement).toHaveClass('weight-medium') - - rerender( - - Heading - , - ) - expect(textElement).toHaveClass('weight-light') - }) - }) - - describe('tone="…"', () => { - it('adds the appropriate class names', () => { - const { rerender } = render( - - Heading - , - ) - const textElement = screen.getByTestId('heading-element') - expect(textElement).not.toHaveClass('tone-normal') - expect(textElement).not.toHaveClass('tone-secondary') - expect(textElement).not.toHaveClass('tone-danger') - - for (const tone of ['secondary', 'danger'] as const) { - rerender( - - Heading - , - ) - expect(textElement).toHaveClass(`tone-${tone}`) - } - }) - }) - - describe('align="…"', () => { - it('adds the appropriate class names', () => { - const { rerender } = render( - - Heading - , - ) - const textElement = screen.getByTestId('heading-element') - expect(textElement).not.toHaveClass('textAlign-start') - expect(textElement).not.toHaveClass('textAlign-center') - expect(textElement).not.toHaveClass('textAlign-end') - expect(textElement).not.toHaveClass('textAlign-justify') - - for (const align of ['start', 'center', 'end', 'justify'] as const) { - rerender( - - Heading - , - ) - expect(textElement).toHaveClass(`textAlign-${align}`) - } - }) - - it('supports responsive values', () => { - render( - - Heading - , - ) - const textElement = screen.getByTestId('heading-element') - expect(textElement).toHaveClass('textAlign-start') - expect(textElement).toHaveClass('tablet-textAlign-center') - expect(textElement).toHaveClass('desktop-textAlign-end') - }) - }) - - describe('lineClamp="…"', () => { - it('adds the expected class names', () => { - const { rerender } = render( - - Heading - , - ) - const textElement = screen.getByTestId('heading-element') - expect(textElement.className).not.toMatch(/lineClamp/) - expect(textElement).not.toHaveClass('paddingRight-xsmall') - - for (const lineClamp of [1, '1'] as const) { - rerender( - - Heading - , - ) - expect(textElement).toHaveClass(`lineClamp-${lineClamp}`) - expect(textElement).not.toHaveClass(`lineClampMultipleLines`) - expect(textElement).toHaveClass('paddingRight-xsmall') - } - - for (const lineClamp of [2, 3, 4, 5, '2', '3', '4', '5'] as const) { - rerender( - - Heading - , - ) - expect(textElement).toHaveClass(`lineClamp-${lineClamp}`) - expect(textElement).toHaveClass(`lineClampMultipleLines`) - expect(textElement).toHaveClass('paddingRight-xsmall') - } - }) - }) - - describe('a11y', () => { - it('renders with no a11y violations', async () => { - const { container } = render( - <> - Heading - Heading - Heading - Heading - Heading - Heading - , - ) - const results = await axe(container) - - expect(results).toHaveNoViolations() - }) - }) -}) diff --git a/src/heading/heading.tsx b/src/heading/heading.tsx deleted file mode 100644 index fb3501e93..000000000 --- a/src/heading/heading.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import * as React from 'react' - -import { Box } from '../box' -import { getClassNames } from '../utils/responsive-props' - -import styles from './heading.module.css' - -import type { BoxProps } from '../box' -import type { ObfuscatedClassName, Tone } from '../utils/common-types' - -type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6 | '1' | '2' | '3' | '4' | '5' | '6' -type HeadingElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' - -type HeadingProps = Omit, 'className' | 'children'> & { - children: React.ReactNode - /** - * The semantic level of the heading. - */ - level: HeadingLevel - - /** - * The weight of the heading. Used to de-emphasize the heading visually when using 'medium' or 'light'. - * - * @default 'regular' - */ - weight?: 'regular' | 'medium' | 'light' - - /** - * Shifts the default heading visual text size up or down, depending on the original size - * imposed by the `level`. The heading continues to be semantically at the given level. - * - * By default, no value is applied, and the default size from the level is applied. The values - * have the following effect: - * - * - 'smaller' shifts the default level size down in the font-size scale (it tends to make the - * level look visually as if it were of the immediately lower level). - * - 'larger' has the opposite effect than 'smaller' shifting the visual font size up in the - * scale. - * - 'largest' can be thought of as applying 'larger' twice. - * - * @see level - * @default undefined - */ - size?: 'smaller' | 'larger' | 'largest' - - /** - * The tone (semantic color) of the heading. - * - * @default 'normal' - */ - tone?: Tone - - /** - * Used to truncate the heading to a given number of lines. - * - * It will add an ellipsis (`…`) to the text at the end of the last line, only if the text was - * truncated. If the text fits without it being truncated, no ellipsis is added. - * - * By default, the text is not truncated at all, no matter how many lines it takes to render it. - * - * @default undefined - */ - lineClamp?: 1 | 2 | 3 | 4 | 5 | '1' | '2' | '3' | '4' | '5' - - /** - * How to align the heading text horizontally. - * - * @default 'start' - */ - align?: BoxProps['textAlign'] -} - -const Heading = React.forwardRef( - function Heading( - { - level, - weight = 'regular', - size, - tone = 'normal', - children, - lineClamp, - align, - exceptionallySetClassName, - ...props - }, - ref, - ) { - // In TypeScript v4.1, this would be properly recognized without needing the type assertion - // https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types - const headingElementName = `h${level}` as HeadingElement - const lineClampMultipleLines = - typeof lineClamp === 'string' ? parseInt(lineClamp, 10) > 1 : (lineClamp || 0) > 1 - - return ( - - {children} - - ) - }, -) - -Heading.displayName = 'Heading' - -export type { HeadingLevel, HeadingProps } -export { Heading } diff --git a/src/heading/index.ts b/src/heading/index.ts deleted file mode 100644 index 84e33f1fe..000000000 --- a/src/heading/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './heading' diff --git a/src/index.ts b/src/index.ts index 6df216374..3f0224112 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,6 @@ export * from './notice' export * from './toast' // text and typography -export * from './heading' export * from './prose' export * from './text' diff --git a/src/inline/inline.stories.tsx b/src/inline/inline.stories.tsx index 8c1d56c4e..a01063a20 100644 --- a/src/inline/inline.stories.tsx +++ b/src/inline/inline.stories.tsx @@ -1,7 +1,7 @@ import * as React from 'react' -import { Heading } from '../heading' import { Stack } from '../stack' +import { Text } from '../text' import { disableResponsiveProps, Placeholder, @@ -96,7 +96,9 @@ export function NestedStackStory({ space }: PartialProps) { const spaceStr = typeof space !== 'string' ? 'none' : space return ( - Parent stack with space=“{spaceStr}” + }> + Parent stack with space=“{spaceStr}” + {renderInlineContent()} {renderInlineContent()} diff --git a/src/menu/menu.stories.jsx b/src/menu/menu.stories.jsx index 8a582829f..b6f43f438 100644 --- a/src/menu/menu.stories.jsx +++ b/src/menu/menu.stories.jsx @@ -47,7 +47,7 @@ function StructuredMenuItem({ icon, label, shortcut }) { ) : null} - {label} + {label} {shortcut ? ( @@ -224,7 +224,7 @@ export const LinkMenuItemStory = { } > - Link without an icon + Link without an icon } > - Disabled link without an icon + Disabled link without an icon diff --git a/src/modal/modal.stories.tsx b/src/modal/modal.stories.tsx index 45fab9a5e..20910ba42 100644 --- a/src/modal/modal.stories.tsx +++ b/src/modal/modal.stories.tsx @@ -8,7 +8,6 @@ import { IconButton } from '../button' import { Column, Columns } from '../columns' import ThreeDotsIcon from '../components/icons/ThreeDotsIcon.svg' import { Divider } from '../divider' -import { Heading } from '../heading' import { Inline } from '../inline' import { Menu, MenuButton, MenuItem, MenuList } from '../menu' import { Stack } from '../stack' @@ -95,10 +94,18 @@ export function ModalWithStandardActionsFooter() { } > - Modal with standard actions footer + }> + Modal with standard actions footer + - Customize modal
} /> + }> + Customize modal +
+ } + /> @@ -130,17 +137,23 @@ export function ModalWithHeaderBodyAndCustomFooter() {
- Modal with header, body and custom footer + }> + Modal with header, body and custom footer + - Customize modal} /> + }> + Customize modal +
+ } + /> - - Do whatever you want down here - + Do whatever you want down here @@ -176,7 +189,9 @@ export function ModalWithSidebar() { - Settings + }> + Settings +
  • @@ -194,7 +209,9 @@ export function ModalWithSidebar() { - Customize modal + }> + Customize modal + @@ -242,7 +259,9 @@ export function ModalWithScrollableTabPanels() { flexDirection="column" > - Task content goest here + }> + Task content goest here + @@ -304,7 +323,7 @@ export function MinimalisticConfirmationModal() { - Are you sure you want to leave? + Are you sure you want to leave? - + By default the `autoFocus` prop is `true`, which shifts the focus onto the first focusable element in the modal. You can further refine this by using the `data-autofocus` attribute if you wish to focus on elements other than the first @@ -396,7 +415,9 @@ export function ModalAutofocus() { - Update your info + }> + Update your info + @@ -435,18 +456,20 @@ export function StackingModals() { - + Modals may be stacked on top of one another, with each of them having their independent states, e.g. `width` and `height`. - Parent modal + }> + Parent modal + - + Modals may be stacked on top of one another, with each of them having their independent states, e.g. `width` and `height`. @@ -460,7 +483,9 @@ export function StackingModals() { - Nested modal + }> + Nested modal + diff --git a/src/password-field/password-field.stories.jsx b/src/password-field/password-field.stories.jsx index 596f22bcb..5b6a58976 100644 --- a/src/password-field/password-field.stories.jsx +++ b/src/password-field/password-field.stories.jsx @@ -190,8 +190,8 @@ export const WithoutLabel = { render: () => ( - Custom label is up here - + Custom label is up here + (click me to focus the textarea) @@ -202,8 +202,8 @@ export const WithoutLabel = { placeholder="Password field without a built-in label" /> - Custom description is down here - + Custom description is down here + (inspect the input element accessibility properties if you are curious) diff --git a/src/select-field/select-field.stories.jsx b/src/select-field/select-field.stories.jsx index 44ac73ad2..86beb5955 100644 --- a/src/select-field/select-field.stories.jsx +++ b/src/select-field/select-field.stories.jsx @@ -184,8 +184,8 @@ export const WithoutLabel = { render: () => ( - Custom label is up here - + Custom label is up here + (click me to focus the select element) @@ -195,8 +195,8 @@ export const WithoutLabel = { - Custom description is down here - + Custom description is down here + (inspect the select element accessibility properties if you are curious) diff --git a/src/stack/stack.stories.tsx b/src/stack/stack.stories.tsx index 6131dd4a9..16f6f7804 100644 --- a/src/stack/stack.stories.tsx +++ b/src/stack/stack.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { Heading } from '../heading' +import { Text } from '../text' import { disableResponsiveProps, Placeholder, @@ -99,15 +99,21 @@ export function NestedStacksStory(args: PartialProps) { const spaceStr = typeof args.space !== 'string' ? 'none' : args.space return ( - Parent stack with space=“{spaceStr}” + }> + Parent stack with space=“{spaceStr}” + - Nested stack with space=“xsmall” + }> + Nested stack with space=“xsmall” + - Nested stack with space=“xsmall” + }> + Nested stack with space=“xsmall” + diff --git a/src/text-area/text-area.stories.jsx b/src/text-area/text-area.stories.jsx index c97bd0700..e30a6f9b2 100644 --- a/src/text-area/text-area.stories.jsx +++ b/src/text-area/text-area.stories.jsx @@ -54,7 +54,7 @@ function AutoExpandStory(props) { } }} /> - + If you press Enter, the input will be cleared. This allows you to test that auto-expand works when the input is cleared programmatically, shrinking the textarea to the new expected height. @@ -265,8 +265,8 @@ export const WithoutLabel = { render: () => ( - Custom label is up here - + Custom label is up here + (click me to focus the textarea) @@ -277,8 +277,8 @@ export const WithoutLabel = { rows={8} /> - Custom description is down here - + Custom description is down here + (inspect the textarea accessibility properties if you are curious) diff --git a/src/text-field/text-field.stories.jsx b/src/text-field/text-field.stories.jsx index a709e6ff6..4167953a7 100644 --- a/src/text-field/text-field.stories.jsx +++ b/src/text-field/text-field.stories.jsx @@ -366,8 +366,8 @@ export const WithoutLabel = { render: () => ( - Custom label is up here - + Custom label is up here + (click me to focus the input element) @@ -378,8 +378,8 @@ export const WithoutLabel = { placeholder="Text field without a built-in label" /> - Custom description is down here - + Custom description is down here + (inspect the input element accessibility properties if you are curious) diff --git a/src/text/index.ts b/src/text/index.ts index 1693b5809..62f1523af 100644 --- a/src/text/index.ts +++ b/src/text/index.ts @@ -1 +1,2 @@ -export * from './text' +export type { TextProps, TextVariant } from './text' +export { Text } from './text' diff --git a/src/text/text.mdx b/src/text/text.mdx new file mode 100644 index 000000000..f39fcfa77 --- /dev/null +++ b/src/text/text.mdx @@ -0,0 +1,52 @@ +import { Meta, ArgTypes, Description } from '@storybook/addon-docs/blocks' +import { Text } from './text' +import * as TextStories from './text.stories' + + + +# Text + + + +## Usage + +`Text` covers the entire typography scale with named variants. Choose one variant for the complete +Figma style; do not combine independent size and weight values. It uses `body-3` by default. + +```tsx +Default body copy +Emphasized body copy +Underlined heading +Metadata +``` + +Every variant supports `underline` and `strikethrough`. Decoration can be combined with uppercase on +`footnote-1`. + +Header variants render the matching heading element (`header-1` renders `h1`, and so on); all +other variants render `div`. + +```tsx +Page title // renders an h1 +42 completed tasks // renders a div +``` + +Use `render` when the copy needs another HTML element. The rendered element owns its semantics, so +pick heading levels from the document outline, not from the variant number. + +```tsx +}> + Prominent section title + +}> + Edit title + +}> + Project name + + +``` + +## Props + + diff --git a/src/text/text.module.css b/src/text/text.module.css index 69d340f15..953fffb9a 100644 --- a/src/text/text.module.css +++ b/src/text/text.module.css @@ -1,39 +1,23 @@ .text { - font-family: var(--reactist-font-family); - font-size: var(--reactist-font-size-body); - font-weight: var(--reactist-font-weight-regular); color: var(--product-library-display-primary-idle-tint); } -.size-caption { - font-size: var(--reactist-font-size-caption); -} -.size-copy { - font-size: var(--reactist-font-size-copy); -} -.size-subtitle { - font-size: var(--reactist-font-size-subtitle); -} - -.weight-semibold { - font-weight: var(--reactist-font-weight-medium); -} -.weight-bold { - font-weight: var(--reactist-font-weight-strong); +.font-family-default { + font-family: var(--reactist-font-family); } .tone-secondary { color: var(--product-library-display-secondary-idle-tint); } + .tone-danger { color: var(--product-library-actionable-destructive-idle-tint); } + .tone-positive { color: var(--product-library-info-positive-primary-idle-tint); } -/* truncated text */ - .lineClampMultipleLines { display: -webkit-box; -webkit-box-orient: vertical; @@ -41,20 +25,181 @@ } .lineClamp-1 { + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - overflow: hidden; } .lineClamp-2 { -webkit-line-clamp: 2; } + .lineClamp-3 { -webkit-line-clamp: 3; } + .lineClamp-4 { -webkit-line-clamp: 4; } + .lineClamp-5 { -webkit-line-clamp: 5; } + +.display { + font-weight: 500; + line-height: normal; +} + +.variant-display-1 { + font-size: 96px; + letter-spacing: 0; +} + +.variant-display-2 { + font-size: 88px; + letter-spacing: 0; +} + +.variant-display-3 { + font-size: 72px; + letter-spacing: 0.14px; +} + +.variant-display-4 { + font-size: 56px; + letter-spacing: 0.3px; +} + +.variant-display-5 { + font-size: 42px; + letter-spacing: 0.37px; +} + +.variant-header-1 { + font-size: 32px; + font-weight: 700; + letter-spacing: 0.41px; + line-height: normal; +} + +.variant-header-2 { + font-size: 26px; + font-weight: 700; + letter-spacing: 0.22px; + line-height: normal; +} + +.variant-header-3 { + font-size: 20px; + font-weight: 700; + letter-spacing: 0; + line-height: normal; +} + +.variant-header-4 { + font-size: 18px; + font-weight: 600; + letter-spacing: 0; + line-height: normal; +} + +.variant-subheader-1 { + font-size: 16px; + font-weight: 600; + letter-spacing: 0; + line-height: 23px; +} + +.variant-subheader-2 { + font-size: 16px; + font-weight: 400; + letter-spacing: 0; + line-height: 23px; +} + +.variant-body-1 { + font-size: 14px; + font-weight: 700; + letter-spacing: -0.15px; + line-height: 21px; +} + +.variant-body-2 { + font-size: 14px; + font-weight: 600; + letter-spacing: -0.15px; + line-height: 21px; +} + +.variant-body-3 { + font-size: 14px; + font-weight: 400; + letter-spacing: -0.15px; + line-height: 21px; +} + +.variant-callout-1 { + font-size: 13px; + font-weight: 600; + letter-spacing: -0.15px; + line-height: 20px; +} + +.variant-callout-2 { + font-size: 13px; + font-weight: 400; + letter-spacing: -0.15px; + line-height: 20px; +} + +.variant-caption-1 { + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + line-height: 20px; +} + +.variant-caption-2 { + font-size: 12px; + font-weight: 600; + letter-spacing: -0.15px; + line-height: 15px; +} + +.variant-caption-3 { + font-size: 12px; + font-weight: 400; + letter-spacing: 0; + line-height: 20px; +} + +.variant-footnote-1 { + font-size: 10px; + font-weight: 700; + letter-spacing: 1px; + line-height: 13px; +} + +.variant-footnote-2 { + font-size: 10px; + font-weight: 500; + letter-spacing: 1px; + line-height: 13px; +} + +.decoration-strikethrough { + text-decoration-line: line-through; + text-decoration-skip-ink: none; + text-underline-position: from-font; +} + +.decoration-underline { + text-decoration-line: underline; + text-decoration-skip-ink: none; + text-underline-position: from-font; +} + +.case-uppercase { + text-transform: uppercase; +} diff --git a/src/text/text.stories.test.tsx b/src/text/text.stories.test.tsx new file mode 100644 index 000000000..aaa61bc7a --- /dev/null +++ b/src/text/text.stories.test.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' + +import { render, screen } from '@testing-library/react' + +import { TextStory } from './text.stories' + +describe('TextStory', () => { + it('uses a 16 px gap between variants', () => { + render() + + expect(screen.getByText('Display 1').parentElement).toHaveClass('gap-large') + }) + + it('shows all variant names in Title Case', () => { + render() + + for (const variantName of [ + 'Display 1', + 'Display 2', + 'Display 3', + 'Display 4', + 'Display 5', + 'Header 1', + 'Header 2', + 'Header 3', + 'Header 4', + 'Subheader 1', + 'Subheader 2', + 'Body 1', + 'Body 2', + 'Body 3', + 'Callout 1', + 'Callout 2', + 'Caption 1', + 'Caption 2', + 'Caption 3', + 'Footnote 1', + 'Footnote 2', + ]) { + expect(screen.getByText(variantName)).toBeInTheDocument() + } + }) +}) diff --git a/src/text/text.stories.tsx b/src/text/text.stories.tsx index 8800540af..242c5a03b 100644 --- a/src/text/text.stories.tsx +++ b/src/text/text.stories.tsx @@ -3,7 +3,16 @@ import * as React from 'react' import { Stack } from '../stack' import { ResponsiveWidthRef, select, selectWithNone } from '../utils/storybook-helper' -import { Text } from './text' +import { bodyVariants, displayVariants, headerVariants, Text } from './text' + +const allVariants = [...displayVariants, ...headerVariants, ...bodyVariants] as const + +function formatVariantName(variant: (typeof allVariants)[number]) { + return variant + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' ') +} export default { title: '🔤 Typography/Text', @@ -11,8 +20,8 @@ export default { parameters: { badges: ['accessible'], figma: { - path: 'Global › Text Styles › SF *FOR WEB* › Body 1', - url: 'https://www.figma.com/design/xo9yAsH8PQUpi0eTJh9pmR/Product-Library---Global?node-id=2524-3594', + path: 'Global › Text Styles', + url: 'https://www.figma.com/design/xo9yAsH8PQUpi0eTJh9pmR/Product-Library---Global?node-id=2527-3732&t=zfZi3AJ6FfmLeBUT-4', }, }, } @@ -20,82 +29,12 @@ export default { export function TextStory() { return (
    - - - Subtitle Regular - - - Subtitle Secondary - - - Subtitle Danger - - - Subtitle Positive - - - Subtitle Semibold - - - Subtitle Bold - - - - Body Regular - - - Body Secondary - - - Body Danger - - - Body Positive - - - Body Semibold - - - Body Bold - - - - Copy Regular - - - Copy Secondary - - - Copy Danger - - - Copy Positive - - - Copy Semibold - - - Copy Bold - - - - Caption Regular - - - Caption Secondary - - - Caption Danger - - - Caption Positive - - - Caption Semibold - - - Caption Bold - + + {allVariants.map((variant) => ( + + {formatVariantName(variant)} + + ))}
    ) @@ -134,6 +73,12 @@ export function TruncatedTextStory() { temporibus, omnis laborum quidem autem totam. Iure, numquam. Totam facilis dolorum, consequatur, eligendi est dolores modi dolore maiores ipsum magnam a.
    + + + This is a long title which we will use demonstrate truncating content. When this + overflows and begins to drop to a new line, its overflowing content will be + replaced by ellipses. +
    ) @@ -153,15 +98,15 @@ export function ResponsiveTextStory(props: React.ComponentProps) { } ResponsiveTextStory.args = { - size: 'body', - weight: 'regular', + variant: 'body-3', tone: 'normal', children: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit', } ResponsiveTextStory.argTypes = { - size: select(['caption', 'copy', 'body', 'subtitle']), - weight: select(['regular', 'semibold', 'bold']), + variant: select(allVariants), + decoration: selectWithNone(['strikethrough', 'underline']), + case: selectWithNone(['uppercase']), lineClamp: selectWithNone([1, 2, 3, 4, 5]), tone: select(['normal', 'secondary', 'danger']), align: { control: false }, @@ -179,15 +124,15 @@ export function TextPlaygroundStory(props: React.ComponentProps) { } TextPlaygroundStory.args = { - size: 'body', - weight: 'regular', + variant: 'body-3', tone: 'normal', children: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit', } TextPlaygroundStory.argTypes = { - size: select(['caption', 'copy', 'body', 'subtitle']), - weight: select(['regular', 'semibold', 'bold']), + variant: select(allVariants), + decoration: selectWithNone(['strikethrough', 'underline']), + case: selectWithNone(['uppercase']), lineClamp: selectWithNone([1, 2, 3, 4, 5]), tone: select(['normal', 'secondary', 'danger']), align: selectWithNone(['start', 'center', 'end', 'justify']), diff --git a/src/text/text.test.tsx b/src/text/text.test.tsx index 0e058516e..1b8c2adb9 100644 --- a/src/text/text.test.tsx +++ b/src/text/text.test.tsx @@ -1,8 +1,14 @@ import * as React from 'react' import { render, screen } from '@testing-library/react' +import { axe } from 'jest-axe' -import { Text } from './text' +import { bodyVariants, displayVariants, headerVariants, Text } from './text' + +const decorations = ['strikethrough', 'underline'] as const +const decoratedTextProps = [...displayVariants, ...headerVariants, ...bodyVariants].flatMap( + (variant) => decorations.map((decoration) => ({ variant, decoration })), +) describe('Text', () => { it('does not acknowledge the className prop, but exceptionallySetClassName instead', () => { @@ -20,71 +26,88 @@ describe('Text', () => { expect(screen.getByTestId('text-element')).not.toHaveClass('wrong') }) - it('can be rendered as any HTML element', () => { + it('defaults to body-3 rendered as a div', () => { + render(Text) + const element = screen.getByTestId('text-element') + expect(element.tagName).toBe('DIV') + expect(element).toHaveClass('text', 'variant-body-3') + }) + + it.each([...displayVariants, ...headerVariants, ...bodyVariants])( + 'applies the %s variant', + (variant) => { + render( + + Text + , + ) + expect(screen.getByTestId('text-element')).toHaveClass('variant-' + variant) + }, + ) + + it.each([ + ['header-1', 'H1'], + ['header-2', 'H2'], + ['header-3', 'H3'], + ['header-4', 'H4'], + ] as const)('renders %s as %s', (variant, tagName) => { render( - + Text , ) - expect(screen.getByTestId('text-element').tagName).toBe('NAV') + expect(screen.getByTestId('text-element').tagName).toBe(tagName) }) - it('renders its children as its content', () => { + it('renders display text as a div with the default font', () => { render( - - Hello world + + Text , ) - expect(screen.getByTestId('text-element').innerHTML).toMatchInlineSnapshot( - `"Hello world"`, - ) + const element = screen.getByTestId('text-element') + expect(element.tagName).toBe('DIV') + expect(element).toHaveClass('display') + expect(element).toHaveClass('font-family-default') }) - describe('size="…"', () => { - it('adds the appropriate class names', () => { - const { rerender } = render( - - Text - , - ) - const textElement = screen.getByTestId('text-element') - expect(textElement).not.toHaveClass('size-body') - expect(textElement).not.toHaveClass('size-caption') - expect(textElement).not.toHaveClass('size-copy') - expect(textElement).not.toHaveClass('size-subtitle') + it('renders custom elements through Ariakit Role', () => { + render( + }> + Name + , + ) + const element = screen.getByTestId('text-element') + expect(element.tagName).toBe('LABEL') + expect(element).toHaveAttribute('for', 'name') + }) - for (const size of ['caption', 'copy', 'subtitle'] as const) { - rerender( - - Text - , - ) - expect(textElement).toHaveClass(`size-${size}`) - } - }) + it('lets render override the header variant default element', () => { + render( + }> + Text + , + ) + const element = screen.getByTestId('text-element') + expect(element.tagName).toBe('H2') + expect(element).toHaveClass('variant-header-1') }) - describe('weight="…"', () => { - it('adds the appropriate class names', () => { - const { rerender } = render( - - Text - , - ) - const textElement = screen.getByTestId('text-element') - expect(textElement).not.toHaveClass('weight-regular') - expect(textElement).not.toHaveClass('weight-semibold') - expect(textElement).not.toHaveClass('weight-bold') + it('forwards its ref', () => { + const ref = React.createRef() + render(Text) + expect(ref.current?.tagName).toBe('DIV') + }) - for (const weight of ['semibold', 'bold'] as const) { - rerender( - - Text - , - ) - expect(textElement).toHaveClass(`weight-${weight}`) - } - }) + it('renders its children as its content', () => { + render( + + Hello world + , + ) + expect(screen.getByTestId('text-element').innerHTML).toMatchInlineSnapshot( + `"Hello world"`, + ) }) describe('tone="…"', () => { @@ -175,4 +198,62 @@ describe('Text', () => { } }) }) + + it.each(decoratedTextProps)('supports $variant with $decoration', (textProps) => { + render( + + Text + , + ) + expect(screen.getByTestId('text-element')).toHaveClass('decoration-' + textProps.decoration) + }) + + it.each(decorations)('supports the default variant with %s', (decoration) => { + render( + + Text + , + ) + expect(screen.getByTestId('text-element')).toHaveClass('decoration-' + decoration) + }) + + it('supports uppercase and decoration together for footnote-1', () => { + render( + + Text + , + ) + expect(screen.getByTestId('text-element')).toHaveClass( + 'case-uppercase', + 'decoration-strikethrough', + ) + }) + + it('rejects uppercase for unsupported variants at type level', () => { + const invalidCase = ( + // @ts-expect-error display variants do not support case + + Invalid + + ) + expect(invalidCase).toBeDefined() + }) + + it('has no accessibility violations', async () => { + const { container } = render( + <> + Heading + }> + Name + + + , + ) + expect(await axe(container)).toHaveNoViolations() + }) }) diff --git a/src/text/text.tsx b/src/text/text.tsx index b0bf36427..c966096da 100644 --- a/src/text/text.tsx +++ b/src/text/text.tsx @@ -1,98 +1,135 @@ import * as React from 'react' -import { Box } from '../box' -import { polymorphicComponent } from '../utils/polymorphism' -import { getClassNames } from '../utils/responsive-props' +import { Role } from '@ariakit/react' +import classNames from 'classnames' + +import { getBoxClassNames } from '../box' import styles from './text.module.css' +import type { RoleProps } from '@ariakit/react' import type { BoxProps } from '../box' -import type { Tone } from '../utils/common-types' - -type TextProps = { - children: React.ReactNode - /** - * The size of the text. - * - * The supported values, from smaller size to larger size, are: - * 'caption', 'copy', 'body', and 'subtitle' - * - * @default 'body' - */ - size?: 'caption' | 'copy' | 'body' | 'subtitle' - /** - * The weight of the text font. - * - * @default 'regular' - */ - weight?: 'regular' | 'semibold' | 'bold' - /** - * The tone (semantic color) of the text. - * - * @default 'normal' - */ +import type { ObfuscatedClassName, Tone } from '../utils/common-types' + +const displayVariants = ['display-1', 'display-2', 'display-3', 'display-4', 'display-5'] as const + +const headerVariants = ['header-1', 'header-2', 'header-3', 'header-4'] as const + +const bodyVariants = [ + 'subheader-1', + 'subheader-2', + 'body-1', + 'body-2', + 'body-3', + 'callout-1', + 'callout-2', + 'caption-1', + 'caption-2', + 'caption-3', + 'footnote-1', + 'footnote-2', +] as const + +type HeaderTextVariant = (typeof headerVariants)[number] +type BodyTextVariant = (typeof bodyVariants)[number] + +type TextVariant = (typeof displayVariants)[number] | HeaderTextVariant | BodyTextVariant +type TextLineClamp = 1 | 2 | 3 | 4 | 5 | '1' | '2' | '3' | '4' | '5' + +type TextStyleProps = ObfuscatedClassName & { + /** The semantic color of the text. */ tone?: Tone - /** - * Used to truncate the text to a given number of lines. - * - * It will add an ellipsis (`…`) to the text at the end of the last line, only if the text was - * truncated. If the text fits without it being truncated, no ellipsis is added. - * - * By default, the text is not truncated at all, no matter how many lines it takes to render it. - * - * @default undefined - */ - lineClamp?: 1 | 2 | 3 | 4 | 5 | '1' | '2' | '3' | '4' | '5' - /** - * How to align the text horizontally. - * - * @default 'start' - */ + /** Horizontal text alignment, including responsive values. */ align?: BoxProps['textAlign'] + /** Truncates text after the given number of lines. */ + lineClamp?: TextLineClamp + /** Adds a line under or through the text. */ + decoration?: 'strikethrough' | 'underline' +} + +type DefaultCaseTextProps = { + /** Visual text style; defaults to body-3. */ + variant?: TextVariant + /** Uppercase text is only available with footnote-1. */ + case?: never } -const Text = polymorphicComponent<'div', TextProps>(function Text( +type UppercaseTextProps = { + /** Visual footnote style supporting uppercase. */ + variant: 'footnote-1' + /** Converts the text to uppercase. */ + case: 'uppercase' +} + +/** Renders interface copy with a named typography variant, from display text to footnotes. */ +type TextProps = Omit, 'children' | 'className'> & + TextStyleProps & { + children: React.ReactNode + /** + * Custom element rendered with the variant's typography. Defaults to the matching heading + * element for header variants, and a div otherwise. + */ + render?: RoleProps['render'] + } & (DefaultCaseTextProps | UppercaseTextProps) + +function isHeaderVariant(variant: TextVariant): variant is HeaderTextVariant { + return variant.startsWith('header-') +} + +const headerElements: Record = { + 'header-1':

    , + 'header-2':

    , + 'header-3':

    , + 'header-4':

    , +} + +/** Renders interface copy with a named typography variant, from display text to footnotes. */ +const Text = React.forwardRef(function Text( { - as, - size = 'body', - weight = 'regular', + variant = 'body-3', + decoration, + case: textCase, tone = 'normal', align, - children, lineClamp, exceptionallySetClassName, + render, + children, ...props }, ref, ) { - const lineClampMultipleLines = - typeof lineClamp === 'string' ? Number(lineClamp) > 1 : (lineClamp ?? 1) > 1 + const display = variant.startsWith('display-') return ( - 1 ? styles.lineClampMultipleLines : null, + lineClamp ? styles['lineClamp-' + lineClamp] : null, + )} + // the rendered element varies by variant and render, so the ref is typed broadly + ref={ref as React.ForwardedRef} > {children} - + ) }) Text.displayName = 'Text' -export type { TextProps } -export { Text } +export type { TextProps, TextVariant } +export { bodyVariants, displayVariants, headerVariants, Text } diff --git a/src/toast/static-toast.tsx b/src/toast/static-toast.tsx index 39f8b6809..02c081a8b 100644 --- a/src/toast/static-toast.tsx +++ b/src/toast/static-toast.tsx @@ -96,7 +96,7 @@ const StaticToast = React.forwardRef(function {description ? ( - {message} + {message} {description} ) : ( diff --git a/src/toast/toast.stories.tsx b/src/toast/toast.stories.tsx index fec548ee9..c4aaaee5d 100644 --- a/src/toast/toast.stories.tsx +++ b/src/toast/toast.stories.tsx @@ -5,7 +5,6 @@ import { action as storybookAction } from 'storybook/actions' import { Box } from '../box' import { Button, IconButton } from '../button' import { CheckboxField } from '../checkbox-field' -import { Heading } from '../heading' import { AlertIcon } from '../icons/alert-icon' import { PasswordVisibleIcon } from '../icons/password-visible-icon' import { Inline } from '../inline' @@ -65,9 +64,9 @@ export function NotificationToastsStory() { return ( - + }> Toasts - + Use the useToast hook to fire notification-like toasts. @@ -172,9 +171,9 @@ export function StaticToastStory() { return ( - + }> Statically-rendered toasts - + Use the StaticToast component to render a toast in custom positions. @@ -216,7 +215,9 @@ export function StaticToastStory() { - Message only + }> + Message only + - Message and description + }> + Message and description + - Very long content + }> + Very long content + - - Upgrade to Pro - + Upgrade to Pro
    • Add reminders to tasks
    • Unlimited projects
    • @@ -253,7 +251,7 @@ export function TooltipImperativeControl() { - + Try hovering the button, then clicking "Force hide" before the 3-second timeout expires. diff --git a/src/utils/storybook-helper.tsx b/src/utils/storybook-helper.tsx index 60035af27..c612d5ab6 100644 --- a/src/utils/storybook-helper.tsx +++ b/src/utils/storybook-helper.tsx @@ -3,8 +3,8 @@ import '../styles/design-tokens.css' import * as React from 'react' import { Box } from '../box' -import { Heading } from '../heading' import { Stack } from '../stack' +import { Text } from '../text' import type { JSX } from 'react' import type { BoxProps } from '../box' @@ -93,7 +93,11 @@ function Wrapper({ }) { return ( - {title ? {title} : null} + {title ? ( + }> + {title} + + ) : null} {children}