From 62c52972fccbbe61a7e7a98e185d1968eebd3146 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 23 Jul 2026 17:19:26 -0700 Subject: [PATCH] fix(dashboard): keep mobile plan-review selection comments on-screen Plan review Add-comment controls now track document-level selectionchange so they appear as soon as text is selected and dismiss when the selection ends. On mobile the trigger and composer are fixed above the nav (with width auto) so operators no longer need to scroll to reach them. --- .../fix-planning-mobile-selection-comment.md | 7 + .../app/components/PlanningModeModal.css | 68 ++++++++- .../app/components/PlanningModeModal.tsx | 132 ++++++++++++++---- .../__tests__/PlanningModeModal.css.test.ts | 13 +- .../PlanningModeModal.planning-flow.test.tsx | 79 ++++++++--- ...PlanningModeModal.ui-interactions.test.tsx | 6 +- .../__tests__/planning-browser-e2e.test.ts | 65 ++++++++- 7 files changed, 310 insertions(+), 60 deletions(-) create mode 100644 .changeset/fix-planning-mobile-selection-comment.md diff --git a/.changeset/fix-planning-mobile-selection-comment.md b/.changeset/fix-planning-mobile-selection-comment.md new file mode 100644 index 0000000000..1215f7eaa6 --- /dev/null +++ b/.changeset/fix-planning-mobile-selection-comment.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep Planning plan-review Add-comment controls on-screen on mobile after text selection. +category: fix +dev: Selectioncapture uses document-level selectionchange; mobile trigger and composer are position:fixed above the nav with width auto so they stay in the visual viewport and dismiss when the selection collapses. diff --git a/packages/dashboard/app/components/PlanningModeModal.css b/packages/dashboard/app/components/PlanningModeModal.css index c16a3fe3a9..f20ecd3acb 100644 --- a/packages/dashboard/app/components/PlanningModeModal.css +++ b/packages/dashboard/app/components/PlanningModeModal.css @@ -1794,6 +1794,13 @@ its established Refine and Proceed action hierarchy. background: var(--card); } +/* +FNXC:PlanningComments 2026-07-23-17:05: +Desktop (≥769px) keeps the in-document trigger under the plan markdown. Mobile (≤768px) lifts +the counterpart into a fixed bottom bar in the visual viewport so it never requires scrolling +the plan document or action rail after a touch selection. +*/ + .planning-comment-quote, .planning-comment-tray blockquote, .planning-comment-tray p { @@ -1886,26 +1893,75 @@ plan actions, and a token-sized bottom inset keep all three controls inline with @media (max-width: 768px) { /* FNXC:PlanningComments 2026-07-31-00:00: - A selected quote needs a reachable mobile action beside Refine and Proceed, not a control lost - in the scrollable document. At 769px and above the selection-adjacent document trigger stays - canonical. These established 768px/1024px media boundaries are permitted; new dimensions use - only existing design tokens. + A selected quote needs a reachable mobile action, not a control lost in the scrollable + document. At 769px and above the selection-adjacent document trigger stays canonical. + + FNXC:PlanningComments 2026-07-23-17:05: + The mobile trigger is position:fixed to the selection midpoint and clamped into the visual + viewport (including safe-area + mobile-nav clearance) so it always appears after a selection + and can be dismissed by selectionchange when the selection collapses — without scrolling the + screen. These established 768px/1024px media boundaries are permitted; new dimensions use only + existing design tokens. */ .planning-add-comment--document { display: none; } - .planning-add-comment--mobile { - display: inline-flex; + /* + FNXC:PlanningComments 2026-07-23-17:05: + Pin the mobile Add-comment control to the visual viewport above the mobile nav/safe-area. + A fixed bar appears immediately after selectionchange and dismisses when the selection + collapses — no document or action-rail scroll required. + + The mobile plan-actions button rule forces width 100 percent. With position fixed that 100 + percent is the viewport width, and combined with left/right insets the control overflowed past + the right edge (measured 780px wide in a 768px viewport). Use a higher-specificity selector and + width auto so left+right define the used width and the bar stays fully on-screen. + */ + .planning-plan-actions .btn.planning-add-comment--mobile { + display: flex; + position: fixed; + left: var(--space-md); + right: var(--space-md); + bottom: calc( + var(--mobile-nav-height, 44px) + + max(env(safe-area-inset-bottom, 0px), 12px) + + var(--space-md) + ); + z-index: var(--z-popover); + width: auto; + max-width: none; margin-top: 0; + justify-content: center; + box-shadow: var(--shadow-md); } .planning-comment-tray li { grid-template-columns: minmax(0, 1fr) auto; } + /* + FNXC:PlanningComments 2026-07-23-17:05: + The comment composer used to flow at the end of the plan markdown, so opening it after a mid- + document selection required scrolling to the document foot. Pin it to the visual viewport above + the mobile nav/safe-area (and above the selection trigger slot) so Cancel / Add comment stay + reachable without scrolling. + */ .planning-comment-editor { + position: fixed; + left: var(--space-md); + right: var(--space-md); + bottom: calc( + var(--mobile-nav-height, 44px) + + max(env(safe-area-inset-bottom, 0px), 12px) + + var(--space-md) + ); + z-index: calc(var(--z-popover) + 1); + margin-top: 0; + max-height: min(50dvh, calc(var(--space-2xl) * 12)); + overflow: auto; padding: var(--space-md); + box-shadow: var(--shadow-lg); } } diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index c1ed11fb32..b3546a402f 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -678,21 +678,36 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat const addCommentTriggerRef = useRef(null); const mobileAddCommentTriggerRef = useRef(null); const restoreCommentTriggerFocusRef = useRef(false); + const isCommentEditorOpenRef = useRef(false); + + const setCommentEditorOpen = useCallback((open: boolean) => { + /* + FNXC:PlanningComments 2026-07-23-17:05: + Flip the lock ref synchronously with the state write. Waiting for useEffect left a window + where focusing the suggestion field emitted selectionchange while the ref was still false, + which cleared the quote and unmounted the composer before the operator could type. + */ + isCommentEditorOpenRef.current = open; + setIsCommentEditorOpen(open); + }, []); const focusAddCommentTrigger = useCallback(() => { - const isMobile = window.matchMedia?.("(max-width: 768px)").matches ?? false; - (isMobile ? mobileAddCommentTriggerRef : addCommentTriggerRef).current?.focus(); + const isMobileViewport = window.matchMedia?.("(max-width: 768px)").matches ?? false; + (isMobileViewport ? mobileAddCommentTriggerRef : addCommentTriggerRef).current?.focus(); }, []); /* FNXC:PlanningComments 2026-07-23-09:30: - Closing the conditional comment editor unmounts its trigger during the state transition. Restore focus only in the post-render effect, after Cancel or Add comment remounts the desktop or mobile trigger. + Closing the conditional comment editor unmounts its trigger during the state transition. Restore focus only in the post-render effect when Cancel leaves a live plan selection and remounts the trigger. */ useEffect(() => { - if (!isCommentEditorOpen && restoreCommentTriggerFocusRef.current) { - restoreCommentTriggerFocusRef.current = false; - focusAddCommentTrigger(); - } + if (isCommentEditorOpen || !restoreCommentTriggerFocusRef.current) return; + restoreCommentTriggerFocusRef.current = false; + queueMicrotask(() => { + if (addCommentTriggerRef.current || mobileAddCommentTriggerRef.current) { + focusAddCommentTrigger(); + } + }); }, [focusAddCommentTrigger, isCommentEditorOpen]); useEffect(() => { @@ -700,11 +715,18 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat setContextualComments([]); setSelectedPlanQuote(null); setCommentDraft(""); - setIsCommentEditorOpen(false); - }, [selectedSessionId]); + setCommentEditorOpen(false); + }, [selectedSessionId, setCommentEditorOpen]); useEffect(() => { - if (isCommentEditorOpen) commentInputRef.current?.focus(); + if (isCommentEditorOpen) { + /* + FNXC:PlanningComments 2026-07-23-17:05: + The mobile composer is position:fixed; preventScroll keeps the plan markdown under the + selection instead of scrolling the document to the editor's former in-flow slot. + */ + commentInputRef.current?.focus({ preventScroll: true }); + } }, [isCommentEditorOpen]); useEffect(() => { @@ -2649,28 +2671,76 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat } }, [connectToPlanningStream, projectId, refinementInstructions, t, view, workspaceQuestion]); + /* + FNXC:PlanningComments 2026-07-23-17:05: + Plan selection must track document-level selectionchange (not only mouseup/touchend on the + markdown root). On mobile, the selection often finalizes after touchend, so waiting on the + element handlers left the Add-comment control missing until a later scroll/gesture. Collapsed + or out-of-plan selections clear the quote so the control dismisses when the selection is done. + While the comment editor is open the quote stays locked — opening the editor collapses the + native selection, and clearing here would unmount the composer mid-edit. + */ const capturePlanSelection = useCallback(() => { + if (isCommentEditorOpenRef.current) return; + const selection = window.getSelection(); const root = planDocumentRef.current; - if (!selection || selection.rangeCount === 0 || !root || !root.contains(selection.anchorNode) || !root.contains(selection.focusNode)) { + if ( + !selection + || selection.rangeCount === 0 + || selection.isCollapsed + || !root + || !root.contains(selection.anchorNode) + || !root.contains(selection.focusNode) + ) { setSelectedPlanQuote(null); return; } + const quote = selection.toString().replace(/\s+/g, " ").trim(); setSelectedPlanQuote(quote || null); }, []); + useEffect(() => { + document.addEventListener("selectionchange", capturePlanSelection); + document.addEventListener("mouseup", capturePlanSelection); + document.addEventListener("touchend", capturePlanSelection); + document.addEventListener("keyup", capturePlanSelection); + return () => { + document.removeEventListener("selectionchange", capturePlanSelection); + document.removeEventListener("mouseup", capturePlanSelection); + document.removeEventListener("touchend", capturePlanSelection); + document.removeEventListener("keyup", capturePlanSelection); + }; + }, [capturePlanSelection]); + + useEffect(() => { + /* + FNXC:PlanningComments 2026-07-23-17:05: + Composer focus collapses the native selection. When the editor closes, re-sync so a done + selection dismisses the Add-comment control instead of leaving a sticky orphaned quote. + */ + if (!isCommentEditorOpen) { + capturePlanSelection(); + } + }, [capturePlanSelection, isCommentEditorOpen]); + const handleAddContextualComment = useCallback(() => { const quote = selectedPlanQuote; const suggestion = commentDraft.trim(); if (!quote || !suggestion) return; setContextualComments((comments) => [...comments, { quote, suggestion }]); setCommentDraft(""); - // FNXC:PlanningComments 2026-07-23-09:30: Keep the quote while closing the editor so its conditional trigger remounts and can receive restored focus after adding a comment. - restoreCommentTriggerFocusRef.current = true; - setIsCommentEditorOpen(false); + /* + FNXC:PlanningComments 2026-07-23-17:05: + Adding a comment clears the native selection, so the quote must dismiss with it. Do not restore + focus onto a remounted trigger — that path only applies when Cancel keeps an active selection. + */ + restoreCommentTriggerFocusRef.current = false; + setCommentEditorOpen(false); + setSelectedPlanQuote(null); window.getSelection()?.removeAllRanges(); - }, [commentDraft, selectedPlanQuote]); + }, [commentDraft, selectedPlanQuote, setCommentEditorOpen]); const handleSubmitContextualComments = useCallback(async () => { const sessionId = currentSessionIdRef.current; @@ -2941,13 +3011,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat Contextual quotes must originate exclusively in rendered plan Markdown. The editor and action controls remain outside this selection root so typing or selecting a suggestion cannot replace the captured plan quote. + + FNXC:PlanningComments 2026-07-23-17:05: + Selection capture is document-level (selectionchange/mouseup/touchend/keyup). This root + only scopes which nodes count as plan text — handlers are not required on the element. */} -
+
setIsCommentEditorOpen(true)} + onMouseDown={(event) => event.preventDefault()} + onClick={() => setCommentEditorOpen(true)} > {t("planning.addComment", "Add comment to selection")} @@ -2973,7 +3043,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat