Add Card atom component

- Create card component tokens (borderRadius, padding, shadow, border, background)
- Build Card component with elevated/outlined variants, interactive hover, padding presets
- Add MUI theme overrides using card tokens (shadow.md resting, border for outlined)
- Create 8 Storybook stories: Default, Variants, Interactive, PaddingPresets,
  PriceCardPreview, ServiceOptionPreview, WithImage, RichContent
- Regenerate token pipeline outputs (7 new card tokens)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 15:31:10 +11:00
parent e4f9edd97f
commit 7169a6559b
7 changed files with 467 additions and 1 deletions

View File

@@ -0,0 +1,309 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Card } from './Card';
import { Typography } from '../Typography';
import { Button } from '../Button';
import Box from '@mui/material/Box';
const meta: Meta<typeof Card> = {
title: 'Atoms/Card',
component: Card,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
argTypes: {
variant: {
control: 'select',
options: ['elevated', 'outlined'],
description: 'Visual style variant',
table: { defaultValue: { summary: 'elevated' } },
},
interactive: {
control: 'boolean',
description: 'Adds hover shadow lift and pointer cursor',
table: { defaultValue: { summary: 'false' } },
},
padding: {
control: 'select',
options: ['default', 'compact', 'none'],
description: 'Padding preset',
table: { defaultValue: { summary: 'default' } },
},
},
};
export default meta;
type Story = StoryObj<typeof Card>;
// ─── Default ────────────────────────────────────────────────────────────────
/** Default card — elevated with standard padding */
export const Default: Story = {
args: {
children: (
<>
<Typography variant="h4" gutterBottom>
Funeral package
</Typography>
<Typography variant="body1" color="text.secondary">
A comprehensive service including chapel ceremony, transport, and
preparation. Suitable for families seeking a traditional farewell.
</Typography>
</>
),
},
decorators: [
(Story) => (
<div style={{ width: 400 }}>
<Story />
</div>
),
],
};
// ─── Variants ───────────────────────────────────────────────────────────────
/** Both visual variants side by side */
export const Variants: Story = {
render: () => (
<div style={{ display: 'flex', gap: 24, maxWidth: 800 }}>
<Card variant="elevated" sx={{ flex: 1 }}>
<Typography variant="h5" gutterBottom>
Elevated
</Typography>
<Typography variant="body2" color="text.secondary">
Uses shadow for depth. Default variant for most content cards.
</Typography>
</Card>
<Card variant="outlined" sx={{ flex: 1 }}>
<Typography variant="h5" gutterBottom>
Outlined
</Typography>
<Typography variant="body2" color="text.secondary">
Uses a subtle border. Good for less prominent or grouped content.
</Typography>
</Card>
</div>
),
};
// ─── Interactive ────────────────────────────────────────────────────────────
/** Interactive cards with hover elevation (click/hover to see effect) */
export const Interactive: Story = {
render: () => (
<div style={{ display: 'flex', gap: 24, maxWidth: 800 }}>
<Card
interactive
sx={{ flex: 1 }}
tabIndex={0}
onClick={() => alert('Card clicked')}
>
<Typography variant="h5" gutterBottom>
Elevated + Interactive
</Typography>
<Typography variant="body2" color="text.secondary">
Hover to see the shadow lift. Click to trigger action.
</Typography>
</Card>
<Card
variant="outlined"
interactive
sx={{ flex: 1 }}
tabIndex={0}
onClick={() => alert('Card clicked')}
>
<Typography variant="h5" gutterBottom>
Outlined + Interactive
</Typography>
<Typography variant="body2" color="text.secondary">
Outlined cards can also be interactive with hover effects.
</Typography>
</Card>
</div>
),
};
// ─── Padding Presets ────────────────────────────────────────────────────────
/** All three padding options */
export const PaddingPresets: Story = {
name: 'Padding Presets',
render: () => (
<div style={{ display: 'flex', gap: 24, maxWidth: 900, alignItems: 'start' }}>
<Card padding="default" sx={{ flex: 1 }}>
<Typography variant="labelLg" gutterBottom>
Default (24px)
</Typography>
<Typography variant="body2" color="text.secondary">
Standard spacing for desktop cards.
</Typography>
</Card>
<Card padding="compact" sx={{ flex: 1 }}>
<Typography variant="labelLg" gutterBottom>
Compact (16px)
</Typography>
<Typography variant="body2" color="text.secondary">
Tighter spacing for mobile or dense layouts.
</Typography>
</Card>
<Card padding="none" sx={{ flex: 1 }}>
<Box sx={{ p: 3 }}>
<Typography variant="labelLg" gutterBottom>
None (manual)
</Typography>
<Typography variant="body2" color="text.secondary">
Full control add your own padding.
</Typography>
</Box>
</Card>
</div>
),
};
// ─── Price Card Preview ─────────────────────────────────────────────────────
/**
* Preview of how Card will be used in the PriceCard molecule.
* Demonstrates realistic content composition with FA typography and brand colours.
*/
export const PriceCardPreview: Story = {
name: 'Price Card Preview',
render: () => (
<div style={{ width: 340 }}>
<Card interactive tabIndex={0}>
<Typography variant="overline" color="text.secondary" gutterBottom>
Essential
</Typography>
<Typography variant="display3" color="primary" sx={{ mb: 1 }}>
$3,200
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
A respectful and simple service with chapel ceremony, transport, and
professional preparation.
</Typography>
<Button fullWidth size="large">
Select this package
</Button>
</Card>
</div>
),
};
// ─── Service Option Preview ─────────────────────────────────────────────────
/**
* Preview of how Card will be used in the ServiceOption molecule.
* Shows a selectable option card pattern.
*/
export const ServiceOptionPreview: Story = {
name: 'Service Option Preview',
render: () => (
<div style={{ display: 'flex', gap: 16, maxWidth: 700 }}>
{[
{ title: 'Chapel service', desc: 'Traditional ceremony in our chapel' },
{ title: 'Graveside service', desc: 'Intimate outdoor farewell' },
{ title: 'Memorial service', desc: 'Celebration of life gathering' },
].map((option) => (
<Card
key={option.title}
variant="outlined"
interactive
padding="compact"
tabIndex={0}
sx={{ flex: 1 }}
>
<Typography variant="labelLg" gutterBottom>
{option.title}
</Typography>
<Typography variant="body2" color="text.secondary">
{option.desc}
</Typography>
</Card>
))}
</div>
),
};
// ─── With Image ─────────────────────────────────────────────────────────────
/** Card with full-bleed image using padding="none" */
export const WithImage: Story = {
name: 'With Image (No Padding)',
render: () => (
<div style={{ width: 340 }}>
<Card padding="none">
<Box
sx={{
height: 180,
backgroundColor: 'action.hover',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Typography variant="body2" color="text.secondary">
Image placeholder
</Typography>
</Box>
<Box sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>
Parsons Chapel
</Typography>
<Typography variant="body2" color="text.secondary">
Our heritage-listed chapel seats up to 120 guests and features
modern audio-visual facilities.
</Typography>
</Box>
</Card>
</div>
),
};
// ─── Nested Content ─────────────────────────────────────────────────────────
/** Card with rich nested content to verify layout flexibility */
export const RichContent: Story = {
name: 'Rich Content',
render: () => (
<div style={{ width: 400 }}>
<Card>
<Typography variant="overline" color="text.secondary">
Package details
</Typography>
<Typography variant="h4" sx={{ mt: 1, mb: 2 }}>
Premium farewell
</Typography>
<Box
component="ul"
sx={{
pl: 4,
mb: 3,
'& li': { mb: 1 },
}}
>
<li>
<Typography variant="body2">Chapel ceremony (up to 120 guests)</Typography>
</li>
<li>
<Typography variant="body2">Premium timber casket</Typography>
</li>
<li>
<Typography variant="body2">Transport within 50km</Typography>
</li>
<li>
<Typography variant="body2">Professional preparation</Typography>
</li>
</Box>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button size="large" sx={{ flex: 1 }}>
Select
</Button>
<Button variant="outlined" size="large" sx={{ flex: 1 }}>
Compare
</Button>
</Box>
</Card>
</div>
),
};