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:
2026-05-22 13:12:45 +10:00
commit 4cafd84142
2555 changed files with 40558 additions and 0 deletions

View 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;