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:
309
src/components/atoms/Card/Card.stories.tsx
Normal file
309
src/components/atoms/Card/Card.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
Reference in New Issue
Block a user