feat(superchat): per-message action bar with sticky overflow menu - #419
feat(superchat): per-message action bar with sticky overflow menu#419garrity-miepub wants to merge 3 commits into
Conversation
- Footer action bar under each message (hover-revealed, always visible on coarse pointers): copy menu + edit, driven by an action registry - Sticky … overflow floats with the scroll and hands off to the footer bar via IntersectionObserver; menu offers "Copy as" submenu + edit - New DropdownSubmenu component with hover/keyboard flyout support; useAnchoredPosition extended with horizontal (left/right) placements - defaultCopyFormat prop (rich | markdown | plain) threaded through SuperChat/SuperChatInbox; Ctrl/Cmd-click on the copy button copies in that format without opening the menu
Deploying ui with
|
| Latest commit: |
c54f9c4
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f4ba3821.ui-6d0.pages.dev |
| Branch Preview URL: | https://feat-superchat-message-actio.ui-6d0.pages.dev |
There was a problem hiding this comment.
Pull request overview
Redesigns SuperChat per-message actions into an extensible system with a footer action bar and a sticky overflow (…) menu, and adds a new DropdownSubmenu flyout backed by horizontal placements in useAnchoredPosition.
Changes:
- Added horizontal (
left*/right*) placements and generalizedactualSideinuseAnchoredPositionto support submenu flyouts. - Implemented per-message footer actions + sticky overflow menu in SuperChat, including
defaultCopyFormatand Ctrl/Cmd-click “default copy” behavior. - Introduced
DropdownSubmenuwith hover + keyboard support, plus Storybook coverage and updated SuperChat tests.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/hooks/useAnchoredPosition.ts | Adds left/right placement support and reports actualSide for horizontal flyouts. |
| src/components/SuperChat/VirtualThread.tsx | Threads defaultCopyFormat down to message rows. |
| src/components/SuperChat/types.ts | Introduces SuperChatCopyFormat type for copy behavior configuration. |
| src/components/SuperChat/SuperChatInbox.tsx | Adds defaultCopyFormat prop and passes it into the thread. |
| src/components/SuperChat/SuperChat.tsx | Exposes defaultCopyFormat on the main component API and passes it through. |
| src/components/SuperChat/SuperChat.test.tsx | Updates copy tests for footer/overflow menu behavior and adds Ctrl/Cmd-click default-format coverage. |
| src/components/SuperChat/parts.tsx | Implements the new per-message action surfaces (footer + sticky overflow) and copy/edit action registry. |
| src/components/SuperChat/index.ts | Re-exports SuperChatCopyFormat. |
| src/components/Dropdown/index.ts | Re-exports DropdownSubmenu and its props type. |
| src/components/Dropdown/Dropdown.tsx | Adds submenu support (filtering, click-outside integration, flyout portal, keyboard interactions). |
| src/components/Dropdown/Dropdown.stories.tsx | Adds WithSubmenu Storybook story demonstrating submenu interactions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| tabIndex={-1} | ||
| data-slot="dropdown-submenu" | ||
| className={cn( | ||
| 'flex min-w-[10rem] flex-col overflow-hidden', |
There was a problem hiding this comment.
Fixed in c54f9c4 — the flyout now uses min-w-[min(10rem,calc(100vw-1rem))] so the preferred width can never beat the hook's maxWidth viewport clamp on narrow screens.
| const actionRevealClass = | ||
| 'opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 [@media(pointer:coarse)]:opacity-100'; |
There was a problem hiding this comment.
Fixed in c54f9c4 — added the new arbitrary classes to miewebUISafelist ([@media(pointer:coarse)]:opacity-100, [@media(pointer:coarse)]:pointer-events-auto, [@media(pointer:coarse)]:visible, z-[60], and the submenu min-w clamp). Note z-[60] was a pre-existing gap from the old CopyMenu, now covered.
| // Hand off to the footer bar when it's visible (desktop only). | ||
| footerVisible && | ||
| !open && | ||
| 'pointer-events-none opacity-0 [@media(pointer:coarse)]:pointer-events-auto [@media(pointer:coarse)]:opacity-100' |
There was a problem hiding this comment.
Fixed in c54f9c4 — the handed-off trigger is now hidden with invisible (plus [@media(pointer:coarse)]:visible), so it drops out of the tab order and accessibility tree while the footer bar covers the actions; coarse pointers keep it visible and interactive. Chose visibility over display:none to preserve the sticky layout box.
| * One-click format for each message's default copy action (footer copy | ||
| * button / top-level "Copy message" menu item). All formats stay reachable | ||
| * per message via the overflow menu's "Copy as" submenu. Defaults to | ||
| * `'rich'`. | ||
| */ |
There was a problem hiding this comment.
Fixed in c54f9c4 — synced the JSDoc across SuperChat, SuperChatInbox, VirtualThread, and MessageRow: the default format now applies to Ctrl/Cmd-click on the footer copy button, with all formats reachable via the copy menus.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (4)
Previously missed (1) — in code that hasn't changed since the last review.
src/hooks/useAnchoredPosition.ts:173
- In horizontal placements, the flip decision uses
floating.offsetWidth, which can under-measure whenmatchMinWidth/matchWidthare in play (the hook already computesfloatingWidthto account for that). This can cause incorrect left/right flipping in cases where the rendered width differs fromoffsetWidthat measurement time.
This issue also appears on line 212 of the same file.
src/hooks/useAnchoredPosition.ts:216
- The horizontal placement branch doesn’t apply
matchWidth/matchMinWidthto the computed style, so those options silently stop working forleft*/right*placements. For API consistency, apply the same width/minWidth handling as the vertical branch.
...(hSide === 'left'
? { right: viewportWidth - rect.left + offset }
: { left: rect.right + offset }),
maxWidth: sideWidth,
maxHeight: Math.min(boundaryHeight, maxHeight ?? Infinity),
src/components/SuperChat/parts.tsx:606
- When
footerVisible && !open, the overflow control is visually hidden viaopacity-0and pointer-disabled, but it remains tabbable and can still be activated from the keyboard (pointer-events doesn’t affect keyboard). This creates an invisible focus target; consider removing it from the tab order (and optionally the a11y tree) while it’s handed off to the footer bar.
<button
type="button"
data-slot="superchat-overflow-button"
aria-label="Message actions"
className={cn(actionButtonClass, actionRevealClass)}
src/components/SuperChat/parts.tsx:594
- New Tailwind arbitrary utilities/variants were introduced here (
z-[60]and[@media(pointer:coarse)]:…). For Tailwind 3 consumers that don’t scannode_modules, these need to be added tomiewebUISafelist(see the rationale insrc/tailwind-preset.ts) or the styles will be purged in downstream builds.
open ? 'z-[60]' : 'z-10',
// Hand off to the footer bar when it's visible (desktop only).
footerVisible &&
!open &&
'pointer-events-none opacity-0 [@media(pointer:coarse)]:pointer-events-auto [@media(pointer:coarse)]:opacity-100'
- Clamp submenu min-width to the viewport so it can't beat the positioning hook's maxWidth clamp on narrow screens - Hide the handed-off overflow trigger with visibility (not just opacity) so it drops out of the tab order for keyboard users - Safelist new arbitrary classes (pointer:coarse variants, z-[60], submenu min-width) for Tailwind 3 consumers - Sync stale defaultCopyFormat JSDoc with the Ctrl/Cmd-click behavior
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
src/components/SuperChat/parts.tsx:528
- This comment says explicit action variants live in the sticky overflow menu, but
FooterActionButtonintentionally opens a variants menu for actions withsubmenu(e.g. copy formats). The current wording is misleading for readers maintaining the action system.
src/hooks/useAnchoredPosition.ts:212
- The horizontal placement branch doesn’t apply
matchWidth/matchMinWidthto the computed style, so those options behave differently for left/right placements than they do for top/bottom placements.
setStyle({
position: 'fixed',
top,
// Anchor the edge nearest the trigger so the flyout grows away from it.
...(hSide === 'left'
| // preferred side is cramped, align vertically (-start = top edges, | ||
| // -end = bottom edges, bare = centered), and clamp to the boundary. | ||
| if (placement.startsWith('left') || placement.startsWith('right')) { | ||
| const contentWidth = floating.offsetWidth; |
Summary
Redesigns SuperChat's per-message actions from a single floating copy button into an extensible action system:
…overflow button floats with the scroll on tall messages and hands off to the footer bar viaIntersectionObserver— when the footer is in view, the overflow hides; scrolled mid-message, the overflow takes over. Its menu offers a "Copy as ▸" submenu (rich text / Markdown / plain text) plus "Edit message".DropdownSubmenucomponent with hover + keyboard (ArrowRight/ArrowLeft/Escape) flyout support, portaled with click-outside registration into the root Dropdown. Includes aWithSubmenustory.useAnchoredPositionextended with horizontal placements (right-start,left-end, etc.) with automatic left/right flipping for the submenu flyout.defaultCopyFormatprop ('rich' | 'markdown' | 'plain') onSuperChat/SuperChatInbox; Ctrl/Cmd-click on the footer copy button copies in that format immediately without opening the menu.Testing
pnpm typecheck,pnpm lint,pnpm format:fixall clean