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:
165
src/components/molecules/StepIndicator/StepIndicator.stories.tsx
Normal file
165
src/components/molecules/StepIndicator/StepIndicator.stories.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
113
src/components/molecules/StepIndicator/StepIndicator.tsx
Normal file
113
src/components/molecules/StepIndicator/StepIndicator.tsx
Normal 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;
|
||||
2
src/components/molecules/StepIndicator/index.ts
Normal file
2
src/components/molecules/StepIndicator/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { StepIndicator } from './StepIndicator';
|
||||
export type { StepIndicatorProps, Step } from './StepIndicator';
|
||||
Reference in New Issue
Block a user