Skip to content

feat(superchat): per-message action bar with sticky overflow menu - #419

Draft
garrity-miepub wants to merge 3 commits into
mainfrom
feat/superchat-message-actions
Draft

feat(superchat): per-message action bar with sticky overflow menu#419
garrity-miepub wants to merge 3 commits into
mainfrom
feat/superchat-message-actions

Conversation

@garrity-miepub

Copy link
Copy Markdown
Collaborator

Summary

Redesigns SuperChat's per-message actions from a single floating copy button into an extensible action system:

  • Footer action bar under each message bubble — hover-revealed on fine pointers, always visible on coarse (touch) pointers. Currently hosts the copy menu and edit button, driven by a per-message action registry so future actions slot in.
  • Sticky overflow button floats with the scroll on tall messages and hands off to the footer bar via IntersectionObserver — 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".
  • New DropdownSubmenu component with hover + keyboard (ArrowRight/ArrowLeft/Escape) flyout support, portaled with click-outside registration into the root Dropdown. Includes a WithSubmenu story.
  • useAnchoredPosition extended with horizontal placements (right-start, left-end, etc.) with automatic left/right flipping for the submenu flyout.
  • defaultCopyFormat prop ('rich' | 'markdown' | 'plain') on SuperChat / 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:fix all clean
  • SuperChat suite: 50 passed (incl. new footer copy menu, overflow submenu, and Ctrl-click default-format tests); Dropdown: 11 passed; useAnchoredPosition: 12 passed
  • Verified visually in Storybook (Playground + Long stories): footer reveal on hover, copy menu, sticky overflow handoff, submenu flyout flip near viewport edge

- 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
Copilot AI lite review requested due to automatic review settings September 5, 2026 01:46
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 5, 2026

Copy link
Copy Markdown

Deploying ui with  Cloudflare Pages  Cloudflare Pages

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

View logs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 generalized actualSide in useAnchoredPosition to support submenu flyouts.
  • Implemented per-message footer actions + sticky overflow menu in SuperChat, including defaultCopyFormat and Ctrl/Cmd-click “default copy” behavior.
  • Introduced DropdownSubmenu with 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.

Comment thread src/components/Dropdown/Dropdown.tsx Outdated
tabIndex={-1}
data-slot="dropdown-submenu"
className={cn(
'flex min-w-[10rem] flex-col overflow-hidden',

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +308 to +309
const actionRevealClass =
'opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 [@media(pointer:coarse)]:opacity-100';

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/components/SuperChat/parts.tsx Outdated
// 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'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/components/SuperChat/SuperChat.tsx Outdated
Comment on lines +79 to +83
* 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'`.
*/

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI review requested due to automatic review settings September 5, 2026 01:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 when matchMinWidth/matchWidth are in play (the hook already computes floatingWidth to account for that). This can cause incorrect left/right flipping in cases where the rendered width differs from offsetWidth at 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 / matchMinWidth to the computed style, so those options silently stop working for left*/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 via opacity-0 and 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 scan node_modules, these need to be added to miewebUISafelist (see the rationale in src/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
Copilot AI review requested due to automatic review settings September 5, 2026 02:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FooterActionButton intentionally opens a variants menu for actions with submenu (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 / matchMinWidth to 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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants