import { useEffect, useState } from 'react';
import {
Button,
CodeBlock,
FlexContainer,
Tag,
} from '@pantheon-systems/pds-toolkit-react';
import { Meta, StoryObj } from '@storybook/react-vite';
import { ThinkingState } from '../ThinkingState/ThinkingState';
import { SlotPlaceholder } from '../_story-helpers/SlotPlaceholder';
import { Timeline } from './Timeline';
import { TimelineItem } from './TimelineItem';
const meta: Meta<typeof Timeline> = {
tags: ['new-pattern', 'ready-for-dev'],
title: 'Component Proposals/Timeline/Timeline',
component: Timeline,
parameters: {
docs: {
description: {
component:
'**New pattern:** not in PDS v2 today. A list of events over time. It owns the list semantics, the spacing and an optional rail that joins the icons of neighbouring Timeline Items. Dividers, a Chain of Thought or a Thinking State can sit between items.',
},
},
},
argTypes: {
children: { control: false, table: { category: 'Content' } },
className: { table: { category: 'Base attributes' } },
hasRail: {
control: 'boolean',
table: { category: 'Component configuration' },
},
size: {
control: 'select',
options: ['xs', 's'],
table: { category: 'Component configuration' },
},
},
args: {
hasRail: true,
},
render: (args) => (
<Timeline {...args}>
<TimelineItem
label='Verified the finding still applies'
status='done'
time='01:00 AM'
/>
<TimelineItem
label='Patch available upstream; no custom code touches this plugin'
status='done'
time='01:00 AM'
/>
<TimelineItem
iconName='codeBranch'
label='Opened a pull request with the update for 5 sites'
time='01:01 AM'
/>
<TimelineItem
label='Ran automated checks: build, smoke tests and visual regression'
status='done'
time='01:05 AM'
/>
<TimelineItem
label='Checks failed on 1 canary site'
status='error'
time='01:07 AM'
/>
<TimelineItem
label='Rolling out to the remaining site'
status='active'
time='now'
/>
</Timeline>
),
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
// A run of five steps. While a step is active it shows its slot; between steps
// a Thinking State shows. Each slot is owned by the screen.
const progressionSteps = [
{
label: 'Reading the failed build logs and error trace',
slot: (
<FlexContainer flexDirection='column' gap='xs'>
<CodeBlock
code={
'ERROR: Your requirements could not be resolved to an installable set of packages.\n drupal/pathauto is locked to ^1.11 and an update was not requested.'
}
hasCopyButton={false}
language='bash'
/>
<FlexContainer gap='xs'>
<Tag size='xs' tagColor='slate' tagLabel='composer.json' />
<Tag size='xs' tagColor='slate' tagLabel='composer.lock' />
</FlexContainer>
</FlexContainer>
),
},
{
label: 'Comparing composer.json against the last known-good commit',
slot: (
<CodeBlock
code={'- "drupal/pathauto": "^1.11"\n+ "drupal/pathauto": "^1.13"'}
hasCopyButton={false}
language='diff'
/>
),
},
{
label: 'Isolating the root cause',
slot: <SlotPlaceholder label='children: root cause callout' />,
},
{
label: 'Forking a sandbox from Dev to reproduce the failure safely',
slot: <SlotPlaceholder label='children: Dev to Sandbox diagram' />,
},
{
label: 'Applying a fix and re-running the full build',
slot: <SlotPlaceholder label='children: before and after screenshots' />,
},
];
const thinkingLabels = ['Thinking...', 'Tinkering...', 'Zappying...'];
// Moment k: even k is step k / 2 working (with its slot); odd k is the pause
// after that step, with a Thinking State. The last moment is all steps done.
const lastMoment = progressionSteps.length * 2 - 1;
// How long each moment shows, in milliseconds.
const stepDelay = 1500;
const ProgressionDemo = () => {
const [moment, setMoment] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
// Advance one moment at a time until the run is done.
useEffect(() => {
if (!isPlaying) return;
if (moment === lastMoment) {
setIsPlaying(false);
return;
}
const timer = setTimeout(() => setMoment((m) => m + 1), stepDelay);
return () => clearTimeout(timer);
}, [isPlaying, moment]);
const isPaused = moment % 2 === 1;
const activeIndex = isPaused ? -1 : moment / 2;
const visibleCount = isPaused ? (moment + 1) / 2 : activeIndex + 1;
return (
<FlexContainer flexDirection='column' gap='l'>
<FlexContainer gap='s'>
<Button
disabled={isPlaying || moment === lastMoment}
label='Play'
onClick={() => setIsPlaying(true)}
variant='primary'
/>
<Button
label='Restart'
onClick={() => {
setIsPlaying(false);
setMoment(0);
}}
variant='secondary'
/>
</FlexContainer>
<Timeline size='s'>
{[
...progressionSteps.slice(0, visibleCount).map((step, index) => (
<TimelineItem
key={step.label}
label={step.label}
status={index === activeIndex ? 'active' : 'done'}
>
{index === activeIndex ? step.slot : undefined}
</TimelineItem>
)),
...(isPaused
? [
<ThinkingState
key='thinking'
label={
thinkingLabels[((moment - 1) / 2) % thinkingLabels.length]
}
size='s'
/>,
]
: []),
]}
</Timeline>
</FlexContainer>
);
};
export const Progression: Story = {
parameters: {
controls: { disable: true },
docs: {
description: {
story:
'A run from start to finish. Press Play to run it and Restart to go back to the start. A step that is working shows its slot (a log, a diff, a callout, a diagram, screenshots); finished steps collapse to one line; between steps a Thinking State sits at the end of the list. What goes in each slot is up to the screen.',
},
},
},
render: () => <ProgressionDemo />,
};
Timeline
Net-new, in two parts.
Net-new componentOpen in Storybook