Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/card-compact-layout.md
Original file line number Diff line number Diff line change
@@ -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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions e2e/components/Card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
})
})
}
})
})
16 changes: 16 additions & 0 deletions packages/react/src/Card/Card.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ export const WithImage = () => {
)
}

export const Compact = () => {
return (
<Card layout="compact">
<Card.Icon icon={RepoIcon} />
<Card.Heading>primer/react</Card.Heading>
<Card.Description>
The compact layout uses tighter spacing, an icon without a background container, and a smaller title.
</Card.Description>
<Card.Metadata>
<StarIcon size={16} />
1.2k stars
</Card.Metadata>
</Card>
)
}

export const WithMetadata = () => {
return (
<Card>
Expand Down
40 changes: 35 additions & 5 deletions packages/react/src/Card/Card.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
11 changes: 9 additions & 2 deletions packages/react/src/Card/Card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlaygroundArgs> = ({showIcon, showMetadata, padding, borderRadius}) => (
<Card padding={padding} borderRadius={borderRadius}>
export const Playground: StoryFn<PlaygroundArgs> = ({showIcon, showMetadata, layout, padding, borderRadius}) => (
<Card layout={layout} padding={padding} borderRadius={borderRadius}>
{showIcon && <Card.Icon icon={RocketIcon} />}
<Card.Heading>Playground Card</Card.Heading>
<Card.Description>Experiment with the Card component and its subcomponents.</Card.Description>
Expand All @@ -49,6 +50,7 @@ export const Playground: StoryFn<PlaygroundArgs> = ({showIcon, showMetadata, pad
Playground.args = {
showIcon: true,
showMetadata: true,
layout: 'default',
padding: 'normal',
borderRadius: 'large',
}
Expand All @@ -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'],
Expand Down
28 changes: 28 additions & 0 deletions packages/react/src/Card/Card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,32 @@ describe('Card', () => {
)
expect(container.firstChild).not.toHaveAttribute('as')
})

it('should set data-layout to default by default', () => {
const {container} = render(
<Card>
<Card.Heading>Default Variant</Card.Heading>
</Card>,
)
expect(container.firstChild).toHaveAttribute('data-layout', 'default')
})

it('should set data-layout to compact when layout="compact"', () => {
const {container} = render(
<Card layout="compact">
<Card.Icon icon={TestIcon} />
<Card.Heading>Compact Card</Card.Heading>
</Card>,
)
expect(container.firstChild).toHaveAttribute('data-layout', 'compact')
})

it('should set data-layout on custom content cards', () => {
const {container} = render(
<Card layout="compact">
<p>Custom</p>
</Card>,
)
expect(container.firstChild).toHaveAttribute('data-layout', 'compact')
})
})
10 changes: 10 additions & 0 deletions packages/react/src/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export type CardProps<As extends CardAs = 'div'> = 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.
Expand Down Expand Up @@ -93,6 +100,7 @@ function CardComponent<As extends CardAs>(
className,
padding = 'normal',
borderRadius = 'large',
layout = 'default',
as = 'div',
...rest
} = props as CardProps<CardAs>
Expand Down Expand Up @@ -149,6 +157,7 @@ function CardComponent<As extends CardAs>(
data-component="Card"
data-padding={padding}
data-border-radius={borderRadius}
data-layout={layout}
{...rest}
>
{children}
Expand All @@ -165,6 +174,7 @@ function CardComponent<As extends CardAs>(
data-component="Card"
data-padding={padding}
data-border-radius={borderRadius}
data-layout={layout}
{...rest}
>
{(image || icon) && (
Expand Down
Loading