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 9d27064c29
commit ba6666fe5f
3 changed files with 46 additions and 9 deletions

View File

@@ -0,0 +1,5 @@
---
"@fusion/dashboard": patch
---
Fix Planning Mode modal getting stuck at partial height on mobile. Two issues: (1) `useModalResizePersist` was replaying a desktop-saved pixel height into the inline `style` attribute, overriding the mobile `height: 100dvh` rule and leaving the modal at half-screen even before the keyboard appeared — now skipped on touch devices ≤768px wide. (2) When the iOS keyboard was dismissed, React reconciled the removed CSS custom properties (`--vv-height`, `--keyboard-overlap`, `--vv-offset-top`) by setting them to empty string instead of calling `removeProperty()`. On Safari that left `var(--vv-height, 100dvh)` resolving to empty (the fallback only kicks in when the variable is undefined), collapsing the modal to content height — now driven imperatively via `setProperty`/`removeProperty` on the modal ref.

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);