← All proposals

Milestone

Net-new, in two parts, in their own family so "timeline" keeps one meaning (the log).

Net-new componentOpen in Storybook

proposals/Milestone/milestone-story-shared.tsx356 lines · View on GitHub
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>,
];