Decisions: Chat Panel
Name: Chat Panel (2026-10-19)
- Decision: the component is called Chat Panel (
ChatPanel). - Why: "Chat Drawer" would collide with the PDS
Drawerand imply an overlay behavior this does not own; "Chat UI" is too broad (Chat Input and Chat Message are already chat UI); "Chat Container" says nothing about what is inside. PDS names its surfaces "Panel". - Port impact: none yet.
Slot props, not compound components (2026-10-19)
- Decision: regions are props (
title,header,footer,children), like Chat Input'saboveField/fieldHeader/actionsStart. - Why: it matches the proposals already here, so the port is mostly a copy. Teams change what is inside a slot without touching the frame. Compound parts (
ChatPanel.Header) were rejected: they let a team reorder or repeat regions, which the layout can't support. - Port impact: none.
The footer is footer, not composer (2026-10-19)
- Decision: the bottom slot is
footer; the field stays named Chat Input. - Why: teams put other things there (a disclaimer, a stop bar). "Composer" names the area of pieces (Chat Input, Chat Attachment, the Send button, the model picker), not a slot.
- Port impact: none.
The Compose / Chat / Review switch lives in header, the panel doesn't know it (2026-10-19)
- Decision: the switch is the PDS
SegmentedButton(sizem) placed inheader(first built withTabs, changed at the designer's request); the screen swapschildrenwhen the value changes. - Why: what each tab contains is not this component's business.
- Port impact: none.
Inset and bleed per region (2026-10-19)
- Decision:
headerInset,contentInsetandfooterInsettakedefaultornone.noneremoves the inline padding only (block padding stays); the child then supplies its own. The title row is always inset so the close button never touches the edge. - Why: a negative-margin utility class on the child breaks when padding changes. A per-region prop keeps the rule in one place.
- Port impact: none.
Dividers are booleans, always on when set (2026-10-19)
- Decision:
hasHeaderDividerandhasFooterDivider, both defaultfalse. Aon-scrollmode was not built. - Why: the reference has no rules; scroll-aware dividers need scroll listeners for a small gain.
- Port impact: none.
Header pinned, scroll owned by the screen (2026-10-19)
- Decision: title row, header and footer are pinned; only the content scrolls. Stick-to-bottom was first left to the screen; it is now opt-in through
hasScrollToLatest(see "Scroll to latest and a content fade"). - Why: the defaults chosen with the designer. A
headerBehavioroption and built-in autoscroll can be added if teams ask. - Port impact: none.
No container query (2026-10-19)
- Decision: the panel fills its container and has no breakpoints of its own.
- Why: nothing in it changes with width yet, so
container-typewould be unused. Add it with the first width-dependent rule. - Port impact: none.
Close button is the package CloseButton (2026-10-19)
- Decision:
hasCloseButton(defaulttrue),onCloseandcloseButtonLabel(the screen-reader text, no default so no English is hardcoded), same names as the PDSDrawer. - Why: reuse; matches Drawer's API.
- Port impact: none.
Default inset matches the PDS Panel content (2026-10-19)
- Decision:
--chat-panel-insetis--pds-spacing-l, and--pds-spacing-xlfrom thembreakpoint up, the same as the PDS Panel's standard content padding (panel.css). It sets the inline padding of the title row and ofdefaultregions. - Why: the designer asked to use the existing default. The block padding of the regions (
--pds-spacing-s/-m) is the panel's own and is not from the Panel. - Port impact: the query is written
(min-width: 768px)because the custom media form doesn't work in this repo; restore@media (--pds-bp-m). The breakpoint is the viewport's, as in the Panel, not the panel's width.
Size: m and l, the PDS Drawer widths (2026-10-19)
- Decision:
sizeism(25rem) orl(37.5rem), the Drawer'smandl; the Drawer'ss(20rem) is not offered.mis the default.max-width: 100%keeps it inside narrow screens. (Afillsize was added later the same day; see below.) - Why: the designer asked for a medium (default) and a large to match the reference drawer, shown through the controls, not a separate story. Named
m/l, notmd/lg, to match the Drawer and the other PDS size props. - Port impact: the Drawer and the panel each set a width; a drawer holding a
size='m'panel should use the same size.
size='fill' removed (2026-10-19)
- Decision:
sizeism(default) orlonly; thefilloption and its--fillmodifier are removed. - Why: it was added for the full-screen chat, which is not needed for now. A team that needs a full-width panel can override the width with a class.
- Port impact: none.
Scroll to latest and a content fade (2026-10-19)
- Decision: two opt-in props.
hasScrollToLatest(withscrollToLatestLabel) does two things from one piece of position tracking: it shows a floating button at the bottom center of the content area while the person is scrolled away from the end, and it keeps the view at the end as content grows, but only while they are at the end.hasContentFadeis separate: a gradient mask fades the bottom edge of the content area into the footer, with bottom padding equal to the fade so the last line is clear at the end. Not built: a "new messages" dot or label on the button (the designer: not right now). - How it works: an invisible marker at the end of the content is watched by an IntersectionObserver (24px slack) to know whether the person is at the end. Content growth fires no scroll event, but a scroll by the person does, and that sets whether they are following; when growth pushes the marker out of view while they are following, the panel scrolls to the end. The button is outside the faded element, so the fade never dims it. Choosing it scrolls to the end (instantly with
prefers-reduced-motion) and focuses the scroll area (tabindex=-1), so keyboard users keep their place when the button goes away. The fade needs an opaque mask color only, so it uses a foreground token. - Why: the recipe called
scrollIntoViewon every update, which yanked the view back to the bottom while the person was reading earlier messages. This partly reverses the earlier decision that the screen owns scrolling to the newest message: with the prop on, the panel owns it; without it, the screen still does. - Shape: the button is the standard square icon button (the input radius), bordered, with
--pds-elevation-raised, not a circle. Two earlier decisions point that way (controls follow the input radius; the Send Button's circle was rejected), it needs no new modifier, and the floating cue comes from the shadow and the position. A circle is oneborder-radiusaway if wanted. The reference uses an arrow; the icon set has noarrowDown, so it was first theangleDownchevron (nowanglesDown, see the last entry). - Port impact: adds the three props and the parts
__end,__scroll,__scroll-buttonand--fade; the panel imports the local IconButton. The fade usesmask-image(with the-webkit-prefix); the scrollbar fades with the edge, which is hidden on macOS anyway.
Empty state heading is xl, set in the story (2026-10-19)
Note (2026-10-20): this sample now lives in the recipe Recipes/Chat with an agent, not in Chat Panel's stories.
- Decision: the empty state in the stories uses the PDS
VerticalEmptyStatewith its heading set to--pds-typography-size-xlby a story-only class. The PDS default is2xland the component has no size prop. - Why: the designer asked for an XL heading in the chat empty state.
- Also: the description is
pds-copy-xsin--pds-color-foreground-default-secondary(a<p>passed asmessage, with a story class for the color). - Port impact: the empty state is not part of Chat Panel. If it ships as a component, the engineer needs a heading size option on
VerticalEmptyStateor a local variant.
Trial: a neutral grey (silver) Tag in the sample header (2026-10-19)
Note (2026-10-20): the sample header is no longer in Chat Panel's stories; check the recipe Recipes/Chat with an agent for whether the trial survived.
- Decision: the "Home" Tag in the sample header borrows the Badge's
silver-mutedtokens (a true neutral grey) through a story-only override of--tag-bg/--tag-fg. The Tag's own greyest color,slate, is blue-tinted andstoneis beige. - Why: the designer wanted a greyer chip that still has the remove X (a Badge can't be removed).
- Port impact: if kept, the PDS Tag needs a neutral color (for example
silver) and the sample uses it; the override is then dropped. Not a Chat Panel change.
Drag and drop files over the whole panel (2026-10-19)
- Decision: the panel owns the drop surface.
onFilesDrop(files)turns it on for the whole panel (title row, header, content, footer); while files are dragged over,dropLabeland a file icon (file) show on a faded sheet over the content area only (pds-chat-panel__drop,pointer-events: none). The screen hands the files to the same handler it gives Chat Input (attachments, limits and errors stay with the screen and Chat Attachment). - Why: the target is the content area, outside the Chat Input's DOM, and the panel owns the regions. Chat Input keeps its own
onFilesDropfor use without a panel. - Details: only dragged files react (not text or links). A counter on dragenter/dragleave stops flicker over children. A drop that Chat Input already took (it calls
preventDefaultbefore the event bubbles here) is not handed over twice: the panel skips a drop whose default is already prevented. A polite status region readsdropLabelto screen readers when the overlay appears. Dropping is mouse-only; the add menu in Chat Input is the keyboard route....propscome last, so a consumer's ownonDroporonDrag*replaces the panel's. - Port impact: adds
onFilesDropanddropLabel, a__bodywrapper around the content, and the overlay. The panel imports the local Icon for thefileicon (the real one at port).
Thanks for the feedback fades back to the thumbs (2026-10-19)
- Decision: in the sample, after a choice the thumbs are replaced by "Thanks for the feedback" for 4.5 seconds, then come back with the chosen thumb pressed. Pressing the pressed thumb clears the choice (no thanks). The timer is in the story (the screen), not in
FeedbackBar, which stays UI only. - Why: a permanent thank-you under every reply is noise, but the bar must not vanish: the pressed thumb records the choice and lets people change their mind. The status is announced when it appears, so the delay is only for people who see it.
- Port impact: the screen owns the delay; the component needs nothing.
The sample chat moved to a recipe (2026-10-19)
- Decision: the working chat that used to be the
With Sample Contentstory is now the recipeRecipes/Chat with an agent(proposals/ChatWithAgent/). Chat Panel keeps Default, Bleed and Content Only. - Why: the sample is the whole chat experience (the empty state, a moving chat input, a chain of thought, a suggestion card with a code diff, feedback thumbs, file drops), not the panel. Under Chat Panel it was hard to find, and a recipe is what the repo calls a composition of existing proposals.
- Port impact: none; the panel is unchanged.
Open questions
- Inside the PDS
Drawer: the Drawer wraps its children in a padded, scrolling content box and adds its own title row and close button. No Drawer story is built; the panel is shown docked. An engineer needs to decide whether the Drawer gets a "no wrapper" option or whether the panel docks only. - Empty state ("Work it out in conversation") is a composition in the recipe
Recipes/Chat with an agent, not a component. Does it need its own component? - Dragging a file out of the window or dropping outside the browser can leave the overlay up until the next dragenter or drop; add a window-level dragend reset if that shows up.
- Widths, breakpoints and small screens (needs the designer's specs).
Scroll to latest: opaque fill and double-angle icon (2026-10-19)
- Decision: the floating button uses the
anglesDownicon (wasangleDown), and its fill rule is doubled up (.pds-chat-panel__scroll-button.pds-icon-button, plus the hover state) so it beats the IconButton's own background. - Why: the fill was not winning, so the content showed through the button (a chevron from the message above overlapped its own). The double angle reads as "jump to the end" and does not look like the chevron that toggles a section open.