Initial commit: FA Design System source files
Copy of the Funeral Arranger design system components, theme, tokens, and Storybook config from the original Parsons project. Pre-upgrade baseline with React 18, MUI v5, Storybook 8. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
140
src/components/atoms/Badge/Badge.tsx
Normal file
140
src/components/atoms/Badge/Badge.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import type { BoxProps } from '@mui/material/Box';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Colour intent for the badge */
|
||||
export type BadgeColor = 'default' | 'brand' | 'success' | 'warning' | 'error' | 'info';
|
||||
|
||||
/** Props for the FA Badge component */
|
||||
export interface BadgeProps extends Omit<BoxProps, 'color'> {
|
||||
/** Colour intent */
|
||||
color?: BadgeColor;
|
||||
/** Visual style: "filled" (solid background) or "soft" (tonal/subtle background) */
|
||||
variant?: 'filled' | 'soft';
|
||||
/** Size preset */
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
/** Optional leading icon */
|
||||
icon?: React.ReactNode;
|
||||
/** Label text */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// ─── Colour maps ─────────────────────────────────────────────────────────────
|
||||
|
||||
const filledColors: Record<BadgeColor, { bg: string; text: string }> = {
|
||||
default: { bg: 'var(--fa-color-neutral-700)', text: 'var(--fa-color-white)' },
|
||||
brand: { bg: 'var(--fa-color-interactive-default)', text: 'var(--fa-color-white)' },
|
||||
success: { bg: 'var(--fa-color-feedback-success)', text: 'var(--fa-color-white)' },
|
||||
warning: { bg: 'var(--fa-color-feedback-warning)', text: 'var(--fa-color-white)' },
|
||||
error: { bg: 'var(--fa-color-feedback-error)', text: 'var(--fa-color-white)' },
|
||||
info: { bg: 'var(--fa-color-feedback-info)', text: 'var(--fa-color-white)' },
|
||||
};
|
||||
|
||||
const softColors: Record<BadgeColor, { bg: string; text: string }> = {
|
||||
default: { bg: 'var(--fa-color-neutral-200)', text: 'var(--fa-color-neutral-700)' },
|
||||
brand: { bg: 'var(--fa-color-brand-200)', text: 'var(--fa-color-brand-700)' },
|
||||
success: {
|
||||
bg: 'var(--fa-color-feedback-success-subtle)',
|
||||
text: 'var(--fa-color-feedback-success)',
|
||||
},
|
||||
warning: { bg: 'var(--fa-color-feedback-warning-subtle)', text: 'var(--fa-color-text-warning)' },
|
||||
error: { bg: 'var(--fa-color-feedback-error-subtle)', text: 'var(--fa-color-feedback-error)' },
|
||||
info: { bg: 'var(--fa-color-feedback-info-subtle)', text: 'var(--fa-color-feedback-info)' },
|
||||
};
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Status indicator label for the FA design system.
|
||||
*
|
||||
* Pill-shaped, display-only badge for communicating status, category,
|
||||
* or emphasis. Used in PriceCard ("Popular"), ServiceOption ("Included"),
|
||||
* and other contexts.
|
||||
*
|
||||
* Colour options:
|
||||
* - `default` — neutral grey (general labels)
|
||||
* - `brand` — warm gold/copper (promoted, featured)
|
||||
* - `success` — green (confirmed, included, available)
|
||||
* - `warning` — amber (limited, expiring, attention)
|
||||
* - `error` — red (sold out, unavailable, urgent)
|
||||
* - `info` — blue (new, updated, informational)
|
||||
*
|
||||
* Variant options:
|
||||
* - `soft` (default) — tonal background, coloured text. Calmer, preferred for FA.
|
||||
* - `filled` — solid background, white text. For high-priority emphasis.
|
||||
*
|
||||
* **Accessibility**: If a Badge contains only an icon (no text children),
|
||||
* provide an `aria-label` prop so screen readers can announce the status.
|
||||
*/
|
||||
export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
({ color = 'default', variant = 'soft', size = 'medium', icon, children, sx, ...props }, ref) => {
|
||||
const sizeMap = {
|
||||
small: {
|
||||
height: 'var(--fa-badge-height-sm)',
|
||||
px: 'var(--fa-badge-padding-x-sm)',
|
||||
fontSize: 'var(--fa-badge-font-size-sm)',
|
||||
iconSize: 'var(--fa-badge-icon-size-sm)',
|
||||
},
|
||||
medium: {
|
||||
height: 'var(--fa-badge-height-md)',
|
||||
px: 'var(--fa-badge-padding-x-md)',
|
||||
fontSize: 'var(--fa-badge-font-size-md)',
|
||||
iconSize: 'var(--fa-badge-icon-size-md)',
|
||||
},
|
||||
large: {
|
||||
height: 'var(--fa-badge-height-lg)',
|
||||
px: 'var(--fa-badge-padding-x-lg)',
|
||||
fontSize: 'var(--fa-badge-font-size-lg)',
|
||||
iconSize: 'var(--fa-badge-icon-size-lg)',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const s = sizeMap[size];
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
component="span"
|
||||
sx={[
|
||||
(theme: Theme) => {
|
||||
const colors = variant === 'filled' ? filledColors[color] : softColors[color];
|
||||
|
||||
return {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--fa-badge-icon-gap-default)',
|
||||
minHeight: s.height,
|
||||
px: s.px,
|
||||
borderRadius: 'var(--fa-badge-border-radius-default)',
|
||||
backgroundColor: colors.bg,
|
||||
color: colors.text,
|
||||
fontSize: s.fontSize,
|
||||
fontWeight: 600,
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
lineHeight: 1,
|
||||
letterSpacing: '0.02em',
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none',
|
||||
// Icon sizing
|
||||
'& > .MuiSvgIcon-root, & > svg': {
|
||||
fontSize: s.iconSize,
|
||||
flexShrink: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Badge.displayName = 'Badge';
|
||||
export default Badge;
|
||||
Reference in New Issue
Block a user