import { ComponentProps, ReactNode } from 'react';
import { Panel, Table } from '@pantheon-systems/pds-toolkit-react';
import type { BadgeColor } from '@pantheon-systems/pds-toolkit-react';
import type { Decorator, Meta } from '@storybook/react-vite';
import { SlotPlaceholder } from '../_story-helpers/SlotPlaceholder';
import { Milestone, milestoneVariants, MilestoneVariant } from './Milestone';
import {
MilestoneList,
MilestoneListPadding,
milestoneListPaddings,
MilestoneListSubtitlePosition,
milestoneListSubtitlePositions,
} from './MilestoneList';
import './milestone-list.stories.css';
/*
* Story-only. The Milestone page and the Milestone List page share these
* controls, defaults, descriptions and examples, so both pages offer the same
* props and options. Not part of the proposal.
*/
// The package exports the Badge color type but no list of values, so the
// control lists them here; `satisfies` keeps each one a valid color.
const badgeColors = [
'ai-active',
'brand',
'brand-muted',
'critical',
'critical-muted',
'discovery',
'discovery-muted',
'gold',
'gold-muted',
'indigo',
'indigo-muted',
'info',
'info-muted',
'ink',
'ink-muted',
'lime',
'lime-muted',
'orange',
'orange-muted',
'rose',
'rose-muted',
'silver',
'silver-muted',
'sky',
'sky-muted',
'slate',
'slate-muted',
'stone',
'stone-muted',
'success',
'success-muted',
'warning',
'warning-muted',
] as const satisfies ReadonlyArray<BadgeColor>;
/**
* Every prop of the Milestone and the Milestone List, plus `showBadge`, which
* is for the stories only.
*/
export type MilestoneStoryArgs = ComponentProps<typeof Milestone> & {
padding?: MilestoneListPadding;
showBadge?: boolean;
subtitlePosition?: MilestoneListSubtitlePosition;
};
type MilestoneStoryArgTypes = NonNullable<Meta<MilestoneStoryArgs>['argTypes']>;
export const milestoneStoryArgTypes: MilestoneStoryArgTypes = {
badgeColor: {
control: 'select',
description:
'Milestone prop. Type (color) of the Badge beside the subtitle (`info-muted` unless set). Only used when `badgeLabel` is set.',
options: badgeColors,
table: { category: 'Appearance' },
},
badgeLabel: {
control: 'text',
description:
'Milestone prop. Content of a PDS Badge beside the subtitle. The Badge is shown when this is set.',
table: { category: 'Content' },
},
badgeSize: {
control: 'select',
description:
'Milestone prop. Size of the Badge beside the subtitle (`xs` unless set). Only used when `badgeLabel` is set.',
options: ['xs', 's', 'm'],
table: { category: 'Component configuration' },
},
children: {
control: 'text',
description:
'Milestone prop. The whole body under the title: the description, and anything after it (a table, a summary, a form).',
table: { category: 'Content' },
},
className: { table: { category: 'Base attributes' } },
headingLevel: {
control: 'select',
description:
'Milestone prop. Level of the title’s heading element (`3` unless set).',
options: [2, 3, 4, 5, 6],
table: { category: 'Accessibility' },
},
isActive: {
control: 'boolean',
description:
'Milestone prop. Is this the milestone in focus: accent dot with a ring, and `aria-current=’step’`.',
table: { category: 'State' },
},
padding: {
control: 'select',
description:
'Milestone List prop. The list’s own space above the first milestone and below the last (`standard` unless set).',
options: milestoneListPaddings,
table: { category: 'Layout' },
},
showBadge: {
control: 'boolean',
description:
'Story only: show or hide the Badge. In use, a Milestone shows its Badge when `badgeLabel` is set.',
table: { category: 'Content' },
},
subtitle: {
control: 'text',
description:
'Milestone prop. Quiet line in the secondary color: a date, a range, a stage.',
table: { category: 'Content' },
},
subtitlePosition: {
control: 'select',
description:
'Milestone List prop. Where each subtitle goes: `below` the title (default), `above` it, or `start`, left of the line.',
options: milestoneListSubtitlePositions,
table: { category: 'Layout' },
},
title: {
control: 'text',
description: 'Milestone prop. What the milestone is called.',
table: { category: 'Content' },
},
variant: {
control: 'select',
description:
'Milestone List prop (a Milestone can override it): on a `card` (default) or `flat`, with the text alone on the line.',
options: milestoneVariants,
table: { category: 'Component configuration' },
},
};
export const milestoneStoryArgs: MilestoneStoryArgs = {
badgeColor: 'info-muted',
badgeLabel: 'You’re here',
badgeSize: 'xs',
children:
'Before you start, you may want to make a list of all your senior year classes and extracurricular activities, so you’re ready to enter those in your application.',
headingLevel: 3,
isActive: true,
padding: 'standard',
showBadge: true,
subtitle: 'Spring 2022',
subtitlePosition: 'below',
title: 'Organize your info',
variant: 'card',
};
// Args for the stories that show the subtitle above the title, and the slot.
export const milestoneAboveArgs: MilestoneStoryArgs = {
children:
'You can edit each site’s plan and traffic until its switch date, or switch all sites to the same date.',
isActive: true,
showBadge: true,
subtitle: 'Before Feb 3, 2027',
subtitlePosition: 'above',
title: 'Review your new plans',
};
export const milestoneSlotArgs: MilestoneStoryArgs = {
...milestoneAboveArgs,
children: (
<SlotPlaceholder className='milestone-example__slot' label='children' />
),
};
export const milestoneStoryDocs = {
above:
'`subtitlePosition=’above’` puts the subtitle over the title as its eyebrow, and the dot sits on that line. `badgeLabel` adds a PDS Badge beside the subtitle (`badgeColor` and `badgeSize` set its type and size). Shown on cards and flat.',
flat: '`variant=’flat’` leaves the text alone on the line, with no card.',
slot: 'A Milestone’s `children` is the whole body under the title, lined up with it: the description, and anything after it. What goes in it is up to the screen; it only renders when something is passed, and blocks in it sit as far apart as the title and the body. The first milestone shows the empty slot as a placeholder; on the Milestone List page the second is a description with a table below it and the third a description with a summary. Shown on cards and flat.',
start:
'`subtitlePosition=’start’` puts each subtitle at the left of the line, beside the dot, and a Badge stacks under it. Under 480px (a phone) it falls back to below the title.',
};
// The line and the dot belong to a Milestone List. The padding keeps the dots,
// which sit on the line, inside the canvas.
export const withMilestoneCanvas: Decorator<MilestoneStoryArgs> = (Story) => (
<div className='milestone-example__canvas'>{Story()}</div>
);
interface MilestoneStoryExampleProps {
args: MilestoneStoryArgs;
/**
* More milestones after the one the controls change. The Milestone page
* shows the one milestone alone.
*/
siblings?: ReactNode[];
/**
* Overrides `args.variant`, to show a card and a flat one together.
*/
variant?: MilestoneVariant;
}
/**
* Story-only. A Milestone List with the milestone the controls change first,
* then the fixed siblings.
*/
export const MilestoneStoryExample = ({
args,
siblings,
variant,
}: MilestoneStoryExampleProps) => {
const {
badgeLabel,
padding,
showBadge,
subtitlePosition,
variant: argsVariant,
...milestoneArgs
} = args;
return (
<MilestoneList
padding={padding}
subtitlePosition={subtitlePosition}
variant={variant ?? argsVariant}
>
<Milestone
{...milestoneArgs}
badgeLabel={showBadge ? badgeLabel : undefined}
/>
{siblings}
</MilestoneList>
);
};
// Fixed milestones that follow the one the controls change.
export const applicationSiblings = [
<Milestone key='campus' subtitle='Summer 2022' title='Pick your campus'>
You can apply to any campus using one app, and you’ll only pay one
application fee.
</Milestone>,
<Milestone key='scores' subtitle='Fall 2022' title='Send your scores'>
Ask your school to send your test scores before the deadline.
</Milestone>,
];
export const billingSiblings = [
<Milestone
key='invoice'
subtitle='On Feb 3, 2027'
title='Pay your first invoice of $183.70'
>
We charge your Visa ending 4242, before tax. We create a new workspace and a
business account to manage billing.
</Milestone>,
<Milestone
key='monthly'
subtitle='After Mar 20, 2027'
title='Pay for all sites on day 3 of each month'
>
Your current workspace is empty. You can change your plan or credits at any
time in Billing Center.
</Milestone>,
];
const columns = [
{ header: 'Site', id: 'site' },
{ header: 'Switch date', id: 'switchDate' },
{ header: 'First invoice (prorated)', id: 'firstInvoice' },
{ header: 'Then monthly', id: 'monthly' },
];
const rowData = [
{
firstInvoice: '$183.70',
id: 'acme-corp',
monthly: '$200.10 from Mar 3, 2027',
site: 'Acme Corp',
switchDate: 'Feb 3, 2027',
},
{
firstInvoice: '$43.59',
id: 'acme-group',
monthly: '$203.40 from Mar 3, 2027',
site: 'Acme Group',
switchDate: 'Feb 25, 2027',
},
];
const summaryRows = [
{ label: 'Total monthly commitment (before tax)', value: '$587.20' },
{ label: 'Credit for time already paid', value: '-$207.47' },
{ label: 'Bonus', value: '-$20.75' },
];
// The slot examples: a table and a summary box stand in for the screen's own
// content.
export const slotSiblings = [
<Milestone
key='table'
subtitle='From Feb 25, 2027 to Mar 20, 2027'
title='Pay as each site switches'
>
<p>
Sites that haven’t switched keep paying as they do today, then join your
new workspace on their switch date.
</p>
<Table
hasPanel
aria-label='Switch dates and invoices for each site'
columns={columns}
getRowId={(row) => String(row.id)}
rowData={rowData}
/>
</Milestone>,
<Milestone
key='summary'
subtitle='On Feb 3, 2027'
title='Pay your first invoice of $358.98'
>
<p>
We charge your Visa ending 4242, before tax. We create a new workspace and
a business account to manage billing.
</p>
<Panel variant='sunken'>
<dl className='milestone-example__summary'>
{summaryRows.map((row) => (
<div className='milestone-example__summary-row' key={row.label}>
<dt>{row.label}</dt>
<dd>{row.value}</dd>
</div>
))}
<div className='milestone-example__summary-row milestone-example__summary-row--total'>
<dt>Amount due (before tax)</dt>
<dd className='pds-heading-m'>$358.98</dd>
</div>
</dl>
</Panel>
</Milestone>,
];
Milestone
Net-new, in two parts, in their own family so "timeline" keeps one meaning (the log).
Net-new componentOpen in Storybook