← All proposals

Chat Attachment

Net-new component: one attached item for chat.

Net-new componentOpen in Storybook

proposals/ChatAttachment/decisions.md33 lines · View on GitHub

Decisions: Chat Attachment

Append as you go, newest last.

  • Separate component, not part of ChatInput (2026-10-16). The first version was a story-only chip. The design has three looks and a hover remove, and an attachment also belongs in a sent ChatMessage, so it is its own proposal. ChatInput's fieldHeader slot stays the container.
  • Name ChatAttachment, not ChatFileUpload. PDS already has FileUpload (the drop zone); an attached item is a different thing. The designer's Figma frame is called ChatFileUpload. Open: confirm the name.
  • Fixed size. File chips are one width (spacing-9xl) and height (spacing-3xl) and the name clamps to two lines; images are a spacing-3xl square. Rows wrap predictably.
  • Middle truncation is done on the string, not with CSS. CSS cannot keep the end of a name while shortening the middle over two lines. Over 24 characters, the name keeps its first 17 and last 6 with an ellipsis (verylong-filename…123.md, as in the design). Counted in characters, not pixels; with a very wide or narrow font it may fit slightly more or less. The full name is in a visually-hidden span (the shortened text is aria-hidden), so screen readers read all of it.
  • Full name in a tooltip (the PDS Tooltip, with the item as its custom trigger). It opens on hover and on keyboard focus (focus lands on the remove button, which sits inside the trigger). Tap is turned off so a tap does not fight the remove button.
  • Remove shows on hover only (designer's call), with fallbacks: also on :focus-within (keyboard) and always on devices that cannot hover (@media (hover: none)). The button is only rendered when onRemove is set; removeLabel carries the full accessible label, since the component holds no text.
  • Image without a preview is a grey square with an image icon. Built as "no preview"; open: is it "loading"?
  • Marked new-pattern. Depends on Icon (local wrapper), IconButton and the PDS Tooltip.
  • Type and color (designer, 2026-10-16): text and icon use foreground-default-secondary, the icon is size s and the text is 2xs.
  • Border color: border-default (not border-default-secondary) on the chip and the remove button.
  • Remove button stays opaque on hover (2026-10-16). The IconButton hover and pressed fills are translucent, so an image showed through the button. The hover and pressed fill is now the same tint laid over an opaque surface-default.
  • Truncation keeps the extension, not the last characters (designer, 2026-10-16). Over 24 characters the name is the start, an ellipsis and the extension after the last dot (verylong-filename-exam….md). No extension: the last 6 characters. Supersedes the "first 17 and last 6" rule above.
  • Error state (2026-10-16), first try: status='error' with errorMessage. The tile keeps its size and takes the critical background, border and text; the file icon becomes an exclamation icon; an image stays visible, dimmed, with the icon over it. The tooltip reads <file name> — <reason>, and a visually hidden role='alert' span announces the reason. status rather than a boolean so an uploading value can join it. Not built: retry (onRetry), uploading. Open: reason on the chip, retry, and whether Remove should always show on an error.
  • No tooltip on the remove button (designer, 2026-10-16). hasTooltip={false} turns off the IconButton tooltip, and title={undefined} removes the native title it falls back to. The aria-label stays, so screen readers still announce it. The file-name tooltip on the whole item still shows when hovering the X. The port needs the same two props if IconButton still falls back to title.
  • How the error type is communicated (2026-10-16): the chip stays quiet (critical look, exclamation icon). The reason is in the tooltip, which now also opens on tap for error chips only (other chips keep tap off so a tap does not fight the remove button), and in a visually hidden alert. The sentence that says what happened and what to do belongs in ChatInput's helper message, which the screen writes; the With Attachment Error story shows it. Open: errorType that drives retry (network) versus remove-only (too large, unsupported).
  • Error icon is diamondExclamation (designer, 2026-10-16), on the chip, over an image, and in the helper message in the ChatInput story.
  • Image error (designer's mock, 2026-10-16): a failed image is the same tile with the critical background and border and the image icon in the critical text color, with no thumbnail (a picture that failed has nothing reliable to show), so the badge and dimming were removed. A failed file chip keeps the diamondExclamation icon. The tooltip and the hidden alert still carry the reason.
  • The chip does not announce its own error (2026-10-16). Its hidden reason text is plain (no role='alert'), so a screen reader reads it when the user reaches the chip. The screen owns the one live announcement (the helper message in ChatInput); with several failures, per-chip alerts would be read all at once. Supersedes the "visually hidden role='alert'" line above.

Semantic type classes instead of font primitives (2026-10-19)

  • Decision: the item now takes the PDS semantic pds-copy-* class in its markup instead of declaring font-family, font-size and line-height from 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 at s).
  • 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.

Handoff note (2026-10-20)

  • The earlier lines that mention a role='alert' span on the chip, and an error image that is dimmed with a badge, are superseded by the two entries above them that say the chip does not announce its own error and that a failed image has no thumbnail. The code matches the later entries.