feat: implement mobile terminal UX
- Add mobile viewport detection hook - Add virtual keyboard detection with fallback - Add auto-hide hook for header/keys strip - Add special keys mapping hook - Create MobileTerminalHeader, SpecialKeysStrip, SpecialKeysPanel components - Create MobileTerminalWrapper component - Update TerminalComponent with mobile support, font scaling, copy/paste, reconnection - Update AppShell to hide chrome on mobile terminal pages - Update TerminalPage to use MobileTerminalWrapper - Add comprehensive mobile terminal styles - TypeScript check passes - Build succeeds
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { Link, NavLink, Outlet } from "react-router-dom";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Link, NavLink, Outlet, useLocation } from "react-router-dom";
|
||||
|
||||
import { getUserSessions } from "../api/sessions";
|
||||
import type { Session } from "../api/sessions";
|
||||
import { useTheme } from "../hooks/use-theme";
|
||||
import { useAuth } from "../state/auth";
|
||||
import { useSessions } from "../state/sessions";
|
||||
import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
||||
import { Icon } from "./icon";
|
||||
import type { IconName } from "../utils/icons";
|
||||
|
||||
@@ -39,6 +40,11 @@ export const AppShell = () => {
|
||||
useTheme();
|
||||
const { user, logout } = useAuth();
|
||||
const { sessions, setAllSessions } = useSessions();
|
||||
const location = useLocation();
|
||||
const isMobile = useMobileViewport();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const isMobileTerminal = isMobile && location.pathname.includes("/instances/") && location.pathname.includes("/terminal");
|
||||
|
||||
const loadSessions = useCallback(async () => {
|
||||
try {
|
||||
@@ -58,6 +64,19 @@ export const AppShell = () => {
|
||||
return () => clearInterval(interval);
|
||||
}, [loadSessions]);
|
||||
|
||||
// Close mobile menu on route change
|
||||
useEffect(() => {
|
||||
setMobileMenuOpen(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
if (isMobileTerminal) {
|
||||
return (
|
||||
<div className="shell mobile-terminal-shell">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<header className="shell-header">
|
||||
@@ -82,7 +101,17 @@ export const AppShell = () => {
|
||||
</header>
|
||||
|
||||
<div className="shell-body">
|
||||
<aside className="shell-nav" aria-label="Primary navigation">
|
||||
<aside className={`shell-nav ${mobileMenuOpen ? "mobile-open" : ""}`} aria-label="Primary navigation">
|
||||
{isMobile && (
|
||||
<button
|
||||
className="mobile-menu-close"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
type="button"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const activeCount = sessions.filter((s) => s.status === "running").length;
|
||||
return (
|
||||
@@ -112,6 +141,13 @@ export const AppShell = () => {
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{isMobile && mobileMenuOpen && (
|
||||
<div
|
||||
className="mobile-menu-overlay"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main className="shell-content">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user