import {
Children,
ComponentPropsWithoutRef,
isValidElement,
ReactNode,
} from 'react';
import type { MilestoneVariant } from './Milestone';
import { MilestoneListContext } from './milestone-list-context';
import './milestone-list.css';
export const milestoneListPaddings = ['none', 'standard'] as const;
export type MilestoneListPadding = (typeof milestoneListPaddings)[number];
export const milestoneListSubtitlePositions = [
'above',
'below',
'start',
] as const;
export type MilestoneListSubtitlePosition =
(typeof milestoneListSubtitlePositions)[number];
export interface MilestoneListProps extends ComponentPropsWithoutRef<'ol'> {
/**
* The milestones. Each child becomes a list item.
*/
children?: ReactNode;
/**
* Additional class names
*/
className?: string;
/**
* Space above the first milestone and below the last. `standard` keeps the
* list's own breathing room; `none` removes it, for a list that sits in a
* container (a Panel) that already has padding.
*/
padding?: MilestoneListPadding;
/**
* Where each milestone's subtitle goes: `below` the title (default),
* `above` it, or `start`, at the left of the line, beside the dot. At
* narrow widths `start` falls back to below the title.
*/
subtitlePosition?: MilestoneListSubtitlePosition;
/**
* Put each milestone on a card (`card`, default) or leave the text on the
* line (`flat`). A milestone can override it.
*/
variant?: MilestoneVariant;
}
// Stand-in for the v2 `mergeClasses` util, which the published package does
// not export. The port uses the real one.
const mergeClasses = (classes: Array<string | null | undefined>) =>
classes.filter(Boolean).join(' ');
/**
* MilestoneList UI component. A list of `Milestone`s hung off one line that
* runs the full height of the list. It owns the list semantics, the spacing,
* the line and the card style that milestones follow.
*/
export const MilestoneList = ({
children,
className,
padding = 'standard',
subtitlePosition = 'below',
variant,
...props
}: MilestoneListProps) => {
const baseClass = 'pds-milestone-list';
const nodes = Children.toArray(children);
const paddingClass = padding === 'none' ? `${baseClass}--padding-none` : null;
const subtitleClass =
subtitlePosition === 'start' ? `${baseClass}--subtitle-start` : null;
return (
<MilestoneListContext.Provider value={{ subtitlePosition, variant }}>
<ol
className={mergeClasses([
baseClass,
paddingClass,
subtitleClass,
className,
])}
{...props}
>
{nodes.map((child, index) => (
<li
className={`${baseClass}__entry`}
key={isValidElement(child) ? (child.key ?? index) : index}
>
{child}
</li>
))}
</ol>
</MilestoneListContext.Provider>
);
};
Milestone
Net-new, in two parts, in their own family so "timeline" keeps one meaning (the log).
Net-new componentOpen in Storybook