Formatting-only changes across all component and story files. No logic or behaviour changes — only whitespace, line breaks, and trailing commas. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
494 lines
16 KiB
TypeScript
494 lines
16 KiB
TypeScript
import { useState } from 'react';
|
|
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 background fill, shadow lift, and pointer cursor',
|
|
table: { defaultValue: { summary: 'false' } },
|
|
},
|
|
selected: {
|
|
control: 'boolean',
|
|
description: 'Highlights the card as selected — brand border + warm background',
|
|
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 background fill and shadow lift */
|
|
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 background fill and shadow lift.
|
|
</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 get a subtle background fill on hover.
|
|
</Typography>
|
|
</Card>
|
|
</div>
|
|
),
|
|
};
|
|
|
|
// ─── Selected ───────────────────────────────────────────────────────────────
|
|
|
|
/** Selected state — brand border + warm background tint */
|
|
export const Selected: Story = {
|
|
render: () => (
|
|
<div style={{ display: 'flex', gap: 24, maxWidth: 800 }}>
|
|
<Card variant="outlined" sx={{ flex: 1 }}>
|
|
<Typography variant="h5" gutterBottom>
|
|
Not selected
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Standard outlined card in its resting state.
|
|
</Typography>
|
|
</Card>
|
|
<Card variant="outlined" selected sx={{ flex: 1 }}>
|
|
<Typography variant="h5" gutterBottom>
|
|
Selected
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Brand border and warm background tint show this is the active choice.
|
|
</Typography>
|
|
</Card>
|
|
</div>
|
|
),
|
|
};
|
|
|
|
// ─── Option Select Pattern ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Interactive option selection matching the FA 1.0 "ListItemPurchaseOption" pattern.
|
|
* Click a card to select it. Matches the Figma states:
|
|
* inactive → hover (bg fill) → active (brand border + warm bg).
|
|
*/
|
|
export const OptionSelect: Story = {
|
|
name: 'Option Select',
|
|
render: function OptionSelectDemo() {
|
|
const [selectedId, setSelectedId] = useState<string | null>('chapel');
|
|
|
|
const options = [
|
|
{
|
|
id: 'chapel',
|
|
title: 'Chapel service',
|
|
desc: 'Traditional ceremony in our heritage-listed chapel, seating up to 120 guests.',
|
|
},
|
|
{
|
|
id: 'graveside',
|
|
title: 'Graveside service',
|
|
desc: 'An intimate outdoor farewell at the final resting place.',
|
|
},
|
|
{
|
|
id: 'memorial',
|
|
title: 'Memorial service',
|
|
desc: 'A celebration of life gathering at a venue of your choosing.',
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 500 }}>
|
|
<Typography variant="h5" sx={{ mb: 1 }}>
|
|
Choose your service type
|
|
</Typography>
|
|
{options.map((option) => (
|
|
<Card
|
|
key={option.id}
|
|
variant="outlined"
|
|
interactive
|
|
selected={selectedId === option.id}
|
|
padding="compact"
|
|
tabIndex={0}
|
|
onClick={() => setSelectedId(option.id)}
|
|
role="radio"
|
|
aria-checked={selectedId === option.id}
|
|
>
|
|
<Typography variant="labelLg" gutterBottom>
|
|
{option.title}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{option.desc}
|
|
</Typography>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
// ─── Multi-Select Pattern ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Multi-select variant — click to toggle multiple cards.
|
|
* Useful for add-on services, package inclusions, etc.
|
|
*/
|
|
export const MultiSelect: Story = {
|
|
name: 'Multi-Select',
|
|
render: function MultiSelectDemo() {
|
|
const [selected, setSelected] = useState<Set<string>>(new Set(['flowers']));
|
|
|
|
const addOns = [
|
|
{ id: 'flowers', title: 'Floral arrangements', desc: 'Custom flowers for the service' },
|
|
{ id: 'catering', title: 'Catering', desc: 'Light refreshments after the service' },
|
|
{ id: 'music', title: 'Live musician', desc: 'Solo musician for the ceremony' },
|
|
{ id: 'printing', title: 'Memorial printing', desc: 'Order of service booklets' },
|
|
];
|
|
|
|
const toggle = (id: string) => {
|
|
setSelected((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div style={{ maxWidth: 500 }}>
|
|
<Typography variant="h5" sx={{ mb: 1 }}>
|
|
Select add-ons
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
Choose as many as you like
|
|
</Typography>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
{addOns.map((item) => (
|
|
<Card
|
|
key={item.id}
|
|
variant="outlined"
|
|
interactive
|
|
selected={selected.has(item.id)}
|
|
padding="compact"
|
|
tabIndex={0}
|
|
onClick={() => toggle(item.id)}
|
|
role="checkbox"
|
|
aria-checked={selected.has(item.id)}
|
|
>
|
|
<Typography variant="labelLg" gutterBottom>
|
|
{item.title}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{item.desc}
|
|
</Typography>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
// ─── On Different Backgrounds ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Demonstrates how cards adapt to different surface colours.
|
|
* Elevated cards stand out via shadow on any surface.
|
|
* Outlined cards use borders on white, contrast on grey.
|
|
*/
|
|
export const OnDifferentBackgrounds: Story = {
|
|
name: 'On Different Backgrounds',
|
|
render: () => (
|
|
<div style={{ display: 'flex', gap: 32, maxWidth: 900 }}>
|
|
{/* White background */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
padding: 24,
|
|
backgroundColor: '#ffffff',
|
|
borderRadius: 8,
|
|
border: '1px dashed #d4d4d4',
|
|
}}
|
|
>
|
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: 'block' }}>
|
|
On white surface
|
|
</Typography>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<Card variant="elevated">
|
|
<Typography variant="labelLg">Elevated</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Shadow defines edges
|
|
</Typography>
|
|
</Card>
|
|
<Card variant="outlined">
|
|
<Typography variant="labelLg">Outlined</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Border defines edges
|
|
</Typography>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
{/* Grey background */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
padding: 24,
|
|
backgroundColor: '#f5f5f5',
|
|
borderRadius: 8,
|
|
}}
|
|
>
|
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: 'block' }}>
|
|
On grey surface
|
|
</Typography>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<Card variant="elevated">
|
|
<Typography variant="labelLg">Elevated</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
White card + shadow on grey
|
|
</Typography>
|
|
</Card>
|
|
<Card variant="outlined">
|
|
<Typography variant="labelLg">Outlined</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Contrast + border on grey
|
|
</Typography>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
),
|
|
};
|
|
|
|
// ─── 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>
|
|
),
|
|
};
|
|
|
|
// ─── Rich 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>
|
|
),
|
|
};
|