Decisions
- Its own component, not a variant of the agentic panel. The other design in the same set is a full-width investigation report on a page (Panel with an agentic border: title, summary, sections, a code diff, Discard / Approve). This is a decision prompt in the message stream: title, one-line summary, changes, Accept / Refine / Dismiss. Their anatomy and lifecycle differ; the card outlives the decision in the history and has resolved states. They share only small parts (Buttons, check lines, a file-diff row), not a component.
- Built on the PDS
Panel(default variant,condensedpadding) for the border, radius and surface, so no container is redrawn. Everything sits in one inner wrapper so the Panel's own first-child heading rules don't resize the title. - Props:
title(required),description,children(the changes),actions(slot),state,statusLabel,headingLevel. No text is hardcoded; the buttons and labels all come from the screen. state:pending|accepted.pendingshowsactions.acceptedreplaces them with a result line (success-colored check plusstatusLabel), inside arole='status'region that is always in the DOM, so the change is announced. The changes stay visible. Erika's call: there is an accepted state; its final look is to be refined later.- Dismissed is designed (see the
dismissedentry at the end). Still open: an expired or superseded state, for a pending card the conversation moved on from. - Open: Refine. The card only holds the button (in
actionsStart). What it does (focus the chat input, open a field) is the screen's. SuggestionChange: a field label over a before and after pair, as applus adl(dtfor the heading,ddfor the value).beforeLabelandafterLabelare required strings because the words are translatable. An empty value is passed as a dash by the consumer. Before is in--pds-color-status-critical-foreground, after in--pds-color-status-success-foreground(semibold), headings uppercased with CSS (--pds-typography-ls-l). The text labels carry the meaning, not only color. Columns are 1fr / 2fr (a guess from the screenshot).- Open: Accept style. Accept is a plain primary button here; the panel's Approve has the agentic gradient. Gradient only when applying something to the site?
- Open: many changes. No height cap or scroll; the chat scrolls.
- Open: text-only changes. A code-diff change (the panel's file row with
CodeBlock) is not built here. - Heading is a
h3by default;headingLevelchanges it. The card is meant to sit inside an agentChatMessage; the Chat Panel's With Sample Content story shows it in a conversation (theWith ChangesandIn Chatstories were removed on 2026-10-19). - Storybook:
Proposals/Suggestion Card,new-pattern. Uses the local Icon wrapper (forcheck) and, in stories, the local Button and ChatMessage. - The content area is a slot, shown as a placeholder in Storybook (the same dashed
childrenbox the PDS stories use, via the story-onlyproposals/_story-helpers/SlotPlaceholder; its class has nopds-prefix so Annotate ignores it). Erika's call: what goes in the card is the screen's.SuggestionChangestays as an optional helper, shown in theWithChangesstory; drop it if the before and after rows are not part of this proposal. Open: should the card still own spacing between its children, or only the frame? padding: the same options asPanel(none,condensed,standard,spacious), passed straight to the Panel. Default iscondensed(Panel's own default isstandard) because the card sits in a chat column. The type comes from Panel's props, so the port follows if Panel's options change.- Footer wrapper for the live region. The empty
role='status'region has to stay in the DOM so the accepted message is announced. As a direct child of the flex column body it added an invisible gap below the actions (extra bottom space). The actions and the status region now share one__footerblock, so there is one gap, not two. - Fills its container, like a Panel. It is a Panel, so it is a block at full width and sets no width of its own; the chat column decides. The stories no longer cap the width, except
InChat, which uses a narrow column (22rem) to imitate a chat panel. - Space above the buttons is
--pds-spacing-m(16px), larger than the--pds-spacing-s(12px) between the other sections, so the decision row reads as its own area. Set with component variables--suggestion-card-gapand--suggestion-card-actions-gap; the footer margin makes up the difference. Tried on Erika's request; revert by setting the actions gap tos. buttonSize:xs|s(defaults) for the whole button set. The buttons are consumer content in a slot, so the card setssizeon everyButtoninactions(looking inside fragments) withcloneElement, overriding any size on them; other elements are left alone. The stories no longer set sizes on their buttons. Heights:xs28px,s32px. Trade-off: the card inspects its slot, which is unusual for PDS; the port can instead pass the size down through context. Open: ismever needed?- Title is Heading M: the PDS
pds-heading-mutility class (16px, semibold, compact heading font, 130% line height) on the heading element, not custom font rules. Erika's call; it was 14px before.headingLevelstill only changes the tag. - Description is Copy S: the PDS
pds-copy-sutility class (14px, regular, default font, 150% line height) on the description, kept in the secondary text color. Erika's call. Before, it used the same values written out by hand. - In a chat, the card sits 20px (
--pds-spacing-l) below the agent's text, the same as the gap between turns.ChatMessagedoesn't space the blocks inside an agent message (the markdown renderer does), so theInChatstory sets it with aFlexContainer. buttonSizedefaults toxs(Erika's call; it wass).s(32px) is the option for a roomier card;xsis 28px. It is a prop with a control in Storybook, so there is no separate story for it.headingSize:m|l(defaultm), the PDS heading classespds-heading-m(16px) andpds-heading-l(18px). Separate fromheadingLevel(the tag). PDS has no heading class smaller than M, andxland up are too large for a card in a chat, so only these two.
Semantic type classes instead of font primitives (2026-10-19)
Decision: the root now takes the PDS semantic
pds-copy-*class in its markup instead of declaringfont-family,font-sizeandline-heightfrom the typography primitives. Colors were already semantic tokens (foreground, surface, border, status, interactive).Why: The designer asked for semantic tokens over primitives where they exist. The package has no semantic font token; the semantic layer is the
pds-copy-*utility classes. Computed values are unchanged (Inter, 14px/21px ats).Port impact: the
pds-copy-*classes are!important, so a CSS rule cannot override size, weight or line height on that element. Components that size text through a CSS variable or override weight on the same element (ChainOfThought, Timeline, TimelineItem, LabeledDivider, ChatInput, IdentityBlock) keep the primitives for now.Title to description gap is
--pds-spacing-xs(2026-10-19). The title and description are in one__headinggroup with their own gap; the sections of the card (group, changes, footer) keep the--pds-spacing-sbody gap. Title is Heading M and the description Copy S in the secondary color, as already decided (confirmed from the designer's spec). Port impact: adds the__headingwrapper div.Optional
feedbackslot in the accepted state (2026-10-19, superseded byactionsEndabove). Shown at the end of the result line ("Changes applied" on the left, the slot on the right) only whilestateisaccepted; it takes aFeedbackBarbut any node. The card holds no feedback logic, as withactions. The accepted moment is when a person has seen the result, so it is where the question is most useful. The Feedback Bar is only the bare thumbs (the bordered prompt bar was dropped). Port impact: adds thefeedbackprop and a__feedbackpart; the footer is now a wrapping flex row.actionsandfeedbackbecameactionsStartandactionsEnd(2026-10-19). Two optional footer slots, named like Chat Input's:actionsStartat the left (the decision buttons, replacingactions) andactionsEndat the right (replacing the accepted-onlyfeedback). Both are optional; with neither, the footer takes no space (the empty status region stays in the DOM so the result is still announced).actionsStartshows whilependingand is replaced by the result line once accepted or dismissed.actionsEndshows in every state, so the screen decides what goes there in each (the Chat with an agent recipe shows the Feedback Bar there after Accept and Undo after Dismiss).buttonSizeapplies to the buttons inactionsStartonly. The footer wraps on a narrow card. The designer's call: a button group at the left and, for example, a Feedback Bar at the right, without the card knowing what the right slot is. Port impact: the props areactionsStart,actionsEndandbuttonSize; there is noactionsorfeedback.A
dismissedstate (2026-10-19).stateispending,acceptedordismissed. Dismissed hides the changes, so the card shrinks to a quiet record: the title and description stay, the title goes secondary, andactionsStartis replaced by the result line (statusLabel, for example "Dismissed") with acircleMinusin the secondary color (it was an xmark; changed at the designer's request), where accepted has a check in the success color. The screen puts an Undo button inactionsEndto bring the card back. Removing the card would erase what the agent proposed, and a fully expanded card with live-looking buttons would be wrong after a dismiss; Undo beats a confirmation dialog for a low-risk action. OnestatusLabelcarries the text for both finished states. The card does not know about Undo or focus: when the Dismiss button disappears, the screen moves focus to Undo, and back to Dismiss after an Undo (the stories useautoFocus). Undo stays available, with no time limit. Port impact: adds'dismissed'to the state type, the--dismissedmodifier and thecircleMinusicon; Undo and any follow-up (such as an agent reply offering another approach) are the screen's.No Feedback Bar on a dismissed card (2026-10-19). A dismiss is not "not good" (it can be right but not now, or already done by hand), it is already a signal the screen can record as an event, and after a dismiss nothing was applied, so there is no outcome to rate. Undo is the control for that moment. An explicit "bad suggestion" can still be collected while the card is
pending, sinceactionsEndshows in every state. If the team wants to know why people dismiss, a short reason follow-up ("Not relevant", "Wrong change", "Not now") is better than thumbs, and is a separate piece to design once the signal's use is known.Open: a pending card the conversation moved on from. What happens when a newer suggestion replaces it without a decision? A "superseded" state may be needed.
Multiple choice type (2026-10-21)
- Decision:
typeprop,changes(default, unchanged) orchoice. The choice card has no overall padding: a padded header (title and description), a PDSPanelListof rows (thechildren), and a padded footer with the sameactionsStart/actionsEndslots and states, divided by lines edge to edge, from the designer's wireframe. A newSuggestionChoicehelper is one row (aPanelRow): a native radio or checkbox (selectionType) with a label and optional description, the whole row being the label. Radios share aname; the screen owns what is selected and what the footer button does. The list hasrole='group'labelled by the title. A dismissed choice card hides the rows like the changes card. - Why: the designer wants a multiple choice suggestion, built on a panel list, with a radio or checkbox per row. One component with a
typekeeps the header, footer and states shared. Assumptions to confirm: row content is a slot (so the screen can put anything in a row;SuggestionChoiceis a helper), andtypeon the card rather than a separate component. - Port impact: adds
type, the--choicemodifier andSuggestionChoice; uses the package's PanelList and PanelRow. Checkbox rows use the PDSCheckbox(so the box and check match PDS). Radio rows are a native input in a label carrying the package'spds-radio-group__optionclass, which styles the radio, because PDS RadioGroup renders a whole group and cannot be one row; the port may want a standalone PDS Radio.paddingdoes not apply tochoice. - Open: whether selecting should also report to the card (for example to disable the confirm button until something is picked), or stay the screen's.
- The choice row's description is Copy XS (
pds-copy-xs, 12px), the label stays Copy S medium (2026-10-21, designer's call). - The choice card's title to description gap stays
--pds-spacing-xs(10px), the same as the default card (2026-10-21; 6px was tried and put back at the designer's request).
Before and after as a stacked diff (2026-10-21)
- Decision:
SuggestionChangeis now the field name over one bordered block with two lines, like a text diff: the old value on a critical-tinted line with a minus icon, the new value on a success-tinted line with a plus icon (medium weight). The words "Before" and "After" (beforeLabel,afterLabel) stay as required props but are visually hidden, so screen readers still hear them and the meaning no longer depends on color alone (the minus and plus carry it too). It replaces the two columns with uppercase headings, which the designer found ugly and which wasted width on short values. - Why: a stacked diff reads top to bottom, gives long text the full width, and matches the diff look used elsewhere (code diffs, version control).
- Port impact: replaces the
__values/__value*classes with__diff,__line*,__marker,__text; uses theminusandplusicons and thevisually-hiddenclass. The two label props are now screen-reader text only.
An "Other" option with a text field (2026-10-21)
- Decision:
SuggestionChoicetakeschildren, shown under the label and lined up with it (an indent of one control plus its gap, from tokens). An "Other" option is a normal row whose child is a PDSTextInput(label hidden, still required for screen readers). The card knows nothing about it: the screen decides whether the field is enabled only when Other is selected, and the stories select Other when the field is focused. - Why: the designer's reference shows a radio labelled Other with an input under it. Keeping it a slot means the same works for a checkbox row.
- Port impact: adds the
childrenprop and__extra. The indent assumes the radio or checkbox is 16px (--pds-spacing-m) wide.
Regenerate and browse the sets: a recipe (2026-10-21)
- Decision: the retry flow (a Regenerate button in
actionsStart, the PDS Compact Pagination inactionsEndto browse earlier sets) is the recipeRecipes/Regenerate a suggestion(proposals/RegenerateSuggestion/), not a story here. The card needed no change for it.
The diff block is the Content Diff component (2026-10-21)
- Decision: the stacked before and after moved out of
SuggestionChangeinto its own component,ContentDiff.SuggestionChangeis now the field name above aContentDiff, with the same props (afterLabelandbeforeLabelare still the screen-reader names). - Port impact: SuggestionCard imports
ContentDiff, so it ports after it.