Skip to content
Open
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/add-card-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Add Card component with subcomponents: Card.Icon, Card.Image, Card.Heading, Card.Description, Card.Menu, and Card.Metadata
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.
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.
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.
59 changes: 59 additions & 0 deletions e2e/components/Card.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {test, expect} from '@playwright/test'
import {visit} from '../test-helpers/storybook'
import {themes} from '../test-helpers/themes'

test.describe('Card', () => {
test.describe('Default', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-card--default',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot()).toMatchSnapshot(`Card.Default.${theme}.png`)
})
})
}
})

test.describe('With Image', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-card-features--with-image',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot()).toMatchSnapshot(`Card.With Image.${theme}.png`)
})
})
}
})

test.describe('With Metadata', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-card-features--with-metadata',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot()).toMatchSnapshot(`Card.With Metadata.${theme}.png`)
})
})
}
})
})
81 changes: 81 additions & 0 deletions packages/react/src/Card/Card.docs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"id": "card",
"name": "Card",
"status": "draft",
"a11yReviewed": false,
"importPath": "@primer/react",
"stories": [
{
"id": "components-card--default"
},
{
"id": "components-card-features--with-image"
},
{
"id": "components-card-features--with-metadata"
}
],
"props": [
{
"name": "className",
"type": "string",
"description": "CSS class name(s) for custom styling."
}
],
"subcomponents": [
{
"name": "Card.Icon",
"props": [
{
"name": "icon",
"type": "React.ElementType",
"description": "An Octicon or custom SVG icon to render."
},
{
"name": "aria-label",
"type": "string",
"description": "Accessible label for the icon. When omitted, the icon is treated as decorative."
}
]
},
{
"name": "Card.Image",
"props": [
{
"name": "src",
"type": "string",
"description": "The image source URL."
},
{
"name": "alt",
"type": "string",
"defaultValue": "\"\"",
"description": "Alt text for accessibility. Defaults to empty string (decorative)."
}
]
},
{
"name": "Card.Heading",
"props": [
{
"name": "as",
"type": "'h2' | 'h3' | 'h4' | 'h5' | 'h6'",
"defaultValue": "'h3'",
"description": "The heading level to render."
}
]
},
{
"name": "Card.Description",
"props": []
},
{
"name": "Card.Menu",
"props": []
},
{
"name": "Card.Metadata",
"props": []
}
]
}
40 changes: 40 additions & 0 deletions packages/react/src/Card/Card.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type {Meta} from '@storybook/react-vite'
import {RepoIcon, StarIcon} from '@primer/octicons-react'
import {Card} from './index'

const meta = {
title: 'Components/Card/Features',
component: Card,
} satisfies Meta<typeof Card>

export default meta

export const WithImage = () => {
return (
<div style={{maxWidth: '400px'}}>
<Card>
<Card.Image src="https://github.com/octocat.png" alt="Octocat" />
<Card.Heading>Card with Image</Card.Heading>
<Card.Description>This card uses an edge-to-edge image instead of an icon.</Card.Description>
</Card>
</div>
)
}

export const WithMetadata = () => {
return (
<div style={{maxWidth: '400px'}}>
<Card>
<Card.Icon icon={RepoIcon} />
<Card.Heading>primer/react</Card.Heading>
<Card.Description>
{"GitHub's design system implemented as React components for building consistent user interfaces."}
</Card.Description>
<Card.Metadata>
<StarIcon size={16} />
1.2k stars
</Card.Metadata>
</Card>
</div>
)
}
89 changes: 89 additions & 0 deletions packages/react/src/Card/Card.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
.Card {
display: grid;
position: relative;
border-radius: var(--borderRadius-large);
overflow: hidden;
grid-auto-rows: max-content auto;
border: var(--borderWidth-thin) solid var(--borderColor-default);
box-shadow: var(--shadow-resting-small);
background-color: var(--bgColor-default);
}

.CardHeader {
display: block;
width: 100%;
height: auto;
/* stylelint-disable primer/spacing */
padding: var(--stack-padding-spacious) var(--stack-padding-spacious) var(--stack-padding-normal)
var(--stack-padding-spacious);
/* stylelint-enable primer/spacing */
}

.CardHeaderEdgeToEdge {
padding: 0;
margin-bottom: var(--base-size-16);
}

.CardImage {
display: block;
width: 100%;
height: auto;
}

.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);
color: var(--fgColor-muted);
}

.CardBody {
display: grid;
gap: var(--base-size-16);
/* stylelint-disable-next-line primer/spacing */
padding: 0 var(--stack-padding-spacious) var(--stack-padding-spacious) var(--stack-padding-spacious);
}

.CardContent {
display: grid;
gap: var(--base-size-8);
}

.CardHeading {
font-size: var(--text-body-size-large);
font-weight: var(--base-text-weight-semibold);
color: var(--fgColor-default);
margin: 0;
}

.CardDescription {
font-size: var(--text-body-size-medium);
color: var(--fgColor-muted);
margin: 0;
}

.CardMetadataContainer {
display: flex;
align-items: center;
gap: var(--base-size-16);
font-size: var(--text-body-size-medium);
color: var(--fgColor-muted);
}

.CardMetadataItem {
display: flex;
align-items: center;
gap: var(--base-size-8);
font: var(--text-body-shorthand-small);
}

.CardMenu {
position: absolute;
top: var(--base-size-16);
right: var(--base-size-16);
z-index: 1;
}
71 changes: 71 additions & 0 deletions packages/react/src/Card/Card.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type {Meta, StoryFn} from '@storybook/react-vite'
import {RocketIcon} from '@primer/octicons-react'
import {Card} from './index'

const meta = {
title: 'Components/Card',
component: Card,
} satisfies Meta<typeof Card>

export default meta

export const Default = () => {
return (
<div style={{maxWidth: '400px'}}>
<Card>
<Card.Icon icon={RocketIcon} />
<Card.Heading>Card Heading</Card.Heading>
<Card.Description>This is a description of the card providing supplemental information.</Card.Description>
<Card.Metadata>Updated 2 hours ago</Card.Metadata>
</Card>
</div>
)
}

type PlaygroundArgs = {
showIcon: boolean
showMetadata: boolean
}

export const Playground: StoryFn<PlaygroundArgs> = ({showIcon, showMetadata}) => (
<div style={{maxWidth: '400px'}}>
<Card>
{showIcon && <Card.Icon icon={RocketIcon} />}
<Card.Heading>Playground Card</Card.Heading>
<Card.Description>Experiment with the Card component and its subcomponents.</Card.Description>
{showMetadata && <Card.Metadata>Just now</Card.Metadata>}
</Card>
</div>
)

Playground.args = {
showIcon: true,
showMetadata: true,
}

Playground.argTypes = {
showIcon: {
control: {type: 'boolean'},
description: 'Show or hide the Card.Icon subcomponent',
},
showMetadata: {
control: {type: 'boolean'},
description: 'Show or hide the Card.Metadata subcomponent',
},
}

export const CustomContent = () => (
<div style={{maxWidth: '400px'}}>
<Card>
<div style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
<strong>Custom Content Card</strong>
<p style={{margin: 0}}>This card uses arbitrary custom content instead of the built-in subcomponents.</p>
<ul style={{margin: 0, paddingLeft: '16px'}}>
<li>Item one</li>
<li>Item two</li>
<li>Item three</li>
</ul>
</div>
</Card>
</div>
)
Loading
Loading