fix(dashboard): pin mobile comment composer to visualViewport above keyboard

Phone fixed bottom:overlap left the panel under the soft keyboard (layout vs
visual viewport). While open on mobile, remeasure visualViewport and pin with
top/max-height so the entry box stays visible when the keyboard opens.
This commit is contained in:
gsxdsm
2026-07-23 18:16:28 -07:00
parent b67dca4178
commit 2bebed18b2
2 changed files with 89 additions and 13 deletions

View File

@@ -1965,6 +1965,15 @@ plan actions, and a token-sized bottom inset keep all three controls inline with
+ var(--space-md)
);
}
/*
FNXC:PlanningComments 2026-07-24-06:40:
Keyboard-open phone composer is positioned via inline `top`/`max-height` from visualViewport.
Clear CSS `bottom` so it cannot fight the JS pin and leave the panel under the keyboard.
*/
.planning-comment-editor.planning-comment-editor--keyboard-open {
bottom: auto;
}
}
@media (min-width: 769px) and (max-width: 1024px) {

View File

@@ -936,24 +936,91 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
});
useMobileScrollLock(viewportMode === "mobile" && isOpen && scrollLockEnabled);
/*
FNXC:PlanningComments 2026-07-24-06:40:
Phone only: live visualViewport frame while the composer is open. `position:fixed; bottom`
is layout-viewport-relative, so when the soft keyboard opens the panel sits under the
keyboard (or outside the visible visual viewport) and looks "dismissed". Re-measure on
vv resize/scroll so we can pin with `top` inside the visual viewport instead.
*/
const [mobileVvFrame, setMobileVvFrame] = useState<{ offsetTop: number; height: number; layoutHeight: number } | null>(null);
useEffect(() => {
if (!isCommentEditorOpen || viewportMode !== "mobile") {
setMobileVvFrame(null);
return;
}
const vv = window.visualViewport;
if (!vv) {
setMobileVvFrame(null);
return;
}
const update = () => {
setMobileVvFrame({
offsetTop: vv.offsetTop,
height: vv.height,
layoutHeight: window.innerHeight || document.documentElement.clientHeight || 0,
});
};
update();
vv.addEventListener("resize", update);
vv.addEventListener("scroll", update);
window.addEventListener("resize", update);
return () => {
vv.removeEventListener("resize", update);
vv.removeEventListener("scroll", update);
window.removeEventListener("resize", update);
};
}, [isCommentEditorOpen, viewportMode]);
const commentEditorStyle = useMemo((): CSSProperties | undefined => {
if (!commentComposerNeedsKeyboardLift) return undefined;
/*
FNXC:PlanningComments 2026-07-24-06:05:
When the keyboard is open, pin the composer to the visual viewport above the keyboard.
First open used to keep the phone fixed bottom (nav-only) so the panel sat under the
keyboard until a second focus; drive bottom/max-height from live metrics instead.
FNXC:PlanningComments 2026-07-24-06:40:
Tablet: simple bottom lift (resizes-content / wider shell) — leave as keyboardOverlap.
Phone: anchor with `top` from visualViewport so the panel stays in the visible area above
the keyboard. Using only `bottom: keyboardOverlap` left the box under the keyboard on first
open (layout vs visual viewport mismatch).
*/
if (!keyboardOpen) return undefined;
const bottomPx = Math.max(keyboardOverlap, 0);
const style: CSSProperties = {
bottom: `calc(${bottomPx}px + var(--space-md))`,
};
if (viewportHeight != null && viewportHeight > 0) {
style.maxHeight = `min(${Math.round(viewportHeight * 0.55)}px, calc(var(--space-2xl) * 12))`;
if (viewportMode === "tablet") {
if (!keyboardOpen) return undefined;
const style: CSSProperties = {
bottom: `calc(${Math.max(keyboardOverlap, 0)}px + var(--space-md))`,
};
if (viewportHeight != null && viewportHeight > 0) {
style.maxHeight = `min(${Math.round(viewportHeight * 0.55)}px, calc(var(--space-2xl) * 12))`;
}
return style;
}
return style;
}, [commentComposerNeedsKeyboardLift, keyboardOpen, keyboardOverlap, viewportHeight]);
if (viewportMode !== "mobile") return undefined;
const vvHeight = mobileVvFrame?.height ?? viewportHeight ?? 0;
const vvOffsetTop = mobileVvFrame?.offsetTop ?? viewportOffsetTop ?? 0;
const layoutHeight = mobileVvFrame?.layoutHeight
?? (typeof window !== "undefined" ? (window.innerHeight || document.documentElement.clientHeight || 0) : 0);
const keyboardLikelyOpen = keyboardOpen
|| (vvHeight > 0 && layoutHeight > 0 && vvHeight < layoutHeight - 80);
if (!keyboardLikelyOpen || vvHeight <= 0) return undefined;
const gap = 12;
const maxHeight = Math.max(140, Math.min(Math.round(vvHeight * 0.5), 320));
const top = Math.round(vvOffsetTop + Math.max(gap, vvHeight - maxHeight - gap));
return {
top: `${top}px`,
bottom: "auto",
maxHeight: `${maxHeight}px`,
};
}, [
commentComposerNeedsKeyboardLift,
keyboardOpen,
keyboardOverlap,
mobileVvFrame,
viewportHeight,
viewportMode,
viewportOffsetTop,
]);
useEffect(() => {
if (!isCommentEditorOpen) return;