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