diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 7475c486..d19a5da3 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,6 +1,7 @@ import { Component, For, Show, createMemo, createEffect, createSignal, onMount, onCleanup } from "solid-js" import { Dialog } from "@kobalte/core/dialog" import { Toaster } from "solid-toast" +import useMediaQuery from "@suid/material/useMediaQuery" import AlertDialog from "./components/alert-dialog" import FolderSelectionView from "./components/folder-selection-view" import { showConfirmDialog } from "./stores/alerts" @@ -82,6 +83,46 @@ const App: Component = () => { const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false) const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0) + const phoneQuery = useMediaQuery("(max-width: 767px)") + const isPhoneLayout = createMemo(() => phoneQuery()) + + // In-memory only: hides chrome on phone; may also request browser fullscreen. + const [mobileFullscreenMode, setMobileFullscreenMode] = createSignal(false) + const [browserFullscreenActive, setBrowserFullscreenActive] = createSignal(false) + + const fullscreenSupported = () => { + if (typeof document === "undefined") return false + const el = document.documentElement as any + return Boolean(document.fullscreenEnabled) && typeof el?.requestFullscreen === "function" + } + + const syncBrowserFullscreenState = () => { + if (typeof document === "undefined") return + setBrowserFullscreenActive(Boolean(document.fullscreenElement)) + } + + const enterMobileFullscreen = async () => { + if (!isPhoneLayout()) return + setMobileFullscreenMode(true) + if (!fullscreenSupported()) return + try { + await document.documentElement.requestFullscreen() + } catch { + // Ignore: immersive mode still works without browser fullscreen. + } + } + + const exitMobileFullscreen = async () => { + if (typeof document !== "undefined" && document.fullscreenElement && typeof document.exitFullscreen === "function") { + try { + await document.exitFullscreen() + } catch { + // Ignore + } + } + setMobileFullscreenMode(false) + } + createEffect(() => { if (typeof document === "undefined") return const shouldShow = @@ -95,6 +136,31 @@ const App: Component = () => { setInstanceTabBarHeight(element?.offsetHeight ?? 0) } + onMount(() => { + if (typeof document === "undefined") return + syncBrowserFullscreenState() + document.addEventListener("fullscreenchange", syncBrowserFullscreenState) + onCleanup(() => document.removeEventListener("fullscreenchange", syncBrowserFullscreenState)) + }) + + // If the user exits browser fullscreen via browser UI, restore chrome. + let lastBrowserFullscreen = false + createEffect(() => { + const active = browserFullscreenActive() + const mode = mobileFullscreenMode() + if (mode && lastBrowserFullscreen && !active) { + setMobileFullscreenMode(false) + } + lastBrowserFullscreen = active + }) + + // If we leave phone layout (rotation / resize), restore chrome. + createEffect(() => { + if (!isPhoneLayout() && mobileFullscreenMode()) { + void exitMobileFullscreen() + } + }) + createEffect(() => { void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error)) }) @@ -410,14 +476,16 @@ const App: Component = () => { when={!hasInstances()} fallback={ <> - setRemoteAccessOpen(true)} - /> + + setRemoteAccessOpen(true)} + /> + {(instance) => { @@ -435,7 +503,10 @@ const App: Component = () => { handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)} handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)} onExecuteCommand={executeCommand} - tabBarOffset={instanceTabBarHeight()} + tabBarOffset={isPhoneLayout() && mobileFullscreenMode() ? 0 : instanceTabBarHeight()} + mobileFullscreenMode={isPhoneLayout() && mobileFullscreenMode()} + onEnterMobileFullscreen={() => void enterMobileFullscreen()} + onExitMobileFullscreen={() => void exitMobileFullscreen()} /> diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index 5238159b..3c6fa711 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -42,7 +42,7 @@ import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests" import RightPanel from "./shell/right-panel/RightPanel" import { useDrawerChrome } from "./shell/useDrawerChrome" import { getSessionStatus } from "../../stores/session-status" -import { ShieldAlert } from "lucide-solid" +import { Maximize2, Minimize2, ShieldAlert } from "lucide-solid" import type { LayoutMode } from "./shell/types" import { @@ -70,6 +70,11 @@ interface InstanceShellProps { handleSidebarModelChange: (sessionId: string, model: { providerId: string; modelId: string }) => Promise onExecuteCommand: (command: Command) => void tabBarOffset: number + + // In-memory only: mobile immersive/fullscreen mode. + mobileFullscreenMode: boolean + onEnterMobileFullscreen: () => void + onExitMobileFullscreen: () => void } const InstanceShell2: Component = (props) => { @@ -118,6 +123,7 @@ const InstanceShell2: Component = (props) => { }) const isPhoneLayout = createMemo(() => layoutMode() === "phone") + const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout()) const leftPinningSupported = createMemo(() => layoutMode() !== "phone") const rightPinningSupported = createMemo(() => layoutMode() !== "phone") @@ -585,13 +591,14 @@ const InstanceShell2: Component = (props) => { {renderLeftPanel()} - - - -
+ + + + +
= (props) => {
+ + + + + = (props) => {
-
-
-
+ + + + + + +
+ +
+