Spaces:
Running
Running
| import { useState, type ReactNode } from 'react' | |
| interface CardProps { | |
| title?: ReactNode | |
| subtitle?: ReactNode | |
| actions?: ReactNode | |
| children: ReactNode | |
| className?: string | |
| /** Render the header as a show/hide toggle for the body. */ | |
| collapsible?: boolean | |
| /** Initial open state when collapsible (default: closed). */ | |
| defaultOpen?: boolean | |
| } | |
| export default function Card({ | |
| title, | |
| subtitle, | |
| actions, | |
| children, | |
| className, | |
| collapsible = false, | |
| defaultOpen = false, | |
| }: CardProps) { | |
| const [open, setOpen] = useState(defaultOpen) | |
| const isOpen = !collapsible || open | |
| const titleNode = collapsible ? ( | |
| <button | |
| type="button" | |
| className="card-collapse-toggle" | |
| aria-expanded={isOpen} | |
| onClick={() => setOpen((v) => !v)} | |
| style={{ | |
| background: 'none', | |
| border: 'none', | |
| padding: 0, | |
| cursor: 'pointer', | |
| font: 'inherit', | |
| color: 'inherit', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 8, | |
| textAlign: 'left', | |
| }} | |
| > | |
| <span aria-hidden className={'caret' + (isOpen ? ' open' : '')}> | |
| ▶ | |
| </span> | |
| {title && <h3 style={{ margin: 0 }}>{title}</h3>} | |
| </button> | |
| ) : ( | |
| title && <h3>{title}</h3> | |
| ) | |
| return ( | |
| <section className={'card' + (className ? ' ' + className : '')}> | |
| {(title || (actions && isOpen)) && ( | |
| <div className="toolbar" style={{ marginBottom: isOpen && subtitle ? 4 : 14 }}> | |
| <div className="card-title">{titleNode}</div> | |
| {actions && isOpen && <div style={{ display: 'flex', gap: 8 }}>{actions}</div>} | |
| </div> | |
| )} | |
| {isOpen && ( | |
| <> | |
| {subtitle && <p className="card-subtitle">{subtitle}</p>} | |
| {children} | |
| </> | |
| )} | |
| </section> | |
| ) | |
| } | |