diff --git a/apps/www/src/content/docs/ai-elements/chat-panel/demo.ts b/apps/www/src/content/docs/ai-elements/chat-panel/demo.ts new file mode 100644 index 000000000..3e34072f0 --- /dev/null +++ b/apps/www/src/content/docs/ai-elements/chat-panel/demo.ts @@ -0,0 +1,351 @@ +'use client'; + +export const preview = { + type: 'code', + code: `function ChatPanelPreview() { + const RESPONSES = [ + 'Done — I created the task and assigned it to you.', + 'On it. I\\'ll ping you when the draft is ready to review.', + 'Good question — the design tokens live in packages/raystack/styles.', + 'That shipped in the last release; update and it should just work.', + 'I\\'ve noted it down. Anything else you\\'d like me to pick up?' + ]; + + const [mode, setMode] = React.useState('docked'); + const [status, setStatus] = React.useState('idle'); + const [messages, setMessages] = React.useState([ + { id: 1, role: 'user', text: 'create a task in design system 2' }, + { id: 2, role: 'assistant', text: RESPONSES[0] } + ]); + const nextIdRef = React.useRef(3); + const timerRef = React.useRef(null); + + React.useEffect(() => () => clearTimeout(timerRef.current), []); + + const handleSubmit = (value, event) => { + event.currentTarget.reset(); + setMessages(prev => [ + ...prev, + { id: nextIdRef.current++, role: 'user', text: value } + ]); + setStatus('submitted'); + timerRef.current = setTimeout(() => { + const reply = RESPONSES[Math.floor(Math.random() * RESPONSES.length)]; + setMessages(prev => [ + ...prev, + { id: nextIdRef.current++, role: 'assistant', text: reply } + ]); + setStatus('idle'); + }, 900); + }; + + const handleStop = () => { + clearTimeout(timerRef.current); + setStatus('idle'); + }; + + return ( + + + + Create task in design system 2 + + + + + + + + + {messages.map(message => ( + + + + {message.role === 'user' ? ( + {message.text} + ) : ( + {message.text} + )} + + + + ))} + {status === 'submitted' && ( + + + + Thinking… + + + + )} + + + + + + + + + + + + + + + + Main content + + The docked panel is a real flex sibling — it squeezes this content + instead of covering it. Pop it out or minimize it from the header; + floating and minimized modes are fixed to the browser viewport. + Send a message to get a (canned) reply. + + + + ); +}` +}; + +export const controlledDemo = { + type: 'code', + code: `function ControlledChatPanel() { + const [mode, setMode] = React.useState('docked'); + + return ( + + + + + + + mode: {mode} + + + + Floating and minimized modes leave this frame and pin to the + browser viewport. + + + + + Assistant + + + + + + + + + Drag the header to move the floating window; resize from any + edge or corner. + + + + + + + + ); +}` +}; + +export const boundedDragDemo = { + type: 'code', + code: `function BoundedDragChatPanel() { + const boundaryRef = React.useRef(null); + const [position, setPosition] = React.useState(null); + const [open, setOpen] = React.useState(false); + + const handleToggle = () => { + if (!open) { + // Start the floating window inside the boundary frame. + const rect = boundaryRef.current?.getBoundingClientRect(); + if (rect) setPosition({ x: rect.left + 24, y: rect.top + 24 }); + } + setOpen(!open); + }; + + return ( + + + + +
+ {open && ( + + + Assistant + + + + + Drag the header — the window can't leave the dashed frame. + + + + + )} +
+
+ ); +}` +}; + +export const resizeDemo = { + type: 'code', + code: `function ResizeAxesChatPanel() { + const [mode, setMode] = React.useState('docked'); + const [resize, setResize] = React.useState('both'); + + return ( + + + resize: + {['both', 'horizontal', 'vertical', 'none'].map(value => ( + + ))} + + + + + Pop the panel out with the header button — the floating window + pins to the browser viewport and only renders handles for the + allowed axes. By default it can only shrink below its initial + size; this demo passes a larger maxSize so it can grow. + + + + + Assistant + + + + + + + + {mode === 'floating' + ? resize === 'none' + ? 'Resizing is disabled.' + : 'Grab an edge or corner to resize me.' + : 'Pop me out to resize me.'} + + + + + + + ); +}` +}; + +export const draggableBubbleDemo = { + type: 'code', + code: `function DraggableBubbleChatPanel() { + const [mode, setMode] = React.useState('docked'); + + return ( + + + The minimized bubble accepts draggable — a short click still + restores the panel, and the dropped spot is kept the next time you + minimize. + + + + + Minimize the panel from its header, then drag the bubble that + appears at the bottom-right of the browser window. Click it to + restore the panel here. + + + + + Assistant + + + + + + + Minimize me again from the header. + + + + + + + ); +}` +}; + +export const unreadBadgeDemo = { + type: 'code', + code: `function MinimizedWithBadge() { + const [mode, setMode] = React.useState('minimized'); + + // The transform makes the frame the containing block for the panel's + // fixed positioning, so this demo's trigger pins to the frame corner + // instead of stacking on the page's other panels. + return ( + + + The minimized trigger is a slot — compose Indicator or Badge for + unread counts. Look at the bottom-right of this frame. + + + + + Click the bubble to restore the panel; minimize it again from the + header. + + + + + Assistant + + + + + + + Minimize me again from the header. + + + + 💬 + + + + + ); +}` +}; diff --git a/apps/www/src/content/docs/ai-elements/chat-panel/index.mdx b/apps/www/src/content/docs/ai-elements/chat-panel/index.mdx new file mode 100644 index 000000000..cfdfe71df --- /dev/null +++ b/apps/www/src/content/docs/ai-elements/chat-panel/index.mdx @@ -0,0 +1,173 @@ +--- +title: ChatPanel +description: A dockable, floating, minimizable window frame for chat — an inline sidebar that pops out to a draggable, resizable window or collapses to a corner bubble. +source: packages/raystack/components/chat-panel +tag: new +--- + +import { preview, controlledDemo, boundedDragDemo, resizeDemo, draggableBubbleDemo, unreadBadgeDemo } from "./demo.ts"; + + + +## Anatomy + +```tsx +import { ChatPanel } from '@raystack/apsara' + + + + + + Create task in design system 2 + + {/* consumer extras, e.g. ⋯ menu */} + + + + + {/* Chat.Messages + PromptInput */} + {/* minimized bubble */} + + +``` + +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: + +- **`docked`** — an in-flow sidebar (a flex sibling, like `SidePanel`) that + squeezes the main content rather than covering it. +- **`floating`** — the same element switched to `position: fixed`: drag it by + the header (clamped to the viewport, or to a container via `dragBoundary`; + disable with `draggable={false}`) and resize it from any edge or corner + (constrain the axes with `resize`). Out of the box resizing can only + shrink the window below its initial size — pass a larger `maxSize` to let + it grow. +- **`minimized`** — the frame collapses to `ChatPanel.Trigger`, a fixed + corner bubble; clicking it restores the previous mode. If you don't render + a trigger, minimized shows nothing and your app supplies its own affordance. + +There is deliberately no close (×) — header actions are slottable, so apps +add their own controls next to the ready-made mode triggers. + +Because floating and minimized rely on `position: fixed`, mount the panel +near the layout root: an ancestor with `transform`, `filter` or +`backdrop-filter` would break fixed positioning. + +## API Reference + +### Root + + + +### Header + +The title bar. In floating mode it is the drag handle — pointer drags on it +move the window (interactive children like buttons are excluded, as is +anything marked `data-chat-panel-no-drag`). + +### Title + +The heading (`

`), truncated with an ellipsis. + +### Actions + +A slot row at the end of the header for controls. + +### MinimizeTrigger / ExpandTrigger + +Ready-made mode buttons for `ChatPanel.Actions`, built on `IconButton`. +MinimizeTrigger switches to `minimized`; ExpandTrigger toggles between +`docked` and `floating` with a state-aware accessible name. Both render a +default icon (minus / size) that children override; ExpandTrigger children +also accept a render function receiving `{ floating }` for per-state icons: + +```tsx + + {({ floating }) => (floating ? : )} + +``` + +### Content + +The body wrapper — a flex column that gives `Chat.Messages` its bounded +height. + +### Trigger + +The minimized-state corner bubble. Children replace the default chat icon, +and `draggable` lets the bubble be dragged around the viewport — a completed +drag never triggers the restore click. + + + +## Examples + +### Controlled mode + +Control `mode` to persist it, or to open the panel from your own UI. The +floating window here is fixed to the browser viewport — pop it out and drag +its header. + + + +### Constraining the drag to a container + +Dragging is built on [dnd-kit](https://dndkit.com). By default the floating +window is clamped to the viewport; pass an element (or a ref to one) as +`dragBoundary` to confine dragging to it instead. The panel keeps +`position: fixed`, so the boundary only limits where a drag can put it. + + + +### Constraining the resize axes + +`resize` mirrors the CSS `resize` property: `both` (default), `horizontal`, +`vertical` or `none`. Only the handles for the allowed axes render. The +default `maxSize` is the initial floating size, so this demo passes a larger +one to allow growing. + + + +### Draggable minimized bubble + + + +### Unread badge on the bubble + + + +### Persisting placement + +Position and size are controllable for apps that restore the floating window +across sessions: + +```tsx + + … + +``` + +## Accessibility + +- The root renders an `