Add StepIndicator molecule — horizontal segmented progress bar

- Maps to Figma Progress Bar - Steps (2375:47468)
- Segmented bars: brand gold for completed/current, grey for incomplete
- Current step label bolded, responsive bar height (10px/6px)
- role="navigation" + aria-current="step" for accessibility
- 7 stories: Default, AllStates, TwoSteps, ManySteps, Interactive,
  Completed, NarrowContainer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 20:57:18 +11:00
parent 1e7fbc0dc5
commit 43e7191ead
3 changed files with 280 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { StepIndicator } from './StepIndicator';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
const meta: Meta<typeof StepIndicator> = {
title: 'Molecules/StepIndicator',
component: StepIndicator,
tags: ['autodocs'],
parameters: {
layout: 'centered',
design: {
type: 'figma',
url: 'https://www.figma.com/design/XUDUrw4yMkEexBCCYHXUvT/Parsons?node-id=2375-47468',
},
},
decorators: [
(Story) => (
<Box sx={{ width: 600 }}>
<Story />
</Box>
),
],
};
export default meta;
type Story = StoryObj<typeof StepIndicator>;
// ─── Arrangement Flow Steps ─────────────────────────────────────────────────
const arrangementSteps = [
{ label: 'Details' },
{ label: 'Venue' },
{ label: 'Service' },
{ label: 'Extras' },
{ label: 'Review' },
];
// ─── Default ────────────────────────────────────────────────────────────────
/** Default — 5-step arrangement flow, currently on step 3 */
export const Default: Story = {
args: {
steps: arrangementSteps,
currentStep: 2,
},
};
// ─── All States ─────────────────────────────────────────────────────────────
/** Shows progression through every step position */
export const AllStates: Story = {
render: () => (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{arrangementSteps.map((_, index) => (
<Box key={index}>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
Step {index + 1} of {arrangementSteps.length}
</Typography>
<StepIndicator steps={arrangementSteps} currentStep={index} />
</Box>
))}
</Box>
),
};
// ─── Two Steps ──────────────────────────────────────────────────────────────
/** Minimal — 2-step flow (e.g. choose + confirm) */
export const TwoSteps: Story = {
args: {
steps: [{ label: 'Choose' }, { label: 'Confirm' }],
currentStep: 0,
},
};
// ─── Many Steps ─────────────────────────────────────────────────────────────
/** Maximum — 8-step flow showing label truncation */
export const ManySteps: Story = {
args: {
steps: [
{ label: 'Details' },
{ label: 'Director' },
{ label: 'Venue' },
{ label: 'Service' },
{ label: 'Coffin' },
{ label: 'Extras' },
{ label: 'Payment' },
{ label: 'Review' },
],
currentStep: 3,
},
};
// ─── Interactive ────────────────────────────────────────────────────────────
/** Interactive — navigate forward and back through steps */
export const Interactive: Story = {
render: function Render() {
const [step, setStep] = React.useState(0);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<StepIndicator steps={arrangementSteps} currentStep={step} />
<Box sx={{ p: 3, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
<Typography variant="h5" sx={{ mb: 1 }}>
{arrangementSteps[step].label}
</Typography>
<Typography variant="body2" color="text.secondary">
Step {step + 1} of {arrangementSteps.length} content for this step would appear here.
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
variant="outlined"
color="secondary"
onClick={() => setStep((s) => Math.max(0, s - 1))}
disabled={step === 0}
>
Back
</Button>
<Button
variant="contained"
onClick={() => setStep((s) => Math.min(arrangementSteps.length - 1, s + 1))}
disabled={step === arrangementSteps.length - 1}
>
{step === arrangementSteps.length - 2 ? 'Review' : 'Continue'}
</Button>
</Box>
</Box>
);
},
};
// ─── Completed ──────────────────────────────────────────────────────────────
/** All steps completed — final step active, all bars filled */
export const Completed: Story = {
args: {
steps: arrangementSteps,
currentStep: arrangementSteps.length - 1,
},
};
// ─── Narrow Container ───────────────────────────────────────────────────────
/** Mobile-width container showing responsive sizing */
export const NarrowContainer: Story = {
decorators: [
(Story) => (
<Box sx={{ width: 320 }}>
<Story />
</Box>
),
],
args: {
steps: arrangementSteps,
currentStep: 2,
},
};

View File

@@ -0,0 +1,113 @@
import React from 'react';
import Box from '@mui/material/Box';
import type { SxProps, Theme } from '@mui/material/styles';
import { Typography } from '../../atoms/Typography';
// ─── Types ───────────────────────────────────────────────────────────────────
/** A single step in the progress indicator */
export interface Step {
/** Step label displayed below the bar segment */
label: string;
}
/** Props for the FA StepIndicator molecule */
export interface StepIndicatorProps {
/** Array of steps in order */
steps: Step[];
/** Zero-indexed current step (this step and all before it are filled) */
currentStep: number;
/** MUI sx prop for style overrides */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Horizontal progress bar for multi-step flows in the FA design system.
*
* Shows a segmented bar where completed and current steps are filled with
* the brand colour, and upcoming steps are grey. Each segment has a label
* below it. The current step label is bold for emphasis.
*
* Maps to Figma "Progress Bar - Steps" (2375:47468). Supports 2-8 steps.
*
* Bar height: 10px desktop, 6px mobile (via responsive styles).
* Labels: body2 desktop, caption mobile.
*
* Usage:
* ```tsx
* <StepIndicator
* steps={[
* { label: 'Details' },
* { label: 'Venue' },
* { label: 'Service' },
* { label: 'Extras' },
* { label: 'Review' },
* ]}
* currentStep={2}
* />
* ```
*/
export const StepIndicator = React.forwardRef<HTMLDivElement, StepIndicatorProps>(
({ steps, currentStep, sx }, ref) => {
return (
<Box
ref={ref}
role="navigation"
aria-label="Progress"
sx={[
{
display: 'flex',
gap: { xs: '3px', sm: '5px' },
width: '100%',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{steps.map((step, index) => {
const isCompleted = index < currentStep;
const isCurrent = index === currentStep;
const isFilled = isCompleted || isCurrent;
return (
<Box
key={index}
sx={{ flex: 1, minWidth: 0 }}
aria-current={isCurrent ? 'step' : undefined}
>
{/* Bar segment */}
<Box
sx={{
height: { xs: 6, sm: 10 },
borderRadius: '5px',
bgcolor: isFilled ? 'primary.main' : 'divider',
transition: 'background-color 300ms ease-in-out',
}}
/>
{/* Step label */}
<Typography
variant="body2"
sx={{
mt: { xs: '3px', sm: '5px' },
fontWeight: isCurrent ? 600 : 400,
color: isCurrent ? 'text.primary' : 'text.secondary',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontSize: { xs: '0.625rem', sm: undefined },
}}
>
{step.label}
</Typography>
</Box>
);
})}
</Box>
);
},
);
StepIndicator.displayName = 'StepIndicator';
export default StepIndicator;

View File

@@ -0,0 +1,2 @@
export { StepIndicator } from './StepIndicator';
export type { StepIndicatorProps, Step } from './StepIndicator';