fix: planning modal full-screen height on mobile

Two related bugs left the Planning Mode modal stuck at partial height on
mobile. useModalResizePersist replayed desktop-saved pixel dimensions
that overrode the mobile 100dvh rule, and React reconciled removed CSS
custom properties to empty string rather than calling removeProperty(),
which on iOS Safari defeated the var() fallback after keyboard dismiss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-05 07:44:56 -07:00
parent f49ec30c54
commit cee8adfb59
3 changed files with 46 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
import "./PlanningModeModal.css";
import { useState, useCallback, useEffect, useRef, useMemo, type CSSProperties } from "react";
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import type { Task, PlanningQuestion, PlanningSummary } from "@fusion/core";
import { getErrorMessage } from "@fusion/core";
import {
@@ -193,13 +193,30 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
useMobileKeyboard({ enabled: viewportMode === "mobile" });
useMobileScrollLock(viewportMode === "mobile" && isOpen);
const modalKeyboardStyle: CSSProperties = keyboardOpen
? ({
"--keyboard-overlap": `${keyboardOverlap}px`,
"--vv-offset-top": `${viewportOffsetTop}px`,
...(viewportHeight !== null ? { "--vv-height": `${viewportHeight}px` } : {}),
} as CSSProperties)
: {};
// Drive --vv-height / --keyboard-overlap / --vv-offset-top imperatively
// rather than via React's style prop. Reason: when React removes a CSS
// custom property between renders it sets it to empty string instead of
// calling removeProperty(). On iOS Safari that leaves the variable defined
// as "", so `height: var(--vv-height, 100dvh)` resolves to empty (the
// fallback only applies when the var is *undefined*) and the modal
// collapses to content height after the keyboard is dismissed.
useEffect(() => {
const node = modalRef.current;
if (!node) return;
if (keyboardOpen) {
node.style.setProperty("--keyboard-overlap", `${keyboardOverlap}px`);
node.style.setProperty("--vv-offset-top", `${viewportOffsetTop}px`);
if (viewportHeight !== null) {
node.style.setProperty("--vv-height", `${viewportHeight}px`);
} else {
node.style.removeProperty("--vv-height");
}
} else {
node.style.removeProperty("--keyboard-overlap");
node.style.removeProperty("--vv-offset-top");
node.style.removeProperty("--vv-height");
}
}, [keyboardOpen, keyboardOverlap, viewportOffsetTop, viewportHeight]);
// Mirror streamingOutput into a ref so SSE handlers can read the latest
// value without stale closure issues.
@@ -1568,7 +1585,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
role="dialog"
aria-modal="true"
>
<div className="modal modal-lg planning-modal" ref={modalRef} style={modalKeyboardStyle}>
<div className="modal modal-lg planning-modal" ref={modalRef}>
<div className="modal-header">
<div className="detail-title-row">
{mobileShowDetail && (

View File

@@ -32,6 +32,21 @@ export function useModalResizePersist(
const node = ref.current;
if (!node) return;
// On mobile, modals render full-screen via CSS (height: 100dvh) and the
// resize grip is disabled. Replaying a desktop-saved pixel height here
// would override the mobile CSS and leave the modal stuck at a partial
// height. Skip restoration; also clear any width/height left over from
// a prior desktop render of the same modal instance.
const isMobile =
typeof window !== "undefined" &&
("ontouchstart" in window || navigator.maxTouchPoints > 0) &&
window.innerWidth <= 768;
if (isMobile) {
node.style.removeProperty("width");
node.style.removeProperty("height");
return;
}
// Apply the persisted size on open.
try {
const raw = localStorage.getItem(storageKey);