From 71ec7baa0452e4504a28d3c6694d7d0336481ba9 Mon Sep 17 00:00:00 2001 From: David Anthony Date: Tue, 7 Jul 2026 15:21:04 -0700 Subject: [PATCH 1/4] feat(apollo-wind): add Message and Bubble chat pattern components Part of a spike to bring shadcn's new chat component family (MessageScroller, Message, Bubble, Attachment, Marker) into apollo-wind for an "Autopilot chat" Patterns page. Message and Bubble are ported and self-contained (no new dependencies). Paused here: MessageScroller needs a new package, @shadcn/react, which this repo's pnpm minimumReleaseAge policy currently blocks (published 2026-06-30, clears the 14-day policy window around 2026-07-14). Attachment, Marker, MessageScroller, the Autopilot chat story, and Patterns sidebar ordering are not yet built. --- .../apollo-wind/src/components/ui/bubble.tsx | 115 ++++++++++++++++++ .../apollo-wind/src/components/ui/message.tsx | 86 +++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 packages/apollo-wind/src/components/ui/bubble.tsx create mode 100644 packages/apollo-wind/src/components/ui/message.tsx diff --git a/packages/apollo-wind/src/components/ui/bubble.tsx b/packages/apollo-wind/src/components/ui/bubble.tsx new file mode 100644 index 000000000..ffdf03a1a --- /dev/null +++ b/packages/apollo-wind/src/components/ui/bubble.tsx @@ -0,0 +1,115 @@ +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; +import { cn } from '@/lib'; + +const BubbleGroup = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +BubbleGroup.displayName = 'BubbleGroup'; + +const bubbleVariants = cva( + 'group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end', + { + variants: { + variant: { + default: + '[&>[data-slot=bubble-content]]:bg-primary [&>[data-slot=bubble-content]]:text-primary-foreground', + secondary: + '[&>[data-slot=bubble-content]]:bg-secondary [&>[data-slot=bubble-content]]:text-secondary-foreground', + muted: '[&>[data-slot=bubble-content]]:bg-surface-overlay', + outline: + '[&>[data-slot=bubble-content]]:border-border [&>[data-slot=bubble-content]]:bg-background', + ghost: + 'border-none [&>[data-slot=bubble-content]]:rounded-none [&>[data-slot=bubble-content]]:bg-transparent [&>[data-slot=bubble-content]]:p-0', + destructive: + '[&>[data-slot=bubble-content]]:bg-destructive/10 [&>[data-slot=bubble-content]]:text-destructive', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +export interface BubbleProps + extends React.HTMLAttributes, + VariantProps { + align?: 'start' | 'end'; +} + +const Bubble = React.forwardRef( + ({ variant = 'default', align = 'start', className, ...props }, ref) => ( +
+ ) +); +Bubble.displayName = 'Bubble'; + +export interface BubbleContentProps extends React.HTMLAttributes { + asChild?: boolean; +} + +const BubbleContent = React.forwardRef( + ({ asChild = false, className, ...props }, ref) => { + const Comp = asChild ? Slot : 'div'; + return ( + + ); + } +); +BubbleContent.displayName = 'BubbleContent'; + +const bubbleReactionsVariants = cva( + 'absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-surface-overlay px-1.5 py-0.5 text-sm ring-2 ring-card', + { + variants: { + side: { + top: 'top-0 -translate-y-3/4', + bottom: 'bottom-0 translate-y-3/4', + }, + align: { + start: 'left-3', + end: 'right-3', + }, + }, + defaultVariants: { + side: 'bottom', + align: 'end', + }, + } +); + +export interface BubbleReactionsProps + extends React.HTMLAttributes, + VariantProps {} + +const BubbleReactions = React.forwardRef( + ({ side = 'bottom', align = 'end', className, ...props }, ref) => ( +
+ ) +); +BubbleReactions.displayName = 'BubbleReactions'; + +export { BubbleGroup, Bubble, BubbleContent, BubbleReactions }; diff --git a/packages/apollo-wind/src/components/ui/message.tsx b/packages/apollo-wind/src/components/ui/message.tsx new file mode 100644 index 000000000..6c662c6b7 --- /dev/null +++ b/packages/apollo-wind/src/components/ui/message.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { cn } from '@/lib'; + +const MessageGroup = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +MessageGroup.displayName = 'MessageGroup'; + +export interface MessageProps extends React.HTMLAttributes { + align?: 'start' | 'end'; +} + +const Message = React.forwardRef( + ({ className, align = 'start', ...props }, ref) => ( +
+ ) +); +Message.displayName = 'Message'; + +const MessageAvatar = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +MessageAvatar.displayName = 'MessageAvatar'; + +const MessageContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +MessageContent.displayName = 'MessageContent'; + +const MessageHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +MessageHeader.displayName = 'MessageHeader'; + +const MessageFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +MessageFooter.displayName = 'MessageFooter'; + +export { MessageGroup, Message, MessageAvatar, MessageContent, MessageHeader, MessageFooter }; From b7d77369b075ed7b4761ad6b9f261c8acbe602c1 Mon Sep 17 00:00:00 2001 From: David Anthony Date: Fri, 10 Jul 2026 09:37:19 -0700 Subject: [PATCH 2/4] feat(apollo-wind): add MessageScroller, Attachment, and Marker chat primitives Completes the shadcn chat component port started with Message/Bubble: - MessageScroller (+ Provider/Viewport/Content/Item/Button) wraps the newly-added @shadcn/react package with apollo-wind styling. Handles auto-scroll-to-latest with a fade-in "scroll to latest" button once the viewport is scrolled away from the live edge. - Attachment (+ Media/Content/Title/Description/Actions/Action/Trigger/ Group) and Marker (+ Icon/Content), ported from shadcn's registry source. - Extends InputGroupAddon with block-start/block-end alignment (a toolbar row stacked below content instead of beside it), which shadcn's current InputGroup supports but apollo-wind's earlier port didn't. The container switches to a column layout via a `has-[]:` variant, forced with `!` since conditional variants and unconditional utilities on the same property aren't reconciled by class merging. - Adds the `shimmer` and `scroll-fade-x`/`scroll-fade-y` Tailwind utilities these components need, scoped to only what's used (the source registry ships direction variants and size modifiers nothing here uses). @shadcn/react was vetted (provenance, publisher, release diff, integrity) and added to minimumReleaseAgeExclude pending the 14-day quarantine window. --- packages/apollo-wind/package.json | 1 + .../src/components/ui/attachment.tsx | 191 +++++ .../apollo-wind/src/components/ui/index.ts | 5 + .../src/components/ui/input-group.tsx | 8 + .../apollo-wind/src/components/ui/marker.tsx | 65 ++ .../src/components/ui/message-scroller.tsx | 94 +++ packages/apollo-wind/src/index.ts | 661 +++++++++--------- .../src/styles/tailwind.consumer.css | 211 ++++++ pnpm-lock.yaml | 19 + pnpm-workspace.yaml | 1 + 10 files changed, 914 insertions(+), 342 deletions(-) create mode 100644 packages/apollo-wind/src/components/ui/attachment.tsx create mode 100644 packages/apollo-wind/src/components/ui/marker.tsx create mode 100644 packages/apollo-wind/src/components/ui/message-scroller.tsx diff --git a/packages/apollo-wind/package.json b/packages/apollo-wind/package.json index f6e66fb2d..efb053585 100644 --- a/packages/apollo-wind/package.json +++ b/packages/apollo-wind/package.json @@ -115,6 +115,7 @@ "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", + "@shadcn/react": "0.2.1", "@tailwindcss/postcss": "^4.1.17", "@tanstack/react-table": "^8.21.3", "@uipath/apollo-core": "workspace:*", diff --git a/packages/apollo-wind/src/components/ui/attachment.tsx b/packages/apollo-wind/src/components/ui/attachment.tsx new file mode 100644 index 000000000..848322498 --- /dev/null +++ b/packages/apollo-wind/src/components/ui/attachment.tsx @@ -0,0 +1,191 @@ +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib'; + +const attachmentVariants = cva( + 'group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed', + { + variants: { + size: { + default: + 'gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2', + sm: 'gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5', + xs: 'gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1', + }, + orientation: { + horizontal: 'min-w-40 items-center', + vertical: 'w-24 flex-col has-data-[slot=attachment-content]:w-30', + }, + }, + } +); + +export interface AttachmentProps + extends React.ComponentProps<'div'>, + VariantProps { + state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done'; +} + +function Attachment({ + className, + state = 'done', + size = 'default', + orientation = 'horizontal', + ...props +}: AttachmentProps) { + return ( +
+ ); +} + +const attachmentMediaVariants = cva( + "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5", + { + variants: { + variant: { + icon: '', + image: + 'opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover', + }, + }, + defaultVariants: { + variant: 'icon', + }, + } +); + +export interface AttachmentMediaProps + extends React.ComponentProps<'div'>, + VariantProps {} + +function AttachmentMedia({ className, variant = 'icon', ...props }: AttachmentMediaProps) { + return ( +
+ ); +} + +function AttachmentContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AttachmentTitle({ className, ...props }: React.ComponentProps<'span'>) { + return ( + + ); +} + +function AttachmentDescription({ className, ...props }: React.ComponentProps<'span'>) { + return ( + + ); +} + +function AttachmentActions({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AttachmentAction({ + className, + variant, + size, + icon = true, + ...props +}: React.ComponentProps) { + return ( + + + New chat + + + + + + Settings + + + + + + History + + {variant !== 'embedded' && ( + + + + + {variant === 'fullscreen' ? 'Collapse' : 'Expand'} + + )} + + + + + Close + +
+ + +
+ {turns.length === 0 ? ( + } + title="Morning!" + description="What are we working on today? Press send to start a new conversation." + /> + ) : ( + + + + + + Today + + {turns.map((turn) => ( + + + {turn.role === 'assistant' && ( + + + AI + + + )} + + {turn.role === 'assistant' && Autopilot} + + {turn.text} + + {turn.attachment && ( + + + + + + + {turn.attachment.name} + + + + )} + + + + ))} + + + + + + )} +
+ + +
{ + e.preventDefault(); + handleSend(); + }} + className="w-full" + > + +
+ + {nextTurn?.role === 'user' ? ( + nextTurn.text + ) : ( + + {isBusy + ? 'Autopilot is replying…' + : 'No messages queued. Reset the conversation.'} + + )} + +
+ + + + + + + + Attach file + + + + + + + + Add resource + + + + + {(() => { + const ActiveIcon = + AGENT_MODES.find((m) => m.label === agentMode)?.icon ?? Bot; + return ; + })()} + {agentMode} + + + + {AGENT_MODES.map((mode) => ( + setAgentMode(mode.label)}> + + + {mode.label} + + + ))} + + + + + Send + + +
+
+
+ + + ); +} + +/** + * Mirrors the legacy `ApChat` Embedded mode: the panel is portaled into a + * consumer-owned container docked to a corner of the host page. In the + * legacy Storybook harness that container is `fixed`, offset 24px from the + * bottom-right, sized 400x600, with an elevated shadow and rounded corners — + * all host-page choices, not something the panel itself controls. + */ +export const Embedded: Story = { + name: 'Layout Embedded', + parameters: { layout: 'fullscreen' }, + render: () => ( +
+
+ Host application +
+
+ +
+
+ ), +}; + +/** + * Mirrors the legacy `ApChat` FullScreen mode: no fixed positioning, the + * panel just expands to fill the page. Same internal UI as `Layout Embedded`, + * only the outer Card's size/chrome changes. + */ +export const Fullscreen: Story = { + name: 'Layout Fullscreen', + parameters: { layout: 'fullscreen' }, + render: () => ( +
+ +
+ ), +}; + +function Section({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+ + {label} + + {children} +
+ ); +} + +function ComponentsGallery() { + return ( +
+
+
+ + + + AI + + + + Autopilot + + I found 3 invoices that need approval. + + + + + + + Go ahead and summarize them. + + + +
+
+ +
+
+ + + + + + + invoices-week-27.pdf + + + +
+
+ +
+
+ + Today + +
+
+ +
+ + + + + + + + + Scrollable, auto-follows new content. + + + + + + + + + Scroll up to see the button appear. + + + + + + + + + +
+
+ ); +} + +export const Components: Story = { + name: 'Components', + parameters: { + docs: { + description: { + story: + 'The individual primitives the Panel story is built from, shown in isolation. Message/Bubble render conversation turns; Attachment renders file cards; Marker renders separators/labels; MessageScroller provides the auto-following, anchor-aware scroll container.', + }, + }, + }, + render: () => , +}; From 44d52abe6691f1ece9cf3613fd107bda3b1915a6 Mon Sep 17 00:00:00 2001 From: David Anthony Date: Fri, 10 Jul 2026 09:46:30 -0700 Subject: [PATCH 4/4] docs(apollo-wind): add Wind vs Material guidance page to Autopilot Chat Adds a "Wind vs Material" page to Patterns/Autopilot Chat clarifying when to use each: apollo-wind's primitives for presentational work (mockups, custom chat UIs, no MUI/Emotion dependency), Material's ApChat for fully wired product integration (real AutopilotChatService backing models, agent modes, resources, streaming, history, mode switching). Includes a capability comparison table and links to where each lives. --- .../ui/autopilot-chat-guidance.stories.tsx | 247 ++++++++++++++++++ .../components/ui/autopilot-chat.stories.tsx | 2 +- 2 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 packages/apollo-wind/src/components/ui/autopilot-chat-guidance.stories.tsx diff --git a/packages/apollo-wind/src/components/ui/autopilot-chat-guidance.stories.tsx b/packages/apollo-wind/src/components/ui/autopilot-chat-guidance.stories.tsx new file mode 100644 index 000000000..68e91d62d --- /dev/null +++ b/packages/apollo-wind/src/components/ui/autopilot-chat-guidance.stories.tsx @@ -0,0 +1,247 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Check, X } from 'lucide-react'; +import type { ReactNode } from 'react'; +import { cn } from '@/lib'; + +const meta = { + title: 'Patterns/Autopilot Chat', + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +function InlineCode({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function InfoCallout({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function Divider() { + return
; +} + +interface CapabilityRow { + label: string; + wind: boolean | string; + material: boolean | string; +} + +const CAPABILITIES: CapabilityRow[] = [ + { label: 'Message/Bubble/Attachment/Marker visuals', wind: true, material: true }, + { label: 'Auto-scroll, scroll anchoring, scroll-to-latest', wind: true, material: true }, + { label: 'Real model/agent-mode registry', wind: false, material: true }, + { label: 'Resource manager (@ references)', wind: false, material: true }, + { label: 'Streaming responses from a backend', wind: false, material: true }, + { label: 'History persistence', wind: false, material: true }, + { + label: 'SideBySide / FullScreen / Embedded mode switching at runtime', + wind: false, + material: true, + }, + { label: 'Custom header actions extension point', wind: false, material: true }, + { label: 'STT / voice input', wind: false, material: true }, + { label: 'Error/loading state injection', wind: false, material: true }, + { label: 'Ships as Tailwind source you own and can restyle freely', wind: true, material: false }, + { label: 'Works without Material UI / Emotion in your app', wind: true, material: false }, +]; + +function Cell({ value }: { value: boolean | string }) { + if (typeof value === 'string') { + return {value}; + } + return value ? ( + + ) : ( + + ); +} + +function DecisionCard({ + title, + subtitle, + bullets, + tone, +}: { + title: string; + subtitle: string; + bullets: string[]; + tone: 'wind' | 'material'; +}) { + return ( +
+
+

{title}

+

{subtitle}

+
+
    + {bullets.map((bullet) => ( +
  • + - + {bullet} +
  • + ))} +
+
+ ); +} + +function WindVsMaterialPage() { + return ( +
+
+

+ Autopilot Chat: Wind vs. Material +

+

+ Two different things live under the "Autopilot chat" name in this design system. They are + not alternatives to pick by preference. They solve different problems. +

+ +
+ +

The short version

+

+ Use apollo-wind's chat primitives (this Patterns page) for{' '} + presentational work: mockups, design + reviews, or a custom chat UI you're wiring up yourself. Use{' '} + apollo-react's ApChat (Material) for{' '} + fully wired product integration: a real, + connected Autopilot experience with almost no glue code. +

+
+
+ + + +
+

Why two implementations

+

+ ApChat is driven by an{' '} + AutopilotChatService, an event-bus + imperative API that owns + models, agent modes, resource references, streaming, history, and mode switching. The + chat UI is a thin rendering layer over that service. The{' '} + apollo-wind version doesn't have that service; it's{' '} + Message, Bubble,{' '} + Attachment, Marker, and{' '} + MessageScroller as Tailwind primitives, assembled into a demo + panel that fakes a conversation with local React state. Neither is "the old one". They + target different jobs. +

+
+ +
+ + +
+ + + +
+

Capability comparison

+
+ + + + + + + + + + {CAPABILITIES.map((row, i) => ( + + + + + + ))} + +
Capabilityapollo-windMaterial
{row.label} +
+ +
+
+
+ +
+
+
+
+ + + +
+

Where to look

+
    +
  • + Components: the apollo-wind primitives + (Message/Bubble, Attachment, Marker, MessageScroller) shown in isolation. +
  • +
  • + Layout Embedded /{' '} + Layout Fullscreen: the apollo-wind panel + assembled into the two placement contexts, mirroring ApChat's + Embedded/FullScreen modes. +
  • +
  • + + Apollo React → Material (Maintenance Only) → Components → Chat + + : the real, service-driven ApChat, with a full configuration + harness (models, agent modes, resource manager, streaming, feature toggles). +
  • +
+
+
+
+ ); +} + +export const WindVsMaterial: Story = { + name: 'Wind vs Material', + render: () => , +}; diff --git a/packages/apollo-wind/src/components/ui/autopilot-chat.stories.tsx b/packages/apollo-wind/src/components/ui/autopilot-chat.stories.tsx index 2db0c8d8d..ea5d6c3f2 100644 --- a/packages/apollo-wind/src/components/ui/autopilot-chat.stories.tsx +++ b/packages/apollo-wind/src/components/ui/autopilot-chat.stories.tsx @@ -367,7 +367,7 @@ function AutopilotChatPanel({ className, variant = 'standard' }: AutopilotChatPa * Mirrors the legacy `ApChat` Embedded mode: the panel is portaled into a * consumer-owned container docked to a corner of the host page. In the * legacy Storybook harness that container is `fixed`, offset 24px from the - * bottom-right, sized 400x600, with an elevated shadow and rounded corners — + * bottom-right, sized 400x600, with an elevated shadow and rounded corners, * all host-page choices, not something the panel itself controls. */ export const Embedded: Story = {