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,77 @@
import type { Meta, StoryObj } from '@storybook/react';
import Box from '@mui/material/Box';
import { ClusterMarker } from './ClusterMarker';
const meta: Meta<typeof ClusterMarker> = {
title: 'Atoms/ClusterMarker',
component: ClusterMarker,
tags: ['autodocs'],
parameters: {
layout: 'centered',
backgrounds: {
default: 'map',
values: [{ name: 'map', value: '#E5E3DF' }],
},
},
argTypes: {
onClick: { action: 'clicked' },
},
};
export default meta;
type Story = StoryObj<typeof ClusterMarker>;
/** Cluster containing at least one verified provider — promoted palette */
export const MixedOrVerified: Story = {
args: {
count: 5,
hasVerified: true,
},
};
/** Cluster of all-unverified providers — neutral palette */
export const AllUnverified: Story = {
args: {
count: 3,
hasVerified: false,
},
};
/** Small cluster — pair of providers */
export const Pair: Story = {
args: {
count: 2,
hasVerified: true,
},
};
/** Large cluster — double-digit count */
export const LargeCluster: Story = {
args: {
count: 27,
hasVerified: true,
},
};
/** Side-by-side comparison — verified vs unverified at various counts */
export const PaletteGrid: Story = {
render: () => (
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 6,
p: 4,
}}
>
<ClusterMarker count={2} hasVerified />
<ClusterMarker count={5} hasVerified />
<ClusterMarker count={12} hasVerified />
<ClusterMarker count={99} hasVerified />
<ClusterMarker count={2} />
<ClusterMarker count={5} />
<ClusterMarker count={12} />
<ClusterMarker count={99} />
</Box>
),
};

View File

@@ -0,0 +1,161 @@
import React from 'react';
import Box from '@mui/material/Box';
import type { SxProps, Theme } from '@mui/material/styles';
// ─── Types ──────────────────────────────────────────────────────────────────
/** Props for the FA ClusterMarker atom */
export interface ClusterMarkerProps {
/** Number of providers in this cluster */
count: number;
/** True if any provider in the cluster is verified — drives the promoted palette */
hasVerified?: boolean;
/** Click handler — opens the cluster popup */
onClick?: (e: React.MouseEvent) => void;
/** MUI sx prop for the root element */
sx?: SxProps<Theme>;
}
// ─── Constants ──────────────────────────────────────────────────────────────
const NUB_SIZE = 'var(--fa-map-pin-nub-size)';
const BADGE_SIZE = 36;
// ─── Colour sets — matches MapPin ───────────────────────────────────────────
const colours = {
verified: {
bg: 'var(--fa-color-brand-700)',
text: 'var(--fa-color-white)',
border: 'var(--fa-color-brand-700)',
nub: 'var(--fa-color-brand-700)',
},
unverified: {
bg: 'var(--fa-color-neutral-100)',
text: 'var(--fa-color-neutral-800)',
border: 'var(--fa-color-neutral-300)',
nub: 'var(--fa-color-neutral-100)',
},
} as const;
// ─── Component ──────────────────────────────────────────────────────────────
/**
* Cluster map marker for the FA design system.
*
* Circular pill with a count, representing N provider pins grouped at the
* same screen location. Sibling to `MapPin` — same palette language (verified
* promoted, unverified neutral), same nub treatment, same shadow.
*
* `hasVerified` drives the palette: if *any* provider in the cluster is
* verified, the cluster adopts the promoted (brand-700) palette. All-unverified
* clusters use the neutral palette.
*
* Designed for use as the `render`-ed output of `@googlemaps/markerclusterer`.
* Pure CSS + SVG — no canvas. role="button" + keyboard + focus ring.
*
* Usage:
* ```tsx
* <ClusterMarker count={5} hasVerified onClick={...} />
* <ClusterMarker count={12} />
* ```
*/
export const ClusterMarker = React.forwardRef<HTMLDivElement, ClusterMarkerProps>(
({ count, hasVerified = false, onClick, sx }, ref) => {
const palette = hasVerified ? colours.verified : colours.unverified;
const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.key === 'Enter' || e.key === ' ') && onClick) {
e.preventDefault();
onClick(e as unknown as React.MouseEvent);
}
};
const label = `${count} providers in this area`;
return (
<Box
ref={ref}
role="button"
tabIndex={0}
aria-label={label}
onClick={onClick}
onKeyDown={handleKeyDown}
sx={[
{
display: 'inline-flex',
flexDirection: 'column',
alignItems: 'center',
cursor: 'pointer',
transition: 'transform 150ms ease-in-out',
// Fade in on mount — matches MapPin and popups for a consistent
// entry timing across the map.
'@keyframes clusterMarkerIn': {
from: { opacity: 0 },
to: { opacity: 1 },
},
animation: 'clusterMarkerIn 180ms ease-out',
'&:hover': { transform: 'scale(1.08)' },
'&:focus-visible': {
outline: 'none',
'& > .ClusterMarker-badge': {
outline: '2px solid var(--fa-color-interactive-focus)',
outlineOffset: '2px',
},
},
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{/* Circular badge */}
<Box
className="ClusterMarker-badge"
sx={{
width: BADGE_SIZE,
height: BADGE_SIZE,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: palette.bg,
border: '1px solid',
borderColor: palette.border,
boxShadow: 'var(--fa-shadow-sm)',
color: palette.text,
fontFamily: 'var(--fa-font-family-body)',
fontSize: 14,
fontWeight: 700,
lineHeight: 1,
}}
>
{count}
</Box>
{/* Nub — same SVG pattern as MapPin for visual continuity */}
<svg
aria-hidden
viewBox="0 0 16 8"
style={{
display: 'block',
width: `calc(2 * ${NUB_SIZE})`,
height: NUB_SIZE,
marginTop: '-1px',
overflow: 'visible',
}}
>
<path d="M 0 -3 L 16 -3 L 16 0 L 8 8 L 0 0 Z" fill={palette.bg} />
<path
d="M 0 0 L 8 8 L 16 0"
fill="none"
stroke={palette.border}
strokeWidth={1}
strokeLinejoin="round"
/>
</svg>
</Box>
);
},
);
ClusterMarker.displayName = 'ClusterMarker';
export default ClusterMarker;

View File

@@ -0,0 +1 @@
export { ClusterMarker, type ClusterMarkerProps } from './ClusterMarker';