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>
384 lines
14 KiB
TypeScript
384 lines
14 KiB
TypeScript
import React, { useState } from 'react';
|
||
import type { Meta, StoryObj } from '@storybook/react';
|
||
import { Chip } from './Chip';
|
||
import { Card } from '../Card';
|
||
import { Typography } from '../Typography';
|
||
import Box from '@mui/material/Box';
|
||
import LocalOfferIcon from '@mui/icons-material/LocalOffer';
|
||
import FaceIcon from '@mui/icons-material/Face';
|
||
import CheckIcon from '@mui/icons-material/Check';
|
||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||
import ChurchIcon from '@mui/icons-material/Church';
|
||
import LocalFloristIcon from '@mui/icons-material/LocalFlorist';
|
||
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
|
||
import RestaurantIcon from '@mui/icons-material/Restaurant';
|
||
import MusicNoteIcon from '@mui/icons-material/MusicNote';
|
||
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
|
||
|
||
const meta: Meta<typeof Chip> = {
|
||
title: 'Atoms/Chip',
|
||
component: Chip,
|
||
tags: ['autodocs'],
|
||
parameters: {
|
||
layout: 'centered',
|
||
},
|
||
argTypes: {
|
||
variant: {
|
||
control: 'select',
|
||
options: ['filled', 'outlined'],
|
||
description: 'Visual style variant',
|
||
table: { defaultValue: { summary: 'filled' } },
|
||
},
|
||
color: {
|
||
control: 'select',
|
||
options: ['default', 'primary'],
|
||
description: 'Colour intent',
|
||
table: { defaultValue: { summary: 'default' } },
|
||
},
|
||
size: {
|
||
control: 'select',
|
||
options: ['small', 'medium'],
|
||
description: 'Size preset',
|
||
table: { defaultValue: { summary: 'medium' } },
|
||
},
|
||
selected: {
|
||
control: 'boolean',
|
||
description: 'Selected/active state',
|
||
table: { defaultValue: { summary: 'false' } },
|
||
},
|
||
clickable: {
|
||
control: 'boolean',
|
||
description: 'Whether the chip is clickable',
|
||
},
|
||
},
|
||
};
|
||
|
||
export default meta;
|
||
type Story = StoryObj<typeof Chip>;
|
||
|
||
// ─── Default ────────────────────────────────────────────────────────────────
|
||
|
||
/** Default chip — filled variant, neutral colour */
|
||
export const Default: Story = {
|
||
args: {
|
||
label: 'Chip label',
|
||
},
|
||
};
|
||
|
||
// ─── Variants ───────────────────────────────────────────────────────────────
|
||
|
||
/** Both visual variants with default and primary colour */
|
||
export const Variants: Story = {
|
||
render: () => (
|
||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||
<Chip label="Filled default" />
|
||
<Chip label="Filled primary" color="primary" />
|
||
<Chip variant="outlined" label="Outlined default" />
|
||
<Chip variant="outlined" label="Outlined primary" color="primary" />
|
||
</Box>
|
||
),
|
||
};
|
||
|
||
// ─── Sizes ──────────────────────────────────────────────────────────────────
|
||
|
||
/** Small and medium sizes side by side */
|
||
export const Sizes: Story = {
|
||
render: () => (
|
||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||
<Chip size="small" label="Small" icon={<LocalOfferIcon />} />
|
||
<Chip size="medium" label="Medium" icon={<LocalOfferIcon />} />
|
||
<Chip size="small" variant="outlined" label="Small outlined" icon={<LocalOfferIcon />} />
|
||
<Chip size="medium" variant="outlined" label="Medium outlined" icon={<LocalOfferIcon />} />
|
||
</Box>
|
||
),
|
||
};
|
||
|
||
// ─── With Icons ─────────────────────────────────────────────────────────────
|
||
|
||
/** Chips with leading icons */
|
||
export const WithIcons: Story = {
|
||
name: 'With Icons',
|
||
render: () => (
|
||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||
<Chip icon={<ChurchIcon />} label="Chapel" />
|
||
<Chip icon={<LocalFloristIcon />} label="Flowers" color="primary" />
|
||
<Chip icon={<DirectionsCarIcon />} label="Transport" variant="outlined" />
|
||
<Chip icon={<FaceIcon />} label="Family" variant="outlined" color="primary" />
|
||
</Box>
|
||
),
|
||
};
|
||
|
||
// ─── Clickable ──────────────────────────────────────────────────────────────
|
||
|
||
/** Clickable chips respond to click events (for filtering/toggling) */
|
||
export const Clickable: Story = {
|
||
render: () => (
|
||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||
<Chip label="Clickable default" onClick={() => {}} />
|
||
<Chip label="Clickable primary" color="primary" onClick={() => {}} />
|
||
<Chip label="Clickable outlined" variant="outlined" onClick={() => {}} />
|
||
<Chip
|
||
label="Clickable outlined primary"
|
||
variant="outlined"
|
||
color="primary"
|
||
onClick={() => {}}
|
||
/>
|
||
</Box>
|
||
),
|
||
};
|
||
|
||
// ─── Deletable ──────────────────────────────────────────────────────────────
|
||
|
||
/** Deletable chips show a close icon */
|
||
export const Deletable: Story = {
|
||
render: () => (
|
||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||
<Chip label="Remove me" onDelete={() => {}} />
|
||
<Chip label="Brand deletable" color="primary" onDelete={() => {}} />
|
||
<Chip label="Outlined deletable" variant="outlined" onDelete={() => {}} />
|
||
<Chip label="With icon" icon={<LocalOfferIcon />} onDelete={() => {}} />
|
||
</Box>
|
||
),
|
||
};
|
||
|
||
// ─── Selected State ─────────────────────────────────────────────────────────
|
||
|
||
/** Selected state promotes chip to brand colour */
|
||
export const Selected: Story = {
|
||
render: () => (
|
||
<Box sx={{ display: 'flex', gap: 2, flexDirection: 'column' }}>
|
||
<Box>
|
||
<Typography variant="label" sx={{ mb: 1 }}>
|
||
Filled
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', gap: 1.5 }}>
|
||
<Chip label="Not selected" onClick={() => {}} />
|
||
<Chip label="Selected" selected onClick={() => {}} />
|
||
</Box>
|
||
</Box>
|
||
<Box>
|
||
<Typography variant="label" sx={{ mb: 1 }}>
|
||
Outlined
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', gap: 1.5 }}>
|
||
<Chip variant="outlined" label="Not selected" onClick={() => {}} />
|
||
<Chip variant="outlined" label="Selected" selected onClick={() => {}} />
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
),
|
||
};
|
||
|
||
// ─── Interactive: Filter Chips ──────────────────────────────────────────────
|
||
|
||
/**
|
||
* Toggle filter pattern — commonly used for service category filtering.
|
||
* Click chips to toggle selection.
|
||
*/
|
||
export const FilterChips: Story = {
|
||
name: 'Interactive — Filter Chips',
|
||
render: () => {
|
||
const categories = [
|
||
{ label: 'Chapel', icon: <ChurchIcon /> },
|
||
{ label: 'Flowers', icon: <LocalFloristIcon /> },
|
||
{ label: 'Transport', icon: <DirectionsCarIcon /> },
|
||
{ label: 'Catering', icon: <RestaurantIcon /> },
|
||
{ label: 'Music', icon: <MusicNoteIcon /> },
|
||
{ label: 'Photography', icon: <PhotoCameraIcon /> },
|
||
];
|
||
|
||
const FilterDemo = () => {
|
||
const [selected, setSelected] = useState<Set<string>>(new Set(['Chapel', 'Flowers']));
|
||
|
||
const toggle = (label: string) => {
|
||
setSelected((prev) => {
|
||
const next = new Set(prev);
|
||
if (next.has(label)) next.delete(label);
|
||
else next.add(label);
|
||
return next;
|
||
});
|
||
};
|
||
|
||
return (
|
||
<Box sx={{ maxWidth: 450 }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||
<FilterListIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||
<Typography variant="label">Filter services</Typography>
|
||
</Box>
|
||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||
{categories.map(({ label, icon }) => (
|
||
<Chip
|
||
key={label}
|
||
label={label}
|
||
icon={selected.has(label) ? <CheckIcon /> : icon}
|
||
selected={selected.has(label)}
|
||
onClick={() => toggle(label)}
|
||
variant="outlined"
|
||
/>
|
||
))}
|
||
</Box>
|
||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||
Selected: {selected.size === 0 ? 'None' : Array.from(selected).join(', ')}
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
return <FilterDemo />;
|
||
},
|
||
};
|
||
|
||
// ─── Interactive: Removable Tags ────────────────────────────────────────────
|
||
|
||
/**
|
||
* Removable tag pattern — for selected items that can be dismissed.
|
||
* Click the × icon to remove a tag.
|
||
*/
|
||
export const RemovableTags: Story = {
|
||
name: 'Interactive — Removable Tags',
|
||
render: () => {
|
||
const TagDemo = () => {
|
||
const [tags, setTags] = useState([
|
||
'White roses',
|
||
'Organ music',
|
||
'Prayer cards',
|
||
'Memorial video',
|
||
'Guest book',
|
||
]);
|
||
|
||
const remove = (tag: string) => {
|
||
setTags((prev) => prev.filter((t) => t !== tag));
|
||
};
|
||
|
||
return (
|
||
<Box sx={{ maxWidth: 450 }}>
|
||
<Typography variant="label" sx={{ mb: 1 }}>
|
||
Selected additions ({tags.length})
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', minHeight: 32 }}>
|
||
{tags.length === 0 ? (
|
||
<Typography variant="body2" color="text.secondary">
|
||
No items selected
|
||
</Typography>
|
||
) : (
|
||
tags.map((tag) => (
|
||
<Chip key={tag} label={tag} color="primary" onDelete={() => remove(tag)} />
|
||
))
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
return <TagDemo />;
|
||
},
|
||
};
|
||
|
||
// ─── In Context: Service Option ─────────────────────────────────────────────
|
||
|
||
/**
|
||
* Chips used inside a ServiceOption-style card layout,
|
||
* showing service tags and category labels.
|
||
*/
|
||
export const InServiceOption: Story = {
|
||
name: 'In Context — Service Option',
|
||
render: () => (
|
||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxWidth: 400 }}>
|
||
<Card interactive>
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', mb: 1 }}>
|
||
<Typography variant="h5">Chapel Ceremony</Typography>
|
||
<Typography variant="display3" color="primary">
|
||
$1,200
|
||
</Typography>
|
||
</Box>
|
||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||
Traditional chapel service with celebrant and music of your choosing.
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||
<Chip size="small" icon={<ChurchIcon />} label="Indoor" />
|
||
<Chip size="small" icon={<MusicNoteIcon />} label="Music included" />
|
||
<Chip size="small" label="60 minutes" />
|
||
</Box>
|
||
</Card>
|
||
|
||
<Card interactive>
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', mb: 1 }}>
|
||
<Typography variant="h5">Graveside Service</Typography>
|
||
<Typography variant="display3" color="primary">
|
||
$900
|
||
</Typography>
|
||
</Box>
|
||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||
Intimate outdoor farewell at the burial site.
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||
<Chip size="small" label="Outdoor" />
|
||
<Chip size="small" label="30 minutes" />
|
||
<Chip size="small" color="primary" label="Popular" />
|
||
</Box>
|
||
</Card>
|
||
</Box>
|
||
),
|
||
};
|
||
|
||
// ─── Complete Matrix ────────────────────────────────────────────────────────
|
||
|
||
/** Full variant × colour × size × state matrix for visual QA */
|
||
export const CompleteMatrix: Story = {
|
||
name: 'Complete Matrix',
|
||
render: () => {
|
||
const variants = ['filled', 'outlined'] as const;
|
||
const colors = ['default', 'primary'] as const;
|
||
const sizes = ['medium', 'small'] as const;
|
||
|
||
return (
|
||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||
{variants.map((variant) => (
|
||
<Box key={variant}>
|
||
<Typography variant="label" sx={{ mb: 1, textTransform: 'capitalize' }}>
|
||
{variant}
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||
{sizes.map((size) => (
|
||
<Box key={size} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||
<Box sx={{ width: 70, fontSize: 12, color: 'text.secondary' }}>{size}</Box>
|
||
{colors.map((color) => (
|
||
<React.Fragment key={color}>
|
||
<Chip variant={variant} color={color} size={size} label={color} />
|
||
<Chip
|
||
variant={variant}
|
||
color={color}
|
||
size={size}
|
||
label={`${color} + icon`}
|
||
icon={<LocalOfferIcon />}
|
||
/>
|
||
<Chip
|
||
variant={variant}
|
||
color={color}
|
||
size={size}
|
||
label={`${color} delete`}
|
||
onDelete={() => {}}
|
||
/>
|
||
</React.Fragment>
|
||
))}
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
</Box>
|
||
))}
|
||
|
||
<Box>
|
||
<Typography variant="label" sx={{ mb: 1 }}>
|
||
Selected state
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||
<Chip selected label="Filled selected" onClick={() => {}} />
|
||
<Chip selected variant="outlined" label="Outlined selected" onClick={() => {}} />
|
||
<Chip selected label="With icon" icon={<CheckIcon />} onClick={() => {}} />
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
},
|
||
};
|