Decisions: Milestone
Append as you go, newest last. Record choices someone could reasonably question later, not every edit.
Its own family, not part of Timeline (2026-10-22)
- Decision:
MilestoneandMilestoneListinproposals/Milestone/. It started asTimelineMilestoneplus avariant='milestone'onTimeline(so the list had two modes) and was split out the same day.TimelineandTimeline Itemare as they were handed off. - Why: too many things called timeline; "timeline" now means the log of events and "milestone" the sequence of steps. The first reason for a separate item was anatomy: a card with a heading, a subtitle and a body, against a one-line row with a status icon and time, with almost no props in common. Splitting the list too keeps the ready-for-dev Timeline free of milestone-only props (
variant,subtitlePosition). Renaming the log (ActivityLog) was rejected: it touches 13 files, including handoffs. - Port impact: two new components. The shared
ol/liand spacing code is duplicated in a small list. Docs say "timeline of milestones" so a search for timeline still finds it.
Answers from Erika (2026-10-22)
Marker state: one boolean,
isActive(notdone/current/upcoming): the milestone in focus. Active has the accent fill and a ring andaria-current='step'; the rest are an empty circle in the line's color. An earlier version had three states and a neutral filled dot; both were replaced.Card: whether there is a card belongs to the list (
variant:cardorflat), and each milestone can override it, so it is not all or nothing. Reaches milestones through a React context. (AcardVariantprop was removed, see below.)Accent: the interactive link color, on the dot only. The subtitle is the default secondary color, not accent.
Heading:
headingLevelprop (default 3); the title ispds-heading-m.Subtitle, not date:
date/dateTimebecamesubtitle(a node), because it cannot always be a date.Subtitle at the start:
subtitlePosition='start'on the list puts it left of the line, baseline-aligned with the title, with a gutter (7xl). Below 480px (phone width) it falls back to under the title; it was 768px at first, which hid the layout in a narrow Storybook pane, so it was lowered (2026-10-22). On the list because the gutter belongs to it; the DOM order is the same either way.Dot placement: the dot sits inside the title (hidden from assistive tech), centered on its first line with
1lh, and is pulled onto the line by the card's border and padding and the list's inset.Active ring stays accent (2026-10-22): the grey ring was compared side by side and rejected. The comparison story and the
--milestone-active-ringvariable are removed.
Default Panel only (2026-10-22)
- Decision: removed
cardVariantfromMilestoneandMilestoneList(and theCardVariantsandMixedCardsstories). The card is the Panel with no variant (default), notraisedas before.hasCardstays (replaced byvariantbelow). - Why: Erika: use the default panel for now, other variants are not needed yet.
- Port impact: smaller API. A
cardVariantpass-through is easy to add back later through the context.
variant: 'card' | 'flat' instead of hasCard (2026-10-22)
- Decision:
hasCardis replaced byvariant(carddefault,flat) on both components; the list sets it and a milestone can override it. The modifier.pds-milestone--cardis unchanged. - Why: PDS uses string-union
variantprops for looks,hasCard={false}is a double negative, and other Panel variants can join the union later. - Port impact: none beyond the rename; nothing outside this proposal used
hasCard.
Subtitle above, list padding, and the body as a slot (2026-11-02)
- Decision:
MilestoneListgetssubtitlePosition='above'(next tobelowandstart) andpadding: 'none' | 'standard'(defaultstandard, as before). The body (children) is documented as a slot, with blocks inside itxsapart. Two new instance stories on both pages (Milestone List and Milestone): Subtitle Above and Slot. The stories do not wrap the list in a Panel (the designer, 2026-11-02);padding='none'is for screens that put the list in a container that already has padding. - Why: a billing-switch screen needs the date as an eyebrow above the title, inside a Panel that already has padding. It was first built as a separate "Milestone Compact"; the only differences from flat Milestone were these two options, so it was folded in rather than kept as a second component.
- Names:
abovepairs withbelowand mirrors ProgressBar'smessagePosition: 'above' | 'below'.paddingfollows PDS's container scale (none/condensed/standard/spacious; Panel'sstandardisspacing-l, the same as the list's own space).density: 'compact'was not used: in PDS it is about cell spacing. - How
aboveworks: the subtitle comes before the title in the DOM and the dot sits on the subtitle's first line; the position reaches each milestone through the list's context. - Subtitle layout: the subtitle is now a flex row with a
2xsgap, so a Badge ("You're here") sits level with a date. Plain text looks the same as before. - Checked:
abovewith the card variant: the dot is on the line and level with the subtitle in all three milestones (measured in Storybook, 2026-11-02). Subtitle Above and Slot pass the a11y panel in light and dark. - Badge is three props (2026-11-02):
badgeLabel,badgeColorandbadgeSizeonMilestonerender a PDS Badge beside the subtitle; it shows whenbadgeLabelis set, so there is nohasBadge(PDS uses ahas…boolean only when the component supplies the content itself, likehasCopyButton). Content, type and size are editable as separate controls. It uses the published PDSBadge, not the local Badge proposal, and the types come from it (BadgeProps['color']).badgeColordefaults toinfo-mutedandbadgeSizetoxs, to sit level with thecopy-xssubtitle. Flat props followSectionMessage(badgeLabel,badgeIcon); a single object likeButtontallywas tried first (abadgeprop) and replaced because separate controls are easier to adjust in Storybook for a proposal. The Badge shares the subtitle line, so it moves withsubtitlePosition. The stories have a story-onlyshowBadgeswitch (a checkbox in Controls) so the Badge can be shown or hidden with one click; it works by passingbadgeLabelor not, so the component's API is unchanged. - Port impact: two new props on
MilestoneList, no change to existing defaults. The class.pds-milestone-list--padding-noneis new. The MilestonevariantandisActiveare unchanged.
Both pages offer the same props and examples (2026-11-02)
- Decision: the Milestone page and the Milestone List page share one set of controls (all 13:
badgeColor,badgeLabel,badgeSize,children,className,headingLevel,isActive,padding, Show Badge,subtitle,subtitlePosition,title,variant; each description says which component the prop belongs to) and the same five examples: Default, Flat, Subtitle At Start, Subtitle Above and Slot. They live inmilestone-story-shared.tsx, so a change reaches both pages. The pages differ only in what they render: the Milestone page shows the one milestone, the Milestone List page shows it with fixed siblings after it. - Why: the designer: what was added should be reflected on both sides. Before, each page had a different set of controls and examples.
- How the list page works: the first milestone is the one the controls change (title, subtitle, body, active, Badge); the rest are fixed. In the stories,
variantsets the list's look. - Port impact: none (stories only).
A Figma component for Milestone (2026-11-02)
- Decision: the
MilestoneFigma page has, next to the captured frames, a sectionMilestone componentwith a real component,Milestone(the item only), built from the code: 24 variants (Variant Card / Flat, State Default / Active, Subtitle Position Below / Above / Start, Badge Without / With) and the properties Title, Subtitle and Body. The card is the PDS Panel (padding=none) behind the content; colors, spacing, radius and stroke widths are bound to PDS variables; the Title usesheading/mand the subtitle and body usecopy/xsandcopy/s. - Badge is a variant, not a switch:
Badge = With / Withoutshows both looks in the component set. A first version had aShow Badgeboolean; it was replaced the same day because the designer wanted with and without visible as a property. The PDS Badge is exposed in the With variants (label, color, size). In code it isbadgeLabel(shown when set); in Figma the variant stands in for that. - Body is a swappable placeholder, not a slot, and it is the whole body: the first version used a Figma slot; the designer asked for a placeholder component to replace.
Bodyis an instance swap, like a slot you replace. It stands for everything under the title, description included, likechildrenin code; there is no separate description property or second slot. Its default isMilestone body(a component set, Extra content Without / With), so a Milestone looks complete as it is: a Description text, with or without a swappable Extra content (a table, a summary, a form) below it, spaced by the PDS xs gap. The PDSPlaceholder content(the placeholder the PDS Panel uses) is a preferred alternative to swap in when the body is not decided. The default was the PDS placeholder at first (the designer asked for a placeholder to replace) and becameMilestone bodythe same day, when the designer asked for the body to be in the item. The Body is an exposed instance, so Description, Extra content (Without / With) and the Extra content swap show on the Milestone instance itself (a top-level Extra content variant was not added: it would double the 24 variants to 48). A Body hugs its content, so a swapped-in body fits. Considered and not done: a Description text property on the Milestone plus a slot below it (it would add adescriptionthat code does not have). - Title font (resolved 2026-11-02): Inter Tight (the font of
heading/m) is not installed where the tool runs, so it was first built with Inter Semi Bold as a stand-in, then restyled by the designer in Figma desktop. The first restyle landed oncopy/m(found on 2026-11-02, after an earlier note saidheading/mwithout the style name being checked); the designer then appliedheading/m, and all 24 Title layers were checked by style name. A tool run here cannot create, copy, restyle or even move a node that contains that text, so the Title layers and anything that moves Milestone instances must be edited in Figma desktop. - Differences from the code: a
padding/Is lastfor stacking items into a list is not modelled (the line belongs to each row), so the Milestone List component draws one continuous line itself (see the next entry). The subtitle in the Start position is lined up with the title by a computed padding (centers), not a CSS baseline. Line heights come from the PDS Figma text styles (automatic), so heights can differ from the browser by a pixel or two. - Port impact: none (Figma only).
Open questions
- The inactive circle and the line are faint (same light grey); darken both through
--milestone-list-line-color? - Narrow phone widths and long subtitles are not checked. Dark mode: a11y panel run, no violations (2026-10-22).
A Figma component for Milestone List (2026-11-02)
- Decision:
Milestone Listis a component set with 12 variants (Variant Card/Flat x Subtitle Position Below/Above/Start x Padding Standard/None), the same props as the code. Each variant holds 3 Milestone instances, set to the list's Variant and Subtitle Position when built; Figma does not pass a list property down, so changing a property on a list instance swaps to the right variant, it does not change rows one by one. - Line: each Milestone row stops at its own height, so the 20px gap (
spacing/L) broke the line. A row's Line stretches with its Rail and cannot be lengthened on an instance. The list now draws one absoluteList linebehind the rows and each row's own Line is hidden (an override). This replaced theShow lineproperty on Milestone that was proposed first; Milestone was not changed. - Rows: exposed instances, so their Badge, Description and Extra content show on the list instance. The titles and dates are the text of existing review rows (billing example). Their Inter Tight text cannot be edited from the tool environment, so changing them is a job for the Figma desktop app. Considered and not done: a slot shell for the designer to fill (an empty list shows nothing to judge).
- Checked: a fresh instance lists Variant, Subtitle Position and Padding; its 3 rows are exposed; switching to Flat / Start / None resizes it; the Title layers use
heading/m.
One Figma page for Milestone (2026-11-02)
- Decision: the designer moved the new frames and components onto the original
Milestonepage (458:8498), and the temporary pageMilestone (2026-11-02)was deleted. The original four frames from 2026-10-22 are kept in a sectionOld.figma.jsonhas one page again (noadditionalPages), with the ten new frame ids and the new section id (608:1716); the component sets kept their ids. Checked after the move: instances still point to the movedMilestoneset, and fresh instances of all three sets list their properties.
Long titles and dates (2026-11-02)
- Checked in Storybook (built static copy, a long title and a long range date with a long Badge label, at desktop and 375px): a long title and a long date wrap inside the card; at
startthe subtitle wraps inside its 96px column (7xl) and the Badge stays inside the page. One word with no spaces, longer than the card, ran out of the card, so the title now hasoverflow-wrap: anywhere. - Figma: in the 8 Start variants of
Milestonethe Subtitle was a hugging single line in a fixed 112px gutter, so a date longer than about 96px stuck out to the left (clipped in a screenshot). The Subtitle now fills the gutter (96px plus the 16px padding), wraps and is right-aligned, the same as the code. - Not checked: a Badge label wider than the 96px column at
start(it sits to the left of the line and is not clipped, but it is wider than the date above it), and very long text inside a slot.