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