feat(FN-2927): merge fusion/fn-2927

- test(FN-2927): complete Step 3 — cover keyboardOpen mobile nav behavior
- feat(FN-2927): complete Step 2 — wire keyboard state into mobile nav
- feat(FN-2927): complete Step 1 — add keyboardOpen hide behavior
- feat(FN-2916): streamline Cloudflare quick tunnel settings UX
- feat(FN-2913): merge fusion/fn-2913

Fusion-Task-Id: FN-2927
This commit is contained in:
Fusion
2026-04-29 00:21:51 -07:00
committed by gsxdsm
parent 3ca287851e
commit 3d277afb90
3 changed files with 21 additions and 2 deletions

View File

@@ -38,6 +38,7 @@ import { useAppSettings } from "./hooks/useAppSettings";
import { useDeepLink } from "./hooks/useDeepLink";
import { useFavorites } from "./hooks/useFavorites";
import { useAuthOnboarding } from "./hooks/useAuthOnboarding";
import { useMobileKeyboard } from "./hooks/useMobileKeyboard";
import { useSetupReadiness } from "./hooks/useSetupReadiness";
import { useUpdateCheck } from "./hooks/useUpdateCheck";
import { useViewState, type TaskView } from "./hooks/useViewState";
@@ -272,6 +273,10 @@ function AppInner() {
const viewportMode = useViewportMode();
const isMobile = viewportMode === "mobile";
const { keyboardOverlap } = useMobileKeyboard({ enabled: isMobile });
// Keyboard visibility controls both MobileNavBar rendering and whether
// the project content reserves bottom padding for the mobile nav bar.
const mobileKeyboardOpen = isMobile && keyboardOverlap > 0;
// App-level mailbox unread count state (used for header/mobile nav badges)
const [mailboxUnreadCount, setMailboxUnreadCount] = useState(0);
@@ -902,7 +907,7 @@ function AppInner() {
/>
)}
<div
className={`project-content${viewMode === "project" && currentProject ? " project-content--with-footer" : ""}${isMobile ? " project-content--with-mobile-nav" : ""}`}
className={`project-content${viewMode === "project" && currentProject ? " project-content--with-footer" : ""}${isMobile && !mobileKeyboardOpen ? " project-content--with-mobile-nav" : ""}`}
>
{renderMainContent()}
</div>
@@ -926,6 +931,7 @@ function AppInner() {
onChangeView={viewMode === "project" && currentProject ? handleTaskViewChange : () => {}}
footerVisible={viewMode === "project" && !!currentProject}
modalOpen={modalManager.anyModalOpen}
keyboardOpen={mobileKeyboardOpen}
onOpenSettings={handleOpenSettings}
onOpenActivityLog={modalManager.openActivityLog}
onOpenSystemStats={modalManager.openSystemStats}

View File

@@ -40,6 +40,8 @@ export interface MobileNavBarProps {
footerVisible: boolean;
/** Whether any full-screen modal is currently open (hides the tab bar) */
modalOpen?: boolean;
/** Whether the on-screen mobile keyboard is open (hides the tab bar) */
keyboardOpen?: boolean;
// Navigation handlers
onOpenSettings?: () => void;
onOpenActivityLog?: () => void;
@@ -89,6 +91,7 @@ export function MobileNavBar({
onChangeView,
footerVisible,
modalOpen = false,
keyboardOpen = false,
onOpenSettings,
onOpenActivityLog,
onOpenSystemStats,
@@ -168,7 +171,7 @@ export function MobileNavBar({
return () => document.removeEventListener("keydown", onKeyDown);
}, [isMoreOpen]);
if (mode !== "mobile" || modalOpen) {
if (mode !== "mobile" || modalOpen || keyboardOpen) {
return null;
}

View File

@@ -367,6 +367,16 @@ describe("MobileNavBar", () => {
expect(container.querySelector(".mobile-nav-bar")).toBeNull();
});
it("returns null when keyboardOpen is true on mobile", () => {
const { container } = render(<MobileNavBar {...createDefaultProps()} keyboardOpen={true} />);
expect(container.querySelector(".mobile-nav-bar")).toBeNull();
});
it("renders nav bar when keyboardOpen is false on mobile", () => {
const { container } = render(<MobileNavBar {...createDefaultProps()} keyboardOpen={false} />);
expect(container.querySelector(".mobile-nav-bar")).not.toBeNull();
});
it("applies footer-visible class when footer is shown", () => {
const { container } = render(<MobileNavBar {...createDefaultProps()} footerVisible={true} />);
expect(container.querySelector(".mobile-nav-bar--with-footer")).not.toBeNull();