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>
141 lines
5.5 KiB
TypeScript
141 lines
5.5 KiB
TypeScript
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;
|