Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
* Canonical table chrome for the file viewer. Both surfaces that render a table for a file — the
* rich markdown editor (`.rich-markdown-prose table`) and the tabular previews CSV/XLSX render
* through `DataTable` (`.document-table`) — share this one definition so a table looks the same
* whichever file it came from. Chrome is shared; *sizing* is not — prose fits the document width
* while a preview sizes to its data and scrolls (see the two rules below). Editor-only concerns
* (prose block margin, fixed layout for column resizing, cell paragraph reset) stay in
* whichever file it came from. Both size to their content inside a scrolling viewport.
* Editor-only concerns (scroll wrappers, prose block margin, cell paragraph reset) stay in
* rich-markdown-editor.css.
*/

Expand All @@ -21,10 +20,6 @@
overflow: hidden;
}

.rich-markdown-prose table {
width: 100%;
}

/* A preview table is data, not prose. A CSV can carry dozens of columns, so sizing the table to the
frame (`width: 100%`) divides that frame between them and — with `overflow-wrap: anywhere` able to
break every column down to one character — renders each header a vertical column of letters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import { cn, MENU_STYLES } from '@sim/emcn'
export const BUBBLE_MENU_CLASS = cn(
MENU_STYLES.surface,
MENU_STYLES.padding,
'scrollbar-none fade-in-0 z-[var(--z-popover)] flex max-w-[calc(100%_-_1rem)] animate-in items-center gap-0.5 overflow-x-auto duration-150 ease-out motion-reduce:animate-none'
'scrollbar-none fade-in-0 absolute top-0 left-0 z-[var(--z-popover)] flex w-max max-w-[calc(100%_-_1rem)] animate-in items-center gap-0.5 overflow-x-auto duration-150 ease-out motion-reduce:animate-none'
)
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,11 @@ export function EditorBubbleMenu({
editor.commands.focus()
}

const { resolveAnchor, appendTo } = useBubbleMenuFloating(editor, scrollContainerRef)
const { resolveAnchor, appendTo, menuRef } = useBubbleMenuFloating(
editor,
scrollContainerRef,
bubbleMenuKey
)
const canFocus = useCallback(
() => hasFormattableSelection(editor, editor.state.selection.from, editor.state.selection.to),
[editor]
Expand All @@ -334,6 +338,7 @@ export function EditorBubbleMenu({

return (
<BubbleMenu
ref={menuRef}
editor={editor}
pluginKey={bubbleMenuKey}
getReferencedVirtualElement={resolveAnchor}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ export function TableBubbleMenu({ editor, scrollContainerRef }: TableBubbleMenuP
}),
})

const { resolveAnchor, appendTo } = useBubbleMenuFloating(editor, scrollContainerRef)
const { resolveAnchor, appendTo, menuRef } = useBubbleMenuFloating(
editor,
scrollContainerRef,
menuKey
)
const canFocus = useCallback(
() =>
shouldShowTableMenu({ editor }) &&
Expand All @@ -62,6 +66,7 @@ export function TableBubbleMenu({ editor, scrollContainerRef }: TableBubbleMenuP

return (
<BubbleMenu
ref={menuRef}
editor={editor}
pluginKey={menuKey}
getReferencedVirtualElement={resolveAnchor}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { autoUpdate } from '@floating-ui/dom'
import { posToDOMRect } from '@tiptap/core'
import { AllSelection, type Selection } from '@tiptap/pm/state'
import { AllSelection, type PluginKey, type Selection } from '@tiptap/pm/state'
import type { Editor } from '@tiptap/react'

/**
Expand Down Expand Up @@ -35,21 +36,42 @@ function selectionVirtualElement(editor: Editor) {
* Wires a BubbleMenu's Floating UI concerns — the selection anchor and the portal target — so the
* text and table toolbars share one source of truth and can't drift.
*
* The toolbar is appended into the editor's scroll container (which must be a positioning context)
* and left at TipTap's default `absolute` strategy, so it becomes an absolutely-positioned child of
* the scrolled content and tracks the selection through native scrolling — no scroll listener, so no
* lag or "chase" as the pane scrolls. TipTap's default `flip` still parks it above or below the
* selection at the pane edges, and the container's overflow clips it once the selection scrolls out.
* The toolbar stays inside the editor's scroll container. Its controls can resize after a selection
* changes, and a table can scroll independently of the document, so track both through Floating UI.
*/
export function useBubbleMenuFloating(
editor: Editor,
scrollContainerRef: React.RefObject<HTMLDivElement | null>
scrollContainerRef: React.RefObject<HTMLDivElement | null>,
pluginKey: PluginKey
) {
const menuRef = useRef<HTMLDivElement>(null)
const resolveAnchor = useCallback(() => selectionVirtualElement(editor), [editor])
const appendTo = useCallback(
() => scrollContainerRef.current ?? document.body,
[scrollContainerRef]
)

return { resolveAnchor, appendTo }
useEffect(() => {
const menu = menuRef.current
if (!menu) return
let stop: (() => void) | undefined
const trackSelection = () => {
stop?.()
const { node } = editor.view.domAtPos(editor.state.selection.from)
const reference = node instanceof Element ? node : (node.parentElement ?? editor.view.dom)
stop = autoUpdate(reference, menu, () => {
if (!editor.isDestroyed && menu.isConnected && menu.style.visibility !== 'hidden') {

@cubic-dev-ai cubic-dev-ai Bot Nov 14, 2026 •

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.

P2: The visibility guard can permanently suppress recovery: after Tiptap hides the menu when the selection leaves a clipping ancestor, subsequent scroll callbacks are ignored, so scrolling back cannot reposition and show the toolbar. Dispatch the position update whenever the connected menu and editor are alive, regardless of its current visibility.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/use-bubble-menu-floating.ts, line 63:

<comment>The visibility guard can permanently suppress recovery: after Tiptap hides the menu when the selection leaves a clipping ancestor, subsequent scroll callbacks are ignored, so scrolling back cannot reposition and show the toolbar. Dispatch the position update whenever the connected menu and editor are alive, regardless of its current visibility.</comment>

<file context>
@@ -35,21 +36,42 @@ function selectionVirtualElement(editor: Editor) {
+      const { node } = editor.view.domAtPos(editor.state.selection.from)
+      const reference = node instanceof Element ? node : (node.parentElement ?? editor.view.dom)
+      stop = autoUpdate(reference, menu, () => {
+        if (!editor.isDestroyed && menu.isConnected && menu.style.visibility !== 'hidden') {
+          editor.commands.setMeta(pluginKey, 'updatePosition')
+        }
</file context>
Suggested change
if (!editor.isDestroyed && menu.isConnected && menu.style.visibility !== 'hidden') {
if (!editor.isDestroyed && menu.isConnected) {
Fix with cubic

editor.commands.setMeta(pluginKey, 'updatePosition')
}
})
}
trackSelection()
editor.on('selectionUpdate', trackSelection)
return () => {
stop?.()
editor.off('selectionUpdate', trackSelection)
}
}, [editor, pluginKey])

return { resolveAnchor, appendTo, menuRef }
}
Original file line number Diff line number Diff line change
Expand Up @@ -378,19 +378,33 @@
border-radius: 4px;
}

/* Borders, padding, typography, and header fill come from document-table.css — the chrome shared
with the CSV/XLSX previews. Only the editor-specific bits live here: `table-layout: fixed` is
required by prosemirror-tables' column-resizing plugin, and `overflow: visible` lets a
collaborator's caret name label escape the table box instead of being clipped (fixed layout means
columns can't exceed the table width, so nothing else needs clipping). */
.rich-markdown-nodes table {
table-layout: fixed;
/* Keep wide tables inside their own viewport. Vertical padding keeps peer labels inside the clip. */
.rich-markdown-nodes .tableWrapper {
max-width: 100%;
overflow-x: auto;
}
Comment on lines +382 to +385

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.

P2 Scrolling clips collaborator names

A collaborator name can be clipped after the user scrolls a wide table. activateCaretLabel chooses the label’s direction only when the peer moves. A label that flipped left near the right edge keeps pointing left when scrolling moves its caret near the left edge, outside the new scroll box. Update the label’s direction on table scroll and keep it within the wrapper’s visible bounds.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


/* Content sizing avoids dividing many columns into single-character strips. */
.rich-markdown-nodes .tableWrapper > table {
table-layout: auto;
width: max-content;
overflow: visible;
}

/* The block margin is prose rhythm, so it stays with the type scale. */
.rich-markdown-prose table {
margin: 1rem 0;
.rich-markdown-nodes .tableWrapper > table > tbody > tr > th,
.rich-markdown-nodes .tableWrapper > table > tbody > tr > td {
min-width: 80px;
max-width: 320px;
overflow-wrap: anywhere;
}

.rich-markdown-prose .tableWrapper {
margin: 0.5rem 0;
padding: 0.5rem 0;
}

.rich-markdown-prose .tableWrapper > table {
margin: 0;
}

/* A diff's links are chips, so they keep their own colour instead of the prose link style. */
Expand Down
Loading
Loading