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';
|
||||
Reference in New Issue
Block a user