feat: ghost message bubbles, PromptInput focus, ChatPanel motion - #868
feat: ghost message bubbles, PromptInput focus, ChatPanel motion#868rohanchkrabrty wants to merge 3 commits into
Conversation
A ghost bubble drops the surface entirely — no background, border, padding
or radius — and runs the full width of the message row instead of
shrink-wrapping, which is the conventional treatment for assistant replies.
It composes textVariants({ size: 'small' }) so it renders as literal Text
output rather than a lookalike; `color` maps onto Text's colours. The grid
widens via :has(), swapping the content column's fit-content(85%) for
minmax(0, 1fr) and dropping the flexible spacer so hover actions land at the
row's far edge.
The Chat and ChatPanel examples now use it in place of the bare Text they
were reaching for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The composer reads as one field, so clicking its dead space — the padding and the gaps around the header and footer slots — now focuses the textarea, with `cursor: text` to advertise it. Handled on mousedown with preventDefault() rather than on click: focus never leaves the textarea and come back, a round trip that would dismiss anything anchored to it. Interactive targets, secondary mouse buttons, the disabled state and a consumer onMouseDown that calls preventDefault() all opt out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add the Co-pilot sparkle from the Figma icons file as CoPilotIcon and use it as the default glyph for the minimized ChatPanel bubble, replacing Radix's ChatBubbleIcon. Reusing an icons/ asset inside a component needs @svgr/rollup wired into the vitest config as a `pre` plugin, otherwise Vite resolves the .svg to a URL string and the named ReactComponent export is undefined in tests; the rollup build already had it. Mode changes flip the panel between in-flow and position: fixed, which no property can tween, so each mode now plays a transform + opacity entrance at --rs-duration-normal / --rs-ease-out: docked settles onto its edge, floating scales and lifts, the bubble pops in (retimed from fast to match). The docked slide runs inward rather than as a full-width drawer slide from off-edge — a transform still grows the nearest scroll container's scroll area, so sliding outward would flash a horizontal scrollbar in any layout that doesn't clip overflow. A data-mode-changed attribute keeps first paint static, and the floating entrance stands down while a drag or resize is in flight so it never fights the live dnd-kit transform. Everything is gated on prefers-reduced-motion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe Message component adds a Sequence Diagram(s)sequenceDiagram
participant User
participant ChatPanelTrigger
participant ChatPanelRoot
participant ChatPanelFrame
User->>ChatPanelTrigger: change panel mode
ChatPanelTrigger->>ChatPanelRoot: update mode
ChatPanelRoot->>ChatPanelFrame: pass modeChanged
ChatPanelFrame->>ChatPanelFrame: render data-mode-changed
sequenceDiagram
participant User
participant PromptInputForm
participant Textarea
User->>PromptInputForm: mousedown on frame dead space
PromptInputForm->>PromptInputForm: prevent default focus shift
PromptInputForm->>Textarea: focus textarea
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/raystack/components/chat-panel/__tests__/chat-panel.test.tsx`:
- Around line 702-712: Update the “falls back to the co-pilot icon” test to
verify the rendered icon is specifically CoPilotIcon rather than only checking
generic SVG attributes. Use a stable CoPilot-specific marker or mock CoPilotIcon
and assert its rendering, while retaining the existing fallback scenario.
In `@packages/raystack/components/chat-panel/chat-panel.module.css`:
- Around line 208-215: Scope the chat-panel-trigger-in entrance animation in the
prefers-reduced-motion rule to `.root[data-mode-changed] .trigger`, while
keeping the existing interactive transition on `.trigger` unchanged.
In `@packages/raystack/components/prompt-input/prompt-input-root.tsx`:
- Around line 60-61: Update the INTERACTIVE_TARGET selector to match all
contenteditable elements, including empty and "plaintext-only" values, so their
native focus is preserved and they are not redirected to the textarea.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: fce4fd05-638f-4379-a62f-851a2c3ed7eb
⛔ Files ignored due to path filters (1)
packages/raystack/icons/assets/co-pilot.svgis excluded by!**/*.svg
📒 Files selected for processing (19)
apps/www/src/content/docs/ai-elements/chat-panel/demo.tsapps/www/src/content/docs/ai-elements/chat-panel/index.mdxapps/www/src/content/docs/ai-elements/chat/demo.tsapps/www/src/content/docs/ai-elements/message/demo.tsapps/www/src/content/docs/ai-elements/message/index.mdxapps/www/src/content/docs/ai-elements/message/props.tsapps/www/src/content/docs/ai-elements/prompt-input/index.mdxpackages/raystack/components/chat-panel/__tests__/chat-panel.test.tsxpackages/raystack/components/chat-panel/chat-panel-root.tsxpackages/raystack/components/chat-panel/chat-panel-trigger.tsxpackages/raystack/components/chat-panel/chat-panel.module.csspackages/raystack/components/message/__tests__/message.test.tsxpackages/raystack/components/message/message-bubble.tsxpackages/raystack/components/message/message.module.csspackages/raystack/components/prompt-input/__tests__/prompt-input.test.tsxpackages/raystack/components/prompt-input/prompt-input-root.tsxpackages/raystack/components/prompt-input/prompt-input.module.csspackages/raystack/icons/index.tsxpackages/raystack/vitest.config.mjs
| it('falls back to the co-pilot icon', () => { | ||
| render(<BasicChatPanel defaultMode='minimized' />); | ||
| const icon = screen.getByTestId('bubble').querySelector('svg'); | ||
| expect(icon).not.toBeNull(); | ||
| expect(icon).toHaveAttribute('aria-hidden', 'true'); | ||
| // Drawn from currentColor so the bubble's own colour carries through. | ||
| expect(icon?.querySelector('path')).toHaveAttribute( | ||
| 'fill', | ||
| 'currentColor' | ||
| ); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the fallback assertion CoPilot-specific.
These checks only prove that some currentColor SVG rendered; reverting to another similarly styled fallback icon would still pass. Assert a stable CoPilot-specific characteristic, or mock CoPilotIcon and assert that mock is rendered.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/chat-panel/__tests__/chat-panel.test.tsx` around
lines 702 - 712, Update the “falls back to the co-pilot icon” test to verify the
rendered icon is specifically CoPilotIcon rather than only checking generic SVG
attributes. Use a stable CoPilot-specific marker or mock CoPilotIcon and assert
its rendering, while retaining the existing fallback scenario.
| @media (prefers-reduced-motion: no-preference) { | ||
| .trigger { | ||
| animation: chat-panel-trigger-in var(--rs-duration-fast) var(--rs-ease-out); | ||
| /* Same tempo as the docked/floating entrances above, so the three modes | ||
| read as one transition rather than three unrelated ones. */ | ||
| animation: chat-panel-trigger-in var(--rs-duration-normal) | ||
| var(--rs-ease-out); | ||
| transition: var(--rs-transition-interactive); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Gate the minimized-bubble entrance on a mode change.
.trigger animates whenever it mounts, so an initially minimized panel still animates despite lacking data-mode-changed. Keep the transition on .trigger, but scope the entrance animation to .root[data-mode-changed] .trigger.
Proposed fix
`@media` (prefers-reduced-motion: no-preference) {
.trigger {
- /* Same tempo as the docked/floating entrances above, so the three modes
- read as one transition rather than three unrelated ones. */
- animation: chat-panel-trigger-in var(--rs-duration-normal)
- var(--rs-ease-out);
transition: var(--rs-transition-interactive);
}
+ .root[data-mode-changed] .trigger {
+ animation: chat-panel-trigger-in var(--rs-duration-normal)
+ var(--rs-ease-out);
+ }
+
.trigger:active {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @media (prefers-reduced-motion: no-preference) { | |
| .trigger { | |
| animation: chat-panel-trigger-in var(--rs-duration-fast) var(--rs-ease-out); | |
| /* Same tempo as the docked/floating entrances above, so the three modes | |
| read as one transition rather than three unrelated ones. */ | |
| animation: chat-panel-trigger-in var(--rs-duration-normal) | |
| var(--rs-ease-out); | |
| transition: var(--rs-transition-interactive); | |
| } | |
| `@media` (prefers-reduced-motion: no-preference) { | |
| .trigger { | |
| transition: var(--rs-transition-interactive); | |
| } | |
| .root[data-mode-changed] .trigger { | |
| animation: chat-panel-trigger-in var(--rs-duration-normal) | |
| var(--rs-ease-out); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/chat-panel/chat-panel.module.css` around lines
208 - 215, Scope the chat-panel-trigger-in entrance animation in the
prefers-reduced-motion rule to `.root[data-mode-changed] .trigger`, while
keeping the existing interactive transition on `.trigger` unchanged.
| const INTERACTIVE_TARGET = | ||
| 'button, a[href], input, textarea, select, label, [contenteditable="true"], [tabindex]:not([tabindex="-1"])'; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle all editable-content targets.
contenteditable is also valid with an empty value or "plaintext-only". Those editors are currently treated as frame dead space, so their native focus is prevented and focus moves to the textarea instead.
Proposed fix
const INTERACTIVE_TARGET =
- 'button, a[href], input, textarea, select, label, [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
+ 'button, a[href], input, textarea, select, label, [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const INTERACTIVE_TARGET = | |
| 'button, a[href], input, textarea, select, label, [contenteditable="true"], [tabindex]:not([tabindex="-1"])'; | |
| const INTERACTIVE_TARGET = | |
| 'button, a[href], input, textarea, select, label, [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])'; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/raystack/components/prompt-input/prompt-input-root.tsx` around lines
60 - 61, Update the INTERACTIVE_TARGET selector to match all contenteditable
elements, including empty and "plaintext-only" values, so their native focus is
preserved and they are not redirected to the textarea.
Summary
variant="ghost"toMessage.Bubble— no background, border or padding, spanning the full message row. It composestextVariants({ size: 'small' })so it renders as literalTextoutput rather than a lookalike, and the grid widens via:has()(content columnfit-content(85%)→minmax(0, 1fr), spacer dropped) so hover actions land at the row's far edge. The Chat and ChatPanel examples now use it in place of the bareTextthey were reaching for.PromptInputframe's dead space focuses the textarea, withcursor: textto advertise it. Handled onmousedownwithpreventDefault()rather than onclick, so focus never leaves the textarea and comes back — a round trip that would dismiss anything anchored to it. Interactive targets, secondary mouse buttons, the disabled state and a consumeronMouseDownthat callspreventDefault()all opt out.CoPilotIcon, and use it as the default glyph for the minimizedChatPanelbubble (was Radix'sChatBubbleIcon).ChatPanelmode changes:positionflips between in-flow andfixed, which no property can tween, so each mode plays a transform + opacity entrance at--rs-duration-normal/--rs-ease-out. The docked slide runs inward rather than as a full-width drawer slide from off-edge — a transform still grows the nearest scroll container's scroll area, so sliding outward would flash a horizontal scrollbar in any layout that doesn't clip overflow. Adata-mode-changedattribute keeps first paint static, the floating entrance stands down mid-drag/resize, and everything is gated onprefers-reduced-motion.@svgr/rollupinto the vitest config as apreplugin — reusing anicons/asset inside a component needs it, or Vite resolves the.svgto a URL string and the namedReactComponentexport is undefined in tests. Docs updated across Message, PromptInput and ChatPanel.