feat(Card): add optional title prop with uppercase monospace header
When a title string is provided, renders an uppercase monospace h3 header with a subtle border separator above the card body. Children are wrapped in a padded body div when title is present; without title, children render directly as before (no breaking change). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,3 +11,22 @@
|
|||||||
.accent-warning { border-top: 3px solid var(--warning); }
|
.accent-warning { border-top: 3px solid var(--warning); }
|
||||||
.accent-error { border-top: 3px solid var(--error); }
|
.accent-error { border-top: 3px solid var(--error); }
|
||||||
.accent-running { border-top: 3px solid var(--running); }
|
.accent-running { border-top: 3px solid var(--running); }
|
||||||
|
|
||||||
|
.titleHeader {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleText {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|||||||
49
src/design-system/primitives/Card/Card.test.tsx
Normal file
49
src/design-system/primitives/Card/Card.test.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { Card } from './Card'
|
||||||
|
|
||||||
|
describe('Card', () => {
|
||||||
|
it('renders children', () => {
|
||||||
|
render(<Card>Hello world</Card>)
|
||||||
|
expect(screen.getByText('Hello world')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders title when provided', () => {
|
||||||
|
render(<Card title="Status">Content</Card>)
|
||||||
|
expect(screen.getByText('Status')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render title header when title is omitted', () => {
|
||||||
|
const { container } = render(<Card>Content</Card>)
|
||||||
|
expect(container.querySelector('h3')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('wraps children in body div when title is provided', () => {
|
||||||
|
render(<Card title="Status"><span>Content</span></Card>)
|
||||||
|
const content = screen.getByText('Content')
|
||||||
|
expect(content.parentElement).toHaveClass('body')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with accent and title together', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Card accent="success" title="Health">Content</Card>,
|
||||||
|
)
|
||||||
|
const card = container.firstChild as HTMLElement
|
||||||
|
expect(card).toHaveClass('accent-success')
|
||||||
|
expect(screen.getByText('Health')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Content')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts className prop', () => {
|
||||||
|
const { container } = render(<Card className="custom">Content</Card>)
|
||||||
|
const card = container.firstChild as HTMLElement
|
||||||
|
expect(card).toHaveClass('custom')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders children directly when no title (no wrapper div)', () => {
|
||||||
|
const { container } = render(<Card><span>Direct child</span></Card>)
|
||||||
|
const card = container.firstChild as HTMLElement
|
||||||
|
const span = screen.getByText('Direct child')
|
||||||
|
expect(span.parentElement).toBe(card)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -4,15 +4,25 @@ import type { ReactNode } from 'react'
|
|||||||
interface CardProps {
|
interface CardProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
accent?: 'amber' | 'success' | 'warning' | 'error' | 'running' | 'none'
|
accent?: 'amber' | 'success' | 'warning' | 'error' | 'running' | 'none'
|
||||||
|
title?: string
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Card({ children, accent = 'none', className }: CardProps) {
|
export function Card({ children, accent = 'none', title, className }: CardProps) {
|
||||||
const classes = [
|
const classes = [
|
||||||
styles.card,
|
styles.card,
|
||||||
accent !== 'none' ? styles[`accent-${accent}`] : '',
|
accent !== 'none' ? styles[`accent-${accent}`] : '',
|
||||||
className ?? '',
|
className ?? '',
|
||||||
].filter(Boolean).join(' ')
|
].filter(Boolean).join(' ')
|
||||||
|
|
||||||
return <div className={classes}>{children}</div>
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
{title && (
|
||||||
|
<div className={styles.titleHeader}>
|
||||||
|
<h3 className={styles.titleText}>{title}</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{title ? <div className={styles.body}>{children}</div> : children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user