diff --git a/.changeset/card-compact-layout.md b/.changeset/card-compact-layout.md new file mode 100644 index 00000000000..c676741df9a --- /dev/null +++ b/.changeset/card-compact-layout.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Card: Add `layout="compact"` prop for a compact card layout with tighter spacing, no icon background, and smaller title diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-colorblind-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-colorblind-linux.png new file mode 100644 index 00000000000..3018dd0ec27 Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-dimmed-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-dimmed-linux.png new file mode 100644 index 00000000000..63a36f1a7e2 Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-high-contrast-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-high-contrast-linux.png new file mode 100644 index 00000000000..f10b130dd8b Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-linux.png new file mode 100644 index 00000000000..3018dd0ec27 Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-linux.png differ diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-tritanopia-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-tritanopia-linux.png new file mode 100644 index 00000000000..3018dd0ec27 Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-colorblind-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-colorblind-linux.png new file mode 100644 index 00000000000..daba2addb44 Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-high-contrast-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-high-contrast-linux.png new file mode 100644 index 00000000000..f51a13ecef8 Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-linux.png new file mode 100644 index 00000000000..daba2addb44 Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-linux.png differ diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-tritanopia-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-tritanopia-linux.png new file mode 100644 index 00000000000..daba2addb44 Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-tritanopia-linux.png differ diff --git a/e2e/components/Card.test.ts b/e2e/components/Card.test.ts index 8c4e77acfde..1b583566aac 100644 --- a/e2e/components/Card.test.ts +++ b/e2e/components/Card.test.ts @@ -56,4 +56,22 @@ test.describe('Card', () => { }) } }) + + test.describe('Compact', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'experimental-components-card-features--compact', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`Card.Compact.${theme}.png`) + }) + }) + } + }) }) diff --git a/packages/react/src/Card/Card.features.stories.tsx b/packages/react/src/Card/Card.features.stories.tsx index a8ef6d5a1e3..dc6b2f286d2 100644 --- a/packages/react/src/Card/Card.features.stories.tsx +++ b/packages/react/src/Card/Card.features.stories.tsx @@ -28,6 +28,22 @@ export const WithImage = () => { ) } +export const Compact = () => { + return ( + + + primer/react + + The compact layout uses tighter spacing, an icon without a background container, and a smaller title. + + + + 1.2k stars + + + ) +} + export const WithMetadata = () => { return ( diff --git a/packages/react/src/Card/Card.module.css b/packages/react/src/Card/Card.module.css index 53360e1e55d..e0e463e81d9 100644 --- a/packages/react/src/Card/Card.module.css +++ b/packages/react/src/Card/Card.module.css @@ -29,12 +29,28 @@ &[data-padding='none'] { padding: 0; } + + &[data-layout='compact'] { + display: flex; + align-items: flex-start; + gap: var(--stack-gap-condensed); + } + + &[data-layout='compact'][data-padding='normal'] { + /* stylelint-disable-next-line primer/spacing */ + padding: var(--stack-padding-normal); + } } .CardHeader { display: block; width: 100%; height: auto; + + .Card:where([data-layout='compact']) & { + flex: 0 0 auto; + width: auto; + } } .CardHeaderEdgeToEdge { @@ -55,17 +71,25 @@ .CardIcon { display: flex; align-items: center; - justify-content: center; - width: var(--base-size-32); - height: var(--base-size-32); - border-radius: var(--borderRadius-medium); - background-color: var(--bgColor-muted); + justify-content: flex-start; color: var(--fgColor-muted); + + .Card:where([data-layout='default']) & { + justify-content: center; + width: var(--base-size-32); + height: var(--base-size-32); + border-radius: var(--borderRadius-medium); + background-color: var(--bgColor-muted); + } } .CardBody { display: grid; gap: var(--stack-gap-normal); + + .Card:where([data-layout='compact']) & { + flex: 1 1 auto; + } } .CardContent { @@ -77,6 +101,12 @@ font: var(--text-title-shorthand-small); color: var(--fgColor-default); margin: 0; + + .Card:where([data-layout='compact']) & { + position: relative; + top: calc(-1 * var(--base-size-4)); + font-size: var(--text-body-size-medium); + } } .CardDescription { diff --git a/packages/react/src/Card/Card.stories.tsx b/packages/react/src/Card/Card.stories.tsx index 3f0f9d30e54..eef867be2a4 100644 --- a/packages/react/src/Card/Card.stories.tsx +++ b/packages/react/src/Card/Card.stories.tsx @@ -33,12 +33,13 @@ export const Default = () => { type PlaygroundArgs = { showIcon: boolean showMetadata: boolean + layout: 'default' | 'compact' padding: 'none' | 'condensed' | 'normal' borderRadius: 'medium' | 'large' } -export const Playground: StoryFn = ({showIcon, showMetadata, padding, borderRadius}) => ( - +export const Playground: StoryFn = ({showIcon, showMetadata, layout, padding, borderRadius}) => ( + {showIcon && } Playground Card Experiment with the Card component and its subcomponents. @@ -49,6 +50,7 @@ export const Playground: StoryFn = ({showIcon, showMetadata, pad Playground.args = { showIcon: true, showMetadata: true, + layout: 'default', padding: 'normal', borderRadius: 'large', } @@ -62,6 +64,11 @@ Playground.argTypes = { control: {type: 'boolean'}, description: 'Show or hide the Card.Metadata subcomponent', }, + layout: { + control: {type: 'radio'}, + options: ['default', 'compact'], + description: 'Controls the layout of the Card', + }, padding: { control: {type: 'radio'}, options: ['none', 'condensed', 'normal'], diff --git a/packages/react/src/Card/Card.test.tsx b/packages/react/src/Card/Card.test.tsx index c20efab9ca6..aeab542fe61 100644 --- a/packages/react/src/Card/Card.test.tsx +++ b/packages/react/src/Card/Card.test.tsx @@ -277,4 +277,32 @@ describe('Card', () => { ) expect(container.firstChild).not.toHaveAttribute('as') }) + + it('should set data-layout to default by default', () => { + const {container} = render( + + Default Variant + , + ) + expect(container.firstChild).toHaveAttribute('data-layout', 'default') + }) + + it('should set data-layout to compact when layout="compact"', () => { + const {container} = render( + + + Compact Card + , + ) + expect(container.firstChild).toHaveAttribute('data-layout', 'compact') + }) + + it('should set data-layout on custom content cards', () => { + const {container} = render( + +

Custom

+
, + ) + expect(container.firstChild).toHaveAttribute('data-layout', 'compact') + }) }) diff --git a/packages/react/src/Card/Card.tsx b/packages/react/src/Card/Card.tsx index dec74a45f0b..46f79f41727 100644 --- a/packages/react/src/Card/Card.tsx +++ b/packages/react/src/Card/Card.tsx @@ -22,6 +22,13 @@ export type CardProps = PolymorphicProps< /** Border radius. @default 'large' */ borderRadius?: 'medium' | 'large' + /** + * Layout of the card. `compact` uses tighter spacing, removes the icon + * background container, and renders a smaller title. + * @default 'default' + */ + layout?: 'default' | 'compact' + /** * Card contents. Provide either `Card.*` subcomponents (e.g. `Card.Heading`, * `Card.Description`, `Card.Metadata`) or custom content. @@ -93,6 +100,7 @@ function CardComponent( className, padding = 'normal', borderRadius = 'large', + layout = 'default', as = 'div', ...rest } = props as CardProps @@ -149,6 +157,7 @@ function CardComponent( data-component="Card" data-padding={padding} data-border-radius={borderRadius} + data-layout={layout} {...rest} > {children} @@ -165,6 +174,7 @@ function CardComponent( data-component="Card" data-padding={padding} data-border-radius={borderRadius} + data-layout={layout} {...rest} > {(image || icon) && (