Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion apps/www/src/content/docs/ai-elements/chat-panel/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const preview = {
{message.role === 'user' ? (
<Message.Bubble>{message.text}</Message.Bubble>
) : (
<Text size="small">{message.text}</Text>
<Message.Bubble variant="ghost">{message.text}</Message.Bubble>
)}
</Message.Content>
</Message>
Expand Down Expand Up @@ -151,6 +151,57 @@ export const controlledDemo = {
}`
};

export const transitionDemo = {
type: 'code',
code: `function ChatPanelTransitions() {
const [transition, setTransition] = React.useState('morph');
const [mode, setMode] = React.useState('docked');

return (
<Flex direction="column" gap={4} style={{ width: '100%' }}>
<Flex gap={7} wrap="wrap">
<Flex gap={2}>
{['minimal', 'morph'].map(type => (
<Button key={type} size="small" color="neutral" variant={transition === type ? 'solid' : 'outline'} onClick={() => setTransition(type)}>{type}</Button>
))}
</Flex>
<Flex gap={2}>
{['docked', 'floating', 'minimized'].map(next => (
<Button key={next} size="small" color="neutral" variant="ghost" onClick={() => setMode(next)}>{next}</Button>
))}
</Flex>
</Flex>
<Flex style={{ width: '100%', height: 360, border: '0.5px solid var(--rs-color-border-base-primary)', borderRadius: 'var(--rs-radius-4)', overflow: 'hidden' }}>
<Flex style={{ flex: 1, padding: 'var(--rs-space-5)' }}>
<Text size="small" variant="secondary">
Switch modes with either transition to compare them. Morph tweens
the panel out of the box it just left; minimal eases it in from its
own edge.
</Text>
</Flex>
<ChatPanel transition={transition} mode={mode} onModeChange={setMode} side="right" defaultSize={{ width: 360, height: 440 }}>
<ChatPanel.Header>
<ChatPanel.Title>Assistant</ChatPanel.Title>
<ChatPanel.Actions>
<ChatPanel.MinimizeTrigger />
<ChatPanel.ExpandTrigger />
</ChatPanel.Actions>
</ChatPanel.Header>
<ChatPanel.Content>
<Flex style={{ padding: 'var(--rs-space-5)' }}>
<Text size="small" variant="secondary">
transition: {transition}
</Text>
</Flex>
</ChatPanel.Content>
<ChatPanel.Trigger />
</ChatPanel>
</Flex>
</Flex>
);
}`
};

export const boundedDragDemo = {
type: 'code',
code: `function BoundedDragChatPanel() {
Expand Down
42 changes: 37 additions & 5 deletions apps/www/src/content/docs/ai-elements/chat-panel/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ source: packages/raystack/components/chat-panel
tag: new
---

import { preview, controlledDemo, boundedDragDemo, resizeDemo, draggableBubbleDemo, unreadBadgeDemo } from "./demo.ts";
import { preview, controlledDemo, transitionDemo, boundedDragDemo, resizeDemo, draggableBubbleDemo, unreadBadgeDemo } from "./demo.ts";

<Demo data={preview} />

Expand All @@ -31,9 +31,9 @@ import { ChatPanel } from '@raystack/apsara'
</Flex>
```

The panel mounts **once** in your layout and morphs between modes with pure
CSS — no portal, no remount, so chat state (scroll position, focus, streams)
survives every transition:
The panel mounts **once** in your layout and switches between modes in place —
no portal, no remount, so chat state (scroll position, focus, streams) survives
every transition:

- **`docked`** — an in-flow sidebar (a flex sibling, like `SidePanel`) that
squeezes the main content rather than covering it.
Expand Down Expand Up @@ -95,12 +95,44 @@ height.

### Trigger

The minimized-state corner bubble. Children replace the default chat icon,
The minimized-state corner bubble. Children replace the default `CoPilotIcon`,
and `draggable` lets the bubble be dragged around the viewport — a completed
drag never triggers the restore click.

<auto-type-table path="./props.ts" name="ChatPanelTriggerProps" />

## Mode transitions

Switching mode flips the panel between in-flow and `position: fixed`, which no
CSS property can tween, so the transform bridges the gap: the panel is rendered
in its new mode already holding the shape it is coming out of, then eased back
to rest. `transition` picks which shape that is.

**`minimal`** (default) brings the new mode in from its own edge or corner and
fades it up: the docked sidebar slides in from the edge it docks to, the
floating window scales and lifts out of its corner, and the bubble grows out of
its own. `--rs-duration-normal`.

**`morph`** measures the box the panel is leaving and draws the new mode back
over it — an inverse `translate` and `scale` off its top-left corner — so the
panel travels and reshapes into its new place, the way a shared-layout
animation does. Docking and popping out morph outright. The bubble is a tenth
of the panel's size, so its two pairs cross-fade as well, and the bubble itself
only travels — scaling it up to the panel's box would stretch its icon for the
length of the tween. `--rs-duration-moderate`, because there is real distance to
cover.

Both use `--rs-ease-out`, so a transition is off the mark on its first frame and
settles into place rather than easing away from a standstill.

<Demo data={transitionDemo} />

Both are skipped under `prefers-reduced-motion: reduce`, and neither runs on
first paint — the transition needs a mode to have changed. Only the individual
transform properties (`scale`, `translate`) are transitioned, never `transform`
itself: dragging writes an inline `transform`, which stays instant, so a drag
never lags the pointer and ending one never replays the transition.

## Examples

### Controlled mode
Expand Down
8 changes: 8 additions & 0 deletions apps/www/src/content/docs/ai-elements/chat-panel/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ export interface ChatPanelProps {
*/
dragBoundary?: HTMLElement | React.RefObject<HTMLElement | null>;

/**
* How a mode change animates. `"minimal"` eases the new mode in from its own
* edge or corner; `"morph"` measures the box the panel is leaving and tweens
* the new mode out of it, so the panel moves and reshapes into place.
* @defaultValue "minimal"
*/
transition?: 'minimal' | 'morph';

/** Custom CSS class names. */
className?: string;
}
Expand Down
4 changes: 2 additions & 2 deletions apps/www/src/content/docs/ai-elements/chat/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const preview = {
<Reasoning.Step label="Creating the task" />
</Reasoning.Content>
</Reasoning>
<Text size="small">I created the task in Design System 2 and assigned it to you.</Text>
<Message.Bubble variant="ghost">I created the task in Design System 2 and assigned it to you.</Message.Bubble>
</Message.Content>
</Message>
</Chat.Item>
Expand Down Expand Up @@ -102,7 +102,7 @@ export const streamingDemo = {
{message.align === 'end' ? (
<Message.Bubble>{message.text}</Message.Bubble>
) : (
<Text size="small">{message.text}</Text>
<Message.Bubble variant="ghost">{message.text}</Message.Bubble>
)}
</Message.Content>
</Message>
Expand Down
36 changes: 36 additions & 0 deletions apps/www/src/content/docs/ai-elements/message/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,43 @@ export const bubbleVariantsDemo = {
<Message.Bubble variant="outline">Bordered bubble on the base background.</Message.Bubble>
<Message.Bubble variant="outline" color="accent">Accent outline bubble.</Message.Bubble>
<Message.Bubble variant="outline" color="danger">Danger outline bubble.</Message.Bubble>
</Flex>`
},
{
name: 'Ghost',
code: `<Flex direction="column" gap={3} align="start">
<Message.Bubble variant="ghost">No surface at all — plain body copy.</Message.Bubble>
<Message.Bubble variant="ghost" color="accent">Accent ghost bubble.</Message.Bubble>
<Message.Bubble variant="ghost" color="danger">Danger ghost bubble.</Message.Bubble>
</Flex>`
}
]
};

export const ghostDemo = {
type: 'code',
code: `<Flex direction="column" gap={5} style={{ width: 440 }}>
<Message align="end">
<Message.Content>
<Message.Bubble>Summarise the release notes for me.</Message.Bubble>
</Message.Content>
</Message>
<Message>
<Message.Avatar>
<Avatar size={3} radius="full" fallback="A" color="indigo" />
</Message.Avatar>
<Message.Header>Assistant</Message.Header>
<Message.Content>
<Message.Bubble variant="ghost">
Three things shipped: the timeline renderer, the AI element set, and a
rewrite of the token docs. The ghost bubble spans the whole row, so
long replies read as page content rather than as a chat bubble.
</Message.Bubble>
</Message.Content>
<Message.Footer>1:52 AM</Message.Footer>
<Message.Actions>
<IconButton size={1} aria-label="Copy message">⧉</IconButton>
</Message.Actions>
</Message>
</Flex>`
};
16 changes: 13 additions & 3 deletions apps/www/src/content/docs/ai-elements/message/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ source: packages/raystack/components/message
tag: new
---

import { preview, anatomyDemo, groupDemo, bubbleVariantsDemo } from "./demo.ts";
import { preview, anatomyDemo, groupDemo, bubbleVariantsDemo, ghostDemo } from "./demo.ts";

<Demo data={preview} />

Expand Down Expand Up @@ -72,11 +72,21 @@ appear once.

### Bubble variants

`variant` picks the surface treatment (`solid` or `outline`) and `color` the
palette (`neutral`, `accent` or `danger`).
`variant` picks the surface treatment (`solid`, `outline` or `ghost`) and
`color` the palette (`neutral`, `accent` or `danger`).

<Demo data={bubbleVariantsDemo} />

### Ghost bubbles

`variant="ghost"` removes the surface — no background, border or padding — and
the bubble runs the full width of the message row instead of shrink-wrapping.
It renders as plain body copy (Text at `size="small"`), which is the
conventional treatment for assistant replies: the reader's own messages keep a
bubble, the assistant's read as page content.

<Demo data={ghostDemo} />

## Accessibility

- `Message.Actions` reveal on hover **and** on focus-within, and stay visible
Expand Down
6 changes: 4 additions & 2 deletions apps/www/src/content/docs/ai-elements/message/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ export interface MessageProps {

export interface MessageBubbleProps {
/**
* Visual style of the message surface.
* Visual style of the message surface. `"ghost"` drops the surface
* entirely — no background, border or padding — and renders the message as
* full-width body copy.
* @defaultValue "solid"
*/
variant?: 'solid' | 'outline';
variant?: 'solid' | 'outline' | 'ghost';

/**
* Color of the message surface.
Expand Down
11 changes: 10 additions & 1 deletion apps/www/src/content/docs/ai-elements/prompt-input/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ anything else.

### Root

The `<form>` that holds the value and submit semantics.
The `<form>` that holds the value and submit semantics. The whole frame reads
as one field: pressing its dead space — the header and footer padding, or the
gap between their controls — focuses the input, while anything you put in those
slots keeps its own press. (The slots sit outside hit testing, so those presses
land on the form, which does the focusing.) `PromptInput.Textarea` registers
itself as that input on mount; pass `inputRef` when you render a custom input
instead. A consumer `onMouseDown` that calls `preventDefault()` opts out.

<auto-type-table path="./props.ts" name="PromptInputProps" />

Expand Down Expand Up @@ -120,3 +126,6 @@ menus, mention pickers.
- The composer frame carries the focus treatment — the border tints when a
child has visible focus (`:has(:focus-visible)`) — while the borderless
textarea suppresses its own duplicate focus ring.
- Click-to-focus runs on `mousedown` and cancels the default focus shift, so
focus never leaves the input and back again — a round trip that would dismiss
anything anchored to it. Keyboard focus order is untouched.
7 changes: 7 additions & 0 deletions apps/www/src/content/docs/ai-elements/prompt-input/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export interface PromptInputProps {
*/
disabled?: boolean;

/**
* The element the frame focuses when its own padding is clicked.
* `PromptInput.Textarea` registers itself here on mount; pass a ref of your
* own when you render a custom input instead. Whichever is set first wins.
*/
inputRef?: React.RefObject<HTMLElement | null>;

/** Custom CSS class names. */
className?: string;
}
Expand Down
Loading
Loading