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: () => , +}; 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/index.ts b/packages/apollo-wind/src/components/ui/index.ts index 218983598..14c7ff9b6 100644 --- a/packages/apollo-wind/src/components/ui/index.ts +++ b/packages/apollo-wind/src/components/ui/index.ts @@ -2,9 +2,11 @@ export * from './accordion'; export * from './alert'; export * from './alert-dialog'; export * from './aspect-ratio'; +export * from './attachment'; export * from './avatar'; export * from './badge'; export * from './breadcrumb'; +export * from './bubble'; export * from './button'; export * from './button-group'; export * from './calendar'; @@ -27,6 +29,9 @@ export * from './input'; export * from './input-group'; export * from './label'; export * from './layout'; +export * from './marker'; +export * from './message'; +export * from './message-scroller'; export * from './multi-select'; export * from './pagination'; export * from './popover'; diff --git a/packages/apollo-wind/src/components/ui/input-group.tsx b/packages/apollo-wind/src/components/ui/input-group.tsx index 7cd4fb16e..f8218e21f 100644 --- a/packages/apollo-wind/src/components/ui/input-group.tsx +++ b/packages/apollo-wind/src/components/ui/input-group.tsx @@ -19,6 +19,12 @@ const InputGroup = React.forwardRef( role="group" className={cn( 'group/input-group relative flex w-full items-center gap-2 transition-colors has-[>textarea]:h-auto has-[>textarea]:items-start', + // Block-aligned addons (toolbar rows) stack the group into a column instead of + // sitting inline. `!` is required: these must win over the unconditional height/ + // items-center utilities below regardless of Tailwind's generated rule order, + // since has-[]: variants and plain utilities aren't reconciled by class merging. + 'has-[>[data-align=block-start]]:!h-auto has-[>[data-align=block-start]]:!flex-col has-[>[data-align=block-start]]:!items-stretch', + 'has-[>[data-align=block-end]]:!h-auto has-[>[data-align=block-end]]:!flex-col has-[>[data-align=block-end]]:!items-stretch', // Size (mirrors Input's own size scale, since Input's box chrome moves up to this wrapper) size === 'default' && 'h-9 rounded-md px-3 py-1 future:h-10 future:rounded-xl future:py-2', @@ -49,6 +55,8 @@ const inputGroupAddonVariants = cva( align: { 'inline-start': 'order-first', 'inline-end': 'order-last', + 'block-start': 'order-first w-full basis-full justify-start', + 'block-end': 'order-last w-full basis-full justify-start border-t border-input', }, }, defaultVariants: { diff --git a/packages/apollo-wind/src/components/ui/marker.tsx b/packages/apollo-wind/src/components/ui/marker.tsx new file mode 100644 index 000000000..077f86363 --- /dev/null +++ b/packages/apollo-wind/src/components/ui/marker.tsx @@ -0,0 +1,65 @@ +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 markerVariants = cva( + "group/marker relative flex min-h-4 w-full items-center gap-2 text-left text-sm text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", + { + variants: { + variant: { + default: '', + separator: + 'before:mr-1 before:h-px before:min-w-0 before:flex-1 before:bg-border after:ml-1 after:h-px after:min-w-0 after:flex-1 after:bg-border', + border: 'border-b border-border pb-2', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +export interface MarkerProps + extends React.ComponentProps<'div'>, + VariantProps { + asChild?: boolean; +} + +function Marker({ className, variant = 'default', asChild = false, ...props }: MarkerProps) { + const Comp = asChild ? Slot : 'div'; + return ( + + ); +} + +function MarkerIcon({ className, ...props }: React.ComponentProps<'span'>) { + return ( +