← 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/decisions.md83 lines · View on GitHub

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: Milestone and MilestoneList in proposals/Milestone/. It started as TimelineMilestone plus a variant='milestone' on Timeline (so the list had two modes) and was split out the same day. Timeline and Timeline Item are 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/li and 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 (not done / current / upcoming): the milestone in focus. Active has the accent fill and a ring and aria-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: card or flat), and each milestone can override it, so it is not all or nothing. Reaches milestones through a React context. (A cardVariant prop was removed, see below.)

  • Accent: the interactive link color, on the dot only. The subtitle is the default secondary color, not accent.

  • Heading: headingLevel prop (default 3); the title is pds-heading-m.

  • Subtitle, not date: date / dateTime became subtitle (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-ring variable are removed.

Default Panel only (2026-10-22)

  • Decision: removed cardVariant from Milestone and MilestoneList (and the CardVariants and MixedCards stories). The card is the Panel with no variant (default), not raised as before. hasCard stays (replaced by variant below).
  • Why: Erika: use the default panel for now, other variants are not needed yet.
  • Port impact: smaller API. A cardVariant pass-through is easy to add back later through the context.

variant: 'card' | 'flat' instead of hasCard (2026-10-22)

  • Decision: hasCard is replaced by variant (card default, flat) on both components; the list sets it and a milestone can override it. The modifier .pds-milestone--card is unchanged.
  • Why: PDS uses string-union variant props 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: MilestoneList gets subtitlePosition='above' (next to below and start) and padding: 'none' | 'standard' (default standard, as before). The body (children) is documented as a slot, with blocks inside it xs apart. 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: above pairs with below and mirrors ProgressBar's messagePosition: 'above' | 'below'. padding follows PDS's container scale (none / condensed / standard / spacious; Panel's standard is spacing-l, the same as the list's own space). density: 'compact' was not used: in PDS it is about cell spacing.
  • How above works: 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 2xs gap, so a Badge ("You're here") sits level with a date. Plain text looks the same as before.
  • Checked: above with 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, badgeColor and badgeSize on Milestone render a PDS Badge beside the subtitle; it shows when badgeLabel is set, so there is no hasBadge (PDS uses a has… boolean only when the component supplies the content itself, like hasCopyButton). Content, type and size are editable as separate controls. It uses the published PDS Badge, not the local Badge proposal, and the types come from it (BadgeProps['color']). badgeColor defaults to info-muted and badgeSize to xs, to sit level with the copy-xs subtitle. Flat props follow SectionMessage (badgeLabel, badgeIcon); a single object like Button tally was tried first (a badge prop) and replaced because separate controls are easier to adjust in Storybook for a proposal. The Badge shares the subtitle line, so it moves with subtitlePosition. The stories have a story-only showBadge switch (a checkbox in Controls) so the Badge can be shown or hidden with one click; it works by passing badgeLabel or 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-none is new. The Milestone variant and isActive are 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 in milestone-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, variant sets the list's look.
  • Port impact: none (stories only).

A Figma component for Milestone (2026-11-02)

  • Decision: the Milestone Figma page has, next to the captured frames, a section Milestone component with 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 uses heading/m and the subtitle and body use copy/xs and copy/s.
  • Badge is a variant, not a switch: Badge = With / Without shows both looks in the component set. A first version had a Show Badge boolean; 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 is badgeLabel (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. Body is an instance swap, like a slot you replace. It stands for everything under the title, description included, like children in code; there is no separate description property or second slot. Its default is Milestone 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 PDS Placeholder 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 became Milestone body the 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 a description that 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 on copy/m (found on 2026-11-02, after an earlier note said heading/m without the style name being checked); the designer then applied heading/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 last for 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 List is 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 absolute List line behind the rows and each row's own Line is hidden (an override). This replaced the Show line property 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 Milestone page (458:8498), and the temporary page Milestone (2026-11-02) was deleted. The original four frames from 2026-10-22 are kept in a section Old. figma.json has one page again (no additionalPages), 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 moved Milestone set, 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 start the 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 has overflow-wrap: anywhere.
  • Figma: in the 8 Start variants of Milestone the 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.