Add IconButton, Divider, and Link atom components
IconButton: - Wraps MUI IconButton with FA theme (focus ring, brand hover colours) - 3 sizes reusing Button height tokens: small 32px, medium 40px, large 48px - 5 stories: Default, Sizes, Colours, States, CommonUseCases Divider: - Wraps MUI Divider with FA border token - Horizontal/vertical, fullWidth/inset/middle variants, text support - 6 stories: Default, Variants, Vertical, WithText, InContent, NavigationList Link: - Wraps MUI Link with FA brand colour (copper brand.600, 4.8:1 contrast) - Underline on hover by default, focus-visible ring, fontWeight 500 - 7 stories: Default, UnderlineVariants, ColourVariants, Inline, Navigation, FooterLinks, OnDifferentBackgrounds Theme: Added MuiIconButton, MuiDivider, MuiLink overrides Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
113
src/components/atoms/Divider/Divider.stories.tsx
Normal file
113
src/components/atoms/Divider/Divider.stories.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Divider } from './Divider';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Divider> = {
|
||||||
|
title: 'Atoms/Divider',
|
||||||
|
component: Divider,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
orientation: { control: 'select', options: ['horizontal', 'vertical'] },
|
||||||
|
variant: { control: 'select', options: ['fullWidth', 'inset', 'middle'] },
|
||||||
|
light: { control: 'boolean' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Divider>;
|
||||||
|
|
||||||
|
// ─── Default ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
decorators: [(Story) => <Box sx={{ width: 400 }}><Story /></Box>],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Variants ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** fullWidth, inset, and middle variants */
|
||||||
|
export const Variants: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ width: 400, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>fullWidth (default)</Box>
|
||||||
|
<Divider />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>inset</Box>
|
||||||
|
<Divider variant="inset" />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>middle</Box>
|
||||||
|
<Divider variant="middle" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Vertical ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Vertical divider inside a flex container */
|
||||||
|
export const Vertical: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, height: 40 }}>
|
||||||
|
<Box>Left</Box>
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
<Box>Right</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── With Text ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Divider with centered text (MUI "textAlign" support) */
|
||||||
|
export const WithText: Story = {
|
||||||
|
name: 'With Text',
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ width: 400, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
<Divider>OR</Divider>
|
||||||
|
<Divider textAlign="left">Section</Divider>
|
||||||
|
<Divider textAlign="right">End</Divider>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── In Content ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Dividers separating content sections */
|
||||||
|
export const InContent: Story = {
|
||||||
|
name: 'In Content',
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ width: 400, p: 3, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
||||||
|
<Box sx={{ fontWeight: 600, mb: 1 }}>Service Details</Box>
|
||||||
|
<Box sx={{ fontSize: 14, color: 'text.secondary', mb: 2 }}>
|
||||||
|
Chapel service with traditional ceremony
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ fontWeight: 600, mt: 2, mb: 1 }}>Venue</Box>
|
||||||
|
<Box sx={{ fontSize: 14, color: 'text.secondary', mb: 2 }}>
|
||||||
|
West Chapel, Strathfield
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ fontWeight: 600, mt: 2, mb: 1 }}>Total</Box>
|
||||||
|
<Box sx={{ fontSize: 14, color: 'text.primary' }}>$2,400</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Navigation List ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Dividers between navigation items (footer pattern) */
|
||||||
|
export const NavigationList: Story = {
|
||||||
|
name: 'Navigation List',
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, height: 20 }}>
|
||||||
|
<Box sx={{ fontSize: 14, color: 'text.secondary' }}>FAQ</Box>
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
<Box sx={{ fontSize: 14, color: 'text.secondary' }}>Contact Us</Box>
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
<Box sx={{ fontSize: 14, color: 'text.secondary' }}>Privacy</Box>
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
<Box sx={{ fontSize: 14, color: 'text.secondary' }}>Terms</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
41
src/components/atoms/Divider/Divider.tsx
Normal file
41
src/components/atoms/Divider/Divider.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MuiDivider from '@mui/material/Divider';
|
||||||
|
import type { DividerProps as MuiDividerProps } from '@mui/material/Divider';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the FA Divider component */
|
||||||
|
export interface DividerProps extends MuiDividerProps {}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visual separator for the FA design system.
|
||||||
|
*
|
||||||
|
* Thin line for separating content sections, navigation groups, or
|
||||||
|
* list items. Wraps MUI Divider with FA border tokens.
|
||||||
|
*
|
||||||
|
* Orientations:
|
||||||
|
* - `horizontal` (default) — full-width horizontal line
|
||||||
|
* - `vertical` — full-height vertical line (use inside flex containers)
|
||||||
|
*
|
||||||
|
* Variants:
|
||||||
|
* - `fullWidth` (default) — spans the full container
|
||||||
|
* - `inset` — indented from the left (for list item separators)
|
||||||
|
* - `middle` — indented from both sides
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```tsx
|
||||||
|
* <Divider />
|
||||||
|
* <Divider orientation="vertical" flexItem />
|
||||||
|
* <Divider variant="inset" />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const Divider = React.forwardRef<HTMLHRElement, DividerProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
return <MuiDivider ref={ref} {...props} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Divider.displayName = 'Divider';
|
||||||
|
export default Divider;
|
||||||
2
src/components/atoms/Divider/index.ts
Normal file
2
src/components/atoms/Divider/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { Divider, default } from './Divider';
|
||||||
|
export type { DividerProps } from './Divider';
|
||||||
155
src/components/atoms/IconButton/IconButton.stories.tsx
Normal file
155
src/components/atoms/IconButton/IconButton.stories.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { IconButton } from './IconButton';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||||
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
|
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||||
|
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
|
||||||
|
|
||||||
|
const meta: Meta<typeof IconButton> = {
|
||||||
|
title: 'Atoms/IconButton',
|
||||||
|
component: IconButton,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
size: { control: 'select', options: ['small', 'medium', 'large'] },
|
||||||
|
color: { control: 'select', options: ['default', 'primary', 'secondary', 'error'] },
|
||||||
|
disabled: { control: 'boolean' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof IconButton>;
|
||||||
|
|
||||||
|
// ─── Default ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
'aria-label': 'Close',
|
||||||
|
children: <CloseIcon />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Sizes ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Three sizes matching Button height scale */
|
||||||
|
export const Sizes: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<IconButton size="small" aria-label="Search">
|
||||||
|
<SearchIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary', mt: 0.5 }}>small (32px)</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<IconButton size="medium" aria-label="Search">
|
||||||
|
<SearchIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary', mt: 0.5 }}>medium (40px)</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<IconButton size="large" aria-label="Search">
|
||||||
|
<SearchIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary', mt: 0.5 }}>large (48px)</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Colours ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Colour options for different contexts */
|
||||||
|
export const Colours: Story = {
|
||||||
|
name: 'Colours',
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<IconButton color="default" aria-label="Menu">
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton color="primary" aria-label="Edit">
|
||||||
|
<EditOutlinedIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton color="secondary" aria-label="More options">
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton color="error" aria-label="Delete">
|
||||||
|
<DeleteOutlineIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── States ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Interactive states: default, hover (try it), disabled */
|
||||||
|
export const States: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<IconButton aria-label="Default">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary', mt: 0.5 }}>Default</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<IconButton disabled aria-label="Disabled">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary', mt: 0.5 }}>Disabled</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Common Use Cases ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Real-world icon button patterns */
|
||||||
|
export const CommonUseCases: Story = {
|
||||||
|
name: 'Common Use Cases',
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
{/* Card actions toolbar */}
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>Card action toolbar</Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, width: 'fit-content' }}>
|
||||||
|
<IconButton size="small" color="primary" aria-label="Favourite">
|
||||||
|
<FavoriteBorderIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton size="small" color="primary" aria-label="Share">
|
||||||
|
<ShareOutlinedIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton size="small" aria-label="More options">
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Dialog close */}
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>Dialog close button</Box>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1, width: 300 }}>
|
||||||
|
<Box sx={{ fontWeight: 600 }}>Confirm Selection</Box>
|
||||||
|
<IconButton size="small" aria-label="Close dialog">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Navigation header */}
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ fontSize: 12, color: 'text.secondary', mb: 1 }}>Mobile navigation toggle</Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 1, backgroundColor: 'var(--fa-color-brand-50)', borderRadius: 1, width: 'fit-content' }}>
|
||||||
|
<IconButton size="large" aria-label="Open menu">
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Box sx={{ fontWeight: 600 }}>Funeral Arranger</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
40
src/components/atoms/IconButton/IconButton.tsx
Normal file
40
src/components/atoms/IconButton/IconButton.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MuiIconButton from '@mui/material/IconButton';
|
||||||
|
import type { IconButtonProps as MuiIconButtonProps } from '@mui/material/IconButton';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the FA IconButton component */
|
||||||
|
export interface IconButtonProps extends MuiIconButtonProps {}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon-only button for the FA design system.
|
||||||
|
*
|
||||||
|
* Square button containing a single icon — used for close buttons, menu
|
||||||
|
* toggles, toolbar actions, and anywhere a text label would be redundant.
|
||||||
|
* Wraps MUI IconButton with FA brand tokens and consistent sizing.
|
||||||
|
*
|
||||||
|
* Sizes use the same height scale as Button:
|
||||||
|
* - `small` — 32px (compact toolbars, card actions)
|
||||||
|
* - `medium` — 40px (default, general actions)
|
||||||
|
* - `large` — 48px (mobile CTAs, meets 44px touch target)
|
||||||
|
*
|
||||||
|
* **Accessibility**: Always provide an `aria-label` prop. Icon-only
|
||||||
|
* buttons have no visible text, so screen readers rely entirely on
|
||||||
|
* the aria-label to announce the action.
|
||||||
|
* ```tsx
|
||||||
|
* <IconButton aria-label="Close dialog">
|
||||||
|
* <CloseIcon />
|
||||||
|
* </IconButton>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
return <MuiIconButton ref={ref} {...props} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
IconButton.displayName = 'IconButton';
|
||||||
|
export default IconButton;
|
||||||
2
src/components/atoms/IconButton/index.ts
Normal file
2
src/components/atoms/IconButton/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { IconButton, default } from './IconButton';
|
||||||
|
export type { IconButtonProps } from './IconButton';
|
||||||
138
src/components/atoms/Link/Link.stories.tsx
Normal file
138
src/components/atoms/Link/Link.stories.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Link } from './Link';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Link> = {
|
||||||
|
title: 'Atoms/Link',
|
||||||
|
component: Link,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
underline: { control: 'select', options: ['none', 'hover', 'always'] },
|
||||||
|
color: { control: 'text' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Link>;
|
||||||
|
|
||||||
|
// ─── Default ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
href: '#',
|
||||||
|
children: 'Frequently Asked Questions',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Underline Variants ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Three underline modes */
|
||||||
|
export const UnderlineVariants: Story = {
|
||||||
|
name: 'Underline Variants',
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Link href="#" underline="hover">Hover (default)</Link>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary' }}>underline="hover"</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Link href="#" underline="always">Always underlined</Link>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary' }}>underline="always"</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Link href="#" underline="none">No underline</Link>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary' }}>underline="none"</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Colour Variants ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Brand (default) and secondary colour options */
|
||||||
|
export const ColourVariants: Story = {
|
||||||
|
name: 'Colour Variants',
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Box>
|
||||||
|
<Link href="#">Brand link (default — copper, 4.8:1 contrast)</Link>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Link href="#" color="text.secondary">Secondary link (neutral grey)</Link>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Link href="#" color="text.primary">Primary text link (charcoal)</Link>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Link href="#" color="error.main">Error link (red — for destructive actions)</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Inline ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Links inline within body text */
|
||||||
|
export const Inline: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ maxWidth: 500, lineHeight: 1.7 }}>
|
||||||
|
If you need help planning a funeral, our{' '}
|
||||||
|
<Link href="#">arrangement guide</Link> walks you through each
|
||||||
|
step. You can also browse our{' '}
|
||||||
|
<Link href="#">provider directory</Link> to find local funeral
|
||||||
|
directors, or <Link href="#">contact us</Link> directly for
|
||||||
|
personalised assistance.
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Navigation ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Links styled for navigation (no underline) */
|
||||||
|
export const Navigation: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||||
|
<Link href="#" underline="none" sx={{ fontWeight: 600 }}>FAQ</Link>
|
||||||
|
<Link href="#" underline="none" sx={{ fontWeight: 600 }}>Contact Us</Link>
|
||||||
|
<Link href="#" underline="none" sx={{ fontWeight: 600 }}>Log In</Link>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Footer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Footer link pattern — secondary colour, smaller text */
|
||||||
|
export const FooterLinks: Story = {
|
||||||
|
name: 'Footer Links',
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||||
|
<Link href="#" color="text.secondary" variant="body2">Privacy Policy</Link>
|
||||||
|
<Link href="#" color="text.secondary" variant="body2">Terms of Service</Link>
|
||||||
|
<Link href="#" color="text.secondary" variant="body2">Accessibility</Link>
|
||||||
|
<Link href="#" color="text.secondary" variant="body2">Cookie Settings</Link>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── On Different Backgrounds ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Links on white vs warm vs grey surfaces */
|
||||||
|
export const OnDifferentBackgrounds: Story = {
|
||||||
|
name: 'On Different Backgrounds',
|
||||||
|
render: () => (
|
||||||
|
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||||
|
<Box sx={{ p: 3, backgroundColor: 'background.default', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary', mb: 1 }}>White</Box>
|
||||||
|
<Link href="#">Learn more</Link>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ p: 3, backgroundColor: 'var(--fa-color-surface-warm)', borderRadius: 1 }}>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary', mb: 1 }}>Warm (brand.50)</Box>
|
||||||
|
<Link href="#">Learn more</Link>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ p: 3, backgroundColor: 'var(--fa-color-surface-subtle)', borderRadius: 1 }}>
|
||||||
|
<Box sx={{ fontSize: 11, color: 'text.secondary', mb: 1 }}>Grey (neutral.50)</Box>
|
||||||
|
<Link href="#">Learn more</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
};
|
||||||
38
src/components/atoms/Link/Link.tsx
Normal file
38
src/components/atoms/Link/Link.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MuiLink from '@mui/material/Link';
|
||||||
|
import type { LinkProps as MuiLinkProps } from '@mui/material/Link';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Props for the FA Link component */
|
||||||
|
export interface LinkProps extends MuiLinkProps {}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation text link for the FA design system.
|
||||||
|
*
|
||||||
|
* Inline or standalone text link with FA brand styling — copper colour
|
||||||
|
* (brand.600) for WCAG AA compliance on white backgrounds. Underline
|
||||||
|
* appears on hover by default.
|
||||||
|
*
|
||||||
|
* Wraps MUI Link with FA theme tokens. Uses `color.text.brand`
|
||||||
|
* (#B0610F, 4.8:1 contrast ratio on white).
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```tsx
|
||||||
|
* <Link href="/faq">Frequently Asked Questions</Link>
|
||||||
|
* <Link href="/contact" color="text.secondary">Contact Us</Link>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* For button-styled links, use `Button` with `component="a"` and `href`.
|
||||||
|
* For navigation menu items, use Link with `underline="none"`.
|
||||||
|
*/
|
||||||
|
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
return <MuiLink ref={ref} {...props} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Link.displayName = 'Link';
|
||||||
|
export default Link;
|
||||||
2
src/components/atoms/Link/index.ts
Normal file
2
src/components/atoms/Link/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { Link, default } from './Link';
|
||||||
|
export type { LinkProps } from './Link';
|
||||||
@@ -679,6 +679,68 @@ export const theme = createTheme({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiIconButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: parseInt(t.ButtonBorderRadiusDefault, 10),
|
||||||
|
transition: 'background-color 150ms ease-in-out, box-shadow 150ms ease-in-out',
|
||||||
|
'&:focus-visible': {
|
||||||
|
outline: `2px solid ${t.ColorInteractiveFocus}`,
|
||||||
|
outlineOffset: '2px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sizeSmall: {
|
||||||
|
width: parseInt(t.ButtonHeightSm, 10),
|
||||||
|
height: parseInt(t.ButtonHeightSm, 10),
|
||||||
|
'& .MuiSvgIcon-root': { fontSize: t.ButtonIconSizeSm },
|
||||||
|
},
|
||||||
|
sizeMedium: {
|
||||||
|
width: parseInt(t.ButtonHeightMd, 10),
|
||||||
|
height: parseInt(t.ButtonHeightMd, 10),
|
||||||
|
'& .MuiSvgIcon-root': { fontSize: t.ButtonIconSizeMd },
|
||||||
|
},
|
||||||
|
sizeLarge: {
|
||||||
|
width: parseInt(t.ButtonHeightLg, 10),
|
||||||
|
height: parseInt(t.ButtonHeightLg, 10),
|
||||||
|
'& .MuiSvgIcon-root': { fontSize: t.ButtonIconSizeLg },
|
||||||
|
},
|
||||||
|
colorPrimary: {
|
||||||
|
color: t.ColorInteractiveDefault,
|
||||||
|
'&:hover': { backgroundColor: t.ColorBrand100 },
|
||||||
|
},
|
||||||
|
colorSecondary: {
|
||||||
|
color: t.ColorNeutral600,
|
||||||
|
'&:hover': { backgroundColor: t.ColorNeutral200 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiDivider: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderColor: t.ColorBorderDefault,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiLink: {
|
||||||
|
defaultProps: {
|
||||||
|
underline: 'hover',
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
color: t.ColorTextBrand,
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'color 150ms ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
color: t.ColorInteractiveActive,
|
||||||
|
},
|
||||||
|
'&:focus-visible': {
|
||||||
|
outline: `2px solid ${t.ColorInteractiveFocus}`,
|
||||||
|
outlineOffset: '2px',
|
||||||
|
borderRadius: '2px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiRadio: {
|
MuiRadio: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
|
|||||||
Reference in New Issue
Block a user