mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-27 23:05:30 +08:00
Compare commits
9 Commits
camoufox-n
...
fix/mobile
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a187a21ef2 | ||
|
|
7d925e95ab | ||
|
|
fb06a0ce20 | ||
|
|
da27dd93db | ||
|
|
5459d7c60e | ||
|
|
645914f2c8 | ||
|
|
c7a0891f56 | ||
|
|
241ed1da54 | ||
|
|
ee002fc3f7 |
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<title>CloudCLI UI</title>
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ITerminalOptions } from '@xterm/xterm';
|
||||
|
||||
export const CODEX_DEVICE_AUTH_URL = 'https://auth.openai.com/codex/device';
|
||||
export const SHELL_RESTART_DELAY_MS = 200;
|
||||
export const TERMINAL_INIT_DELAY_MS = 100;
|
||||
export const TERMINAL_RESIZE_DELAY_MS = 50;
|
||||
|
||||
@@ -24,7 +24,6 @@ type UseShellConnectionOptions = {
|
||||
autoConnect: boolean;
|
||||
closeSocket: () => void;
|
||||
clearTerminalScreen: () => void;
|
||||
setAuthUrl: (nextAuthUrl: string) => void;
|
||||
onOutputRef?: MutableRefObject<(() => void) | null>;
|
||||
};
|
||||
|
||||
@@ -49,7 +48,6 @@ export function useShellConnection({
|
||||
autoConnect,
|
||||
closeSocket,
|
||||
clearTerminalScreen,
|
||||
setAuthUrl,
|
||||
onOutputRef,
|
||||
}: UseShellConnectionOptions): UseShellConnectionResult {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
@@ -100,14 +98,8 @@ export function useShellConnection({
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'auth_url' || message.type === 'url_open') {
|
||||
const nextAuthUrl = typeof message.url === 'string' ? message.url : '';
|
||||
if (nextAuthUrl) {
|
||||
setAuthUrl(nextAuthUrl);
|
||||
}
|
||||
}
|
||||
},
|
||||
[handleProcessCompletion, onOutputRef, setAuthUrl, terminalRef],
|
||||
[handleProcessCompletion, onOutputRef, terminalRef],
|
||||
);
|
||||
|
||||
const connectWebSocket = useCallback(
|
||||
@@ -133,7 +125,6 @@ export function useShellConnection({
|
||||
setIsConnected(true);
|
||||
setIsConnecting(false);
|
||||
connectingRef.current = false;
|
||||
setAuthUrl('');
|
||||
|
||||
window.setTimeout(() => {
|
||||
const currentTerminal = terminalRef.current;
|
||||
@@ -196,7 +187,6 @@ export function useShellConnection({
|
||||
isPlainShellRef,
|
||||
selectedProjectRef,
|
||||
selectedSessionRef,
|
||||
setAuthUrl,
|
||||
terminalRef,
|
||||
wsRef,
|
||||
],
|
||||
@@ -225,8 +215,7 @@ export function useShellConnection({
|
||||
setIsConnecting(false);
|
||||
connectingRef.current = false;
|
||||
forceRestartOnInitRef.current = false;
|
||||
setAuthUrl('');
|
||||
}, [clearTerminalScreen, closeSocket, setAuthUrl]);
|
||||
}, [clearTerminalScreen, closeSocket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { FitAddon } from '@xterm/addon-fit';
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
|
||||
import type { UseShellRuntimeOptions, UseShellRuntimeResult } from '../types/types';
|
||||
import { copyTextToClipboard } from '../../../utils/clipboard';
|
||||
|
||||
import { useShellConnection } from './useShellConnection';
|
||||
import { useShellTerminal } from './useShellTerminal';
|
||||
|
||||
@@ -22,15 +23,11 @@ export function useShellRuntime({
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
const [authUrl, setAuthUrl] = useState('');
|
||||
const [authUrlVersion, setAuthUrlVersion] = useState(0);
|
||||
|
||||
const selectedProjectRef = useRef(selectedProject);
|
||||
const selectedSessionRef = useRef(selectedSession);
|
||||
const initialCommandRef = useRef(initialCommand);
|
||||
const isPlainShellRef = useRef(isPlainShell);
|
||||
const onProcessCompleteRef = useRef(onProcessComplete);
|
||||
const authUrlRef = useRef('');
|
||||
const lastSessionIdRef = useRef<string | null>(selectedSession?.id ?? null);
|
||||
|
||||
// Keep mutable values in refs so websocket handlers always read current data.
|
||||
@@ -42,12 +39,6 @@ export function useShellRuntime({
|
||||
onProcessCompleteRef.current = onProcessComplete;
|
||||
}, [selectedProject, selectedSession, initialCommand, isPlainShell, onProcessComplete]);
|
||||
|
||||
const setCurrentAuthUrl = useCallback((nextAuthUrl: string) => {
|
||||
authUrlRef.current = nextAuthUrl;
|
||||
setAuthUrl(nextAuthUrl);
|
||||
setAuthUrlVersion((previous) => previous + 1);
|
||||
}, []);
|
||||
|
||||
const closeSocket = useCallback(() => {
|
||||
const activeSocket = wsRef.current;
|
||||
if (!activeSocket) {
|
||||
@@ -64,32 +55,6 @@ export function useShellRuntime({
|
||||
wsRef.current = null;
|
||||
}, []);
|
||||
|
||||
const openAuthUrlInBrowser = useCallback((url = authUrlRef.current) => {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const popup = window.open(url, '_blank');
|
||||
if (popup) {
|
||||
try {
|
||||
popup.opener = null;
|
||||
} catch {
|
||||
// Ignore cross-origin restrictions when trying to null opener.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const copyAuthUrlToClipboard = useCallback(async (url = authUrlRef.current) => {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return copyTextToClipboard(url);
|
||||
}, []);
|
||||
|
||||
const { isInitialized, clearTerminalScreen, disposeTerminal } = useShellTerminal({
|
||||
terminalContainerRef,
|
||||
terminalRef,
|
||||
@@ -98,10 +63,6 @@ export function useShellRuntime({
|
||||
selectedProject,
|
||||
minimal,
|
||||
isRestarting,
|
||||
initialCommandRef,
|
||||
isPlainShellRef,
|
||||
authUrlRef,
|
||||
copyAuthUrlToClipboard,
|
||||
closeSocket,
|
||||
});
|
||||
|
||||
@@ -118,7 +79,6 @@ export function useShellRuntime({
|
||||
autoConnect,
|
||||
closeSocket,
|
||||
clearTerminalScreen,
|
||||
setAuthUrl: setCurrentAuthUrl,
|
||||
onOutputRef,
|
||||
});
|
||||
|
||||
@@ -156,11 +116,7 @@ export function useShellRuntime({
|
||||
isConnected,
|
||||
isInitialized,
|
||||
isConnecting,
|
||||
authUrl,
|
||||
authUrlVersion,
|
||||
connectToShell,
|
||||
disconnectFromShell,
|
||||
openAuthUrlInBrowser,
|
||||
copyAuthUrlToClipboard,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,15 +4,18 @@ import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import { WebglAddon } from '@xterm/addon-webgl';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
|
||||
import type { Project } from '../../../types/app';
|
||||
import { copyTextToClipboard } from '../../../utils/clipboard';
|
||||
import {
|
||||
CODEX_DEVICE_AUTH_URL,
|
||||
TERMINAL_INIT_DELAY_MS,
|
||||
TERMINAL_OPTIONS,
|
||||
TERMINAL_RESIZE_DELAY_MS,
|
||||
} from '../constants/constants';
|
||||
import { copyTextToClipboard } from '../../../utils/clipboard';
|
||||
import { isCodexLoginCommand } from '../utils/auth';
|
||||
import {
|
||||
installMobileTerminalSelection,
|
||||
type MobileTerminalSelectionManager,
|
||||
} from '../utils/mobileTerminalSelection';
|
||||
import { sendSocketMessage } from '../utils/socket';
|
||||
import { ensureXtermFocusStyles } from '../utils/terminalStyles';
|
||||
|
||||
@@ -24,10 +27,6 @@ type UseShellTerminalOptions = {
|
||||
selectedProject: Project | null | undefined;
|
||||
minimal: boolean;
|
||||
isRestarting: boolean;
|
||||
initialCommandRef: MutableRefObject<string | null | undefined>;
|
||||
isPlainShellRef: MutableRefObject<boolean>;
|
||||
authUrlRef: MutableRefObject<string>;
|
||||
copyAuthUrlToClipboard: (url?: string) => Promise<boolean>;
|
||||
closeSocket: () => void;
|
||||
};
|
||||
|
||||
@@ -45,14 +44,11 @@ export function useShellTerminal({
|
||||
selectedProject,
|
||||
minimal,
|
||||
isRestarting,
|
||||
initialCommandRef,
|
||||
isPlainShellRef,
|
||||
authUrlRef,
|
||||
copyAuthUrlToClipboard,
|
||||
closeSocket,
|
||||
}: UseShellTerminalOptions): UseShellTerminalResult {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const resizeTimeoutRef = useRef<number | null>(null);
|
||||
const mobileSelectionRef = useRef<MobileTerminalSelectionManager | null>(null);
|
||||
const selectedProjectKey = selectedProject?.fullPath || selectedProject?.path || '';
|
||||
const hasSelectedProject = Boolean(selectedProject);
|
||||
|
||||
@@ -70,6 +66,11 @@ export function useShellTerminal({
|
||||
}, [terminalRef]);
|
||||
|
||||
const disposeTerminal = useCallback(() => {
|
||||
if (mobileSelectionRef.current) {
|
||||
mobileSelectionRef.current.dispose();
|
||||
mobileSelectionRef.current = null;
|
||||
}
|
||||
|
||||
if (terminalRef.current) {
|
||||
terminalRef.current.dispose();
|
||||
terminalRef.current = null;
|
||||
@@ -80,7 +81,8 @@ export function useShellTerminal({
|
||||
}, [fitAddonRef, terminalRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminalContainerRef.current || !hasSelectedProject || isRestarting || terminalRef.current) {
|
||||
const terminalContainer = terminalContainerRef.current;
|
||||
if (!terminalContainer || !hasSelectedProject || isRestarting || terminalRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +104,28 @@ export function useShellTerminal({
|
||||
console.warn('[Shell] WebGL renderer unavailable, using Canvas fallback');
|
||||
}
|
||||
|
||||
nextTerminal.open(terminalContainerRef.current);
|
||||
nextTerminal.open(terminalContainer);
|
||||
mobileSelectionRef.current = installMobileTerminalSelection(
|
||||
nextTerminal,
|
||||
terminalContainer,
|
||||
{
|
||||
onFontSizeChange: (fontSize) => {
|
||||
nextTerminal.options.fontSize = fontSize;
|
||||
|
||||
const currentFitAddon = fitAddonRef.current;
|
||||
if (currentFitAddon) {
|
||||
currentFitAddon.fit();
|
||||
sendSocketMessage(wsRef.current, {
|
||||
type: 'resize',
|
||||
cols: nextTerminal.cols,
|
||||
rows: nextTerminal.rows,
|
||||
});
|
||||
} else {
|
||||
nextTerminal.refresh(0, nextTerminal.rows - 1);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const copyTerminalSelection = async () => {
|
||||
const selection = nextTerminal.getSelection();
|
||||
@@ -133,29 +156,9 @@ export function useShellTerminal({
|
||||
void copyTextToClipboard(selection);
|
||||
};
|
||||
|
||||
terminalContainerRef.current.addEventListener('copy', handleTerminalCopy);
|
||||
terminalContainer.addEventListener('copy', handleTerminalCopy);
|
||||
|
||||
nextTerminal.attachCustomKeyEventHandler((event) => {
|
||||
const activeAuthUrl = isCodexLoginCommand(initialCommandRef.current)
|
||||
? CODEX_DEVICE_AUTH_URL
|
||||
: authUrlRef.current;
|
||||
|
||||
if (
|
||||
event.type === 'keydown' &&
|
||||
minimal &&
|
||||
isPlainShellRef.current &&
|
||||
activeAuthUrl &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!event.altKey &&
|
||||
event.key?.toLowerCase() === 'c'
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void copyAuthUrlToClipboard(activeAuthUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'keydown' &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
@@ -240,10 +243,10 @@ export function useShellTerminal({
|
||||
}, TERMINAL_RESIZE_DELAY_MS);
|
||||
});
|
||||
|
||||
resizeObserver.observe(terminalContainerRef.current);
|
||||
resizeObserver.observe(terminalContainer);
|
||||
|
||||
return () => {
|
||||
terminalContainerRef.current?.removeEventListener('copy', handleTerminalCopy);
|
||||
terminalContainer.removeEventListener('copy', handleTerminalCopy);
|
||||
resizeObserver.disconnect();
|
||||
if (resizeTimeoutRef.current !== null) {
|
||||
window.clearTimeout(resizeTimeoutRef.current);
|
||||
@@ -254,16 +257,12 @@ export function useShellTerminal({
|
||||
disposeTerminal();
|
||||
};
|
||||
}, [
|
||||
authUrlRef,
|
||||
closeSocket,
|
||||
copyAuthUrlToClipboard,
|
||||
disposeTerminal,
|
||||
fitAddonRef,
|
||||
initialCommandRef,
|
||||
isPlainShellRef,
|
||||
isRestarting,
|
||||
minimal,
|
||||
hasSelectedProject,
|
||||
minimal,
|
||||
selectedProjectKey,
|
||||
terminalContainerRef,
|
||||
terminalRef,
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { Terminal } from '@xterm/xterm';
|
||||
|
||||
import type { Project, ProjectSession } from '../../../types/app';
|
||||
|
||||
export type AuthCopyStatus = 'idle' | 'copied' | 'failed';
|
||||
|
||||
export type ShellInitMessage = {
|
||||
type: 'init';
|
||||
projectPath: string;
|
||||
@@ -54,7 +52,6 @@ export type ShellSharedRefs = {
|
||||
wsRef: MutableRefObject<WebSocket | null>;
|
||||
terminalRef: MutableRefObject<Terminal | null>;
|
||||
fitAddonRef: MutableRefObject<FitAddon | null>;
|
||||
authUrlRef: MutableRefObject<string>;
|
||||
selectedProjectRef: MutableRefObject<Project | null | undefined>;
|
||||
selectedSessionRef: MutableRefObject<ProjectSession | null | undefined>;
|
||||
initialCommandRef: MutableRefObject<string | null | undefined>;
|
||||
@@ -69,10 +66,6 @@ export type UseShellRuntimeResult = {
|
||||
isConnected: boolean;
|
||||
isInitialized: boolean;
|
||||
isConnecting: boolean;
|
||||
authUrl: string;
|
||||
authUrlVersion: number;
|
||||
connectToShell: (options?: { forceRestart?: boolean }) => void;
|
||||
disconnectFromShell: (options?: { suppressAutoConnect?: boolean }) => void;
|
||||
openAuthUrlInBrowser: (url?: string) => boolean;
|
||||
copyAuthUrlToClipboard: (url?: string) => Promise<boolean>;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
import type { ProjectSession } from '../../../types/app';
|
||||
import { CODEX_DEVICE_AUTH_URL } from '../constants/constants';
|
||||
|
||||
export function isCodexLoginCommand(command: string | null | undefined): boolean {
|
||||
return typeof command === 'string' && /\bcodex\s+login\b/i.test(command);
|
||||
}
|
||||
|
||||
export function resolveAuthUrlForDisplay(command: string | null | undefined, authUrl: string): string {
|
||||
if (isCodexLoginCommand(command)) {
|
||||
return CODEX_DEVICE_AUTH_URL;
|
||||
}
|
||||
|
||||
return authUrl;
|
||||
}
|
||||
|
||||
export function getSessionDisplayName(session: ProjectSession | null | undefined): string | null {
|
||||
if (!session) {
|
||||
|
||||
1044
src/components/shell/utils/mobileTerminalSelection.ts
Normal file
1044
src/components/shell/utils/mobileTerminalSelection.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -59,12 +59,8 @@ export default function Shell({
|
||||
isConnected,
|
||||
isInitialized,
|
||||
isConnecting,
|
||||
authUrl,
|
||||
authUrlVersion,
|
||||
connectToShell,
|
||||
disconnectFromShell,
|
||||
openAuthUrlInBrowser,
|
||||
copyAuthUrlToClipboard,
|
||||
} = useShellRuntime({
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
@@ -243,15 +239,7 @@ export default function Shell({
|
||||
if (minimal) {
|
||||
return (
|
||||
<>
|
||||
<ShellMinimalView
|
||||
terminalContainerRef={terminalContainerRef}
|
||||
authUrl={authUrl}
|
||||
authUrlVersion={authUrlVersion}
|
||||
initialCommand={initialCommand}
|
||||
isConnected={isConnected}
|
||||
openAuthUrlInBrowser={openAuthUrlInBrowser}
|
||||
copyAuthUrlToClipboard={copyAuthUrlToClipboard}
|
||||
/>
|
||||
<ShellMinimalView terminalContainerRef={terminalContainerRef} />
|
||||
<TerminalShortcutsPanel
|
||||
wsRef={wsRef}
|
||||
terminalRef={terminalRef}
|
||||
|
||||
@@ -1,45 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
import type { AuthCopyStatus } from '../../types/types';
|
||||
import { resolveAuthUrlForDisplay } from '../../utils/auth';
|
||||
|
||||
type ShellMinimalViewProps = {
|
||||
terminalContainerRef: RefObject<HTMLDivElement>;
|
||||
authUrl: string;
|
||||
authUrlVersion: number;
|
||||
initialCommand: string | null | undefined;
|
||||
isConnected: boolean;
|
||||
openAuthUrlInBrowser: (url: string) => boolean;
|
||||
copyAuthUrlToClipboard: (url: string) => Promise<boolean>;
|
||||
};
|
||||
|
||||
export default function ShellMinimalView({
|
||||
terminalContainerRef,
|
||||
authUrl,
|
||||
authUrlVersion,
|
||||
initialCommand,
|
||||
isConnected,
|
||||
openAuthUrlInBrowser,
|
||||
copyAuthUrlToClipboard,
|
||||
}: ShellMinimalViewProps) {
|
||||
const [authUrlCopyStatus, setAuthUrlCopyStatus] = useState<AuthCopyStatus>('idle');
|
||||
const [isAuthPanelHidden, setIsAuthPanelHidden] = useState(false);
|
||||
|
||||
const displayAuthUrl = useMemo(
|
||||
() => resolveAuthUrlForDisplay(initialCommand, authUrl),
|
||||
[authUrl, initialCommand],
|
||||
);
|
||||
|
||||
// Keep auth panel UI state local to minimal mode and reset it when connection/url changes.
|
||||
useEffect(() => {
|
||||
setAuthUrlCopyStatus('idle');
|
||||
setIsAuthPanelHidden(false);
|
||||
}, [authUrlVersion, displayAuthUrl, isConnected]);
|
||||
|
||||
const hasAuthUrl = Boolean(displayAuthUrl);
|
||||
const showMobileAuthPanel = hasAuthUrl && !isAuthPanelHidden;
|
||||
const showMobileAuthPanelToggle = hasAuthUrl && isAuthPanelHidden;
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full bg-gray-900">
|
||||
<div
|
||||
@@ -47,67 +14,6 @@ export default function ShellMinimalView({
|
||||
className="h-full w-full focus:outline-none"
|
||||
style={{ outline: 'none' }}
|
||||
/>
|
||||
|
||||
{showMobileAuthPanel && (
|
||||
<div className="absolute inset-x-0 bottom-14 z-20 border-t border-gray-700/80 bg-gray-900/95 p-3 backdrop-blur-sm md:hidden">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs text-gray-300">Open or copy the login URL:</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsAuthPanelHidden(true)}
|
||||
className="rounded bg-gray-700 px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-gray-100 hover:bg-gray-600"
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={displayAuthUrl}
|
||||
readOnly
|
||||
onClick={(event) => event.currentTarget.select()}
|
||||
className="w-full rounded border border-gray-600 bg-gray-800 px-2 py-1 text-xs text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
aria-label="Authentication URL"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
openAuthUrlInBrowser(displayAuthUrl);
|
||||
}}
|
||||
className="flex-1 rounded bg-blue-600 px-3 py-2 text-xs font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
Open URL
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
const copied = await copyAuthUrlToClipboard(displayAuthUrl);
|
||||
setAuthUrlCopyStatus(copied ? 'copied' : 'failed');
|
||||
}}
|
||||
className="flex-1 rounded bg-gray-700 px-3 py-2 text-xs font-medium text-white hover:bg-gray-600"
|
||||
>
|
||||
{authUrlCopyStatus === 'copied' ? 'Copied' : 'Copy URL'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showMobileAuthPanelToggle && (
|
||||
<div className="absolute bottom-14 right-3 z-20 md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsAuthPanelHidden(false)}
|
||||
className="rounded bg-gray-800/95 px-3 py-2 text-xs font-medium text-gray-100 shadow-lg backdrop-blur-sm hover:bg-gray-700"
|
||||
>
|
||||
Show login URL
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const MOBILE_KEYS: Shortcut[] = [
|
||||
{ type: 'arrow', id: 'arrow-down', sequence: '\x1b[B', icon: 'down' },
|
||||
{ type: 'arrow', id: 'arrow-left', sequence: '\x1b[D', icon: 'left' },
|
||||
{ type: 'arrow', id: 'arrow-right', sequence: '\x1b[C', icon: 'right' },
|
||||
{ type: 'key', id: 'ctrl-c', label: 'Ctrl+C', sequence: '\x03' },
|
||||
];
|
||||
|
||||
const ARROW_ICONS = {
|
||||
|
||||
@@ -137,6 +137,12 @@
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* The app shell is a fixed inset-0 container (see AppContent), so the
|
||||
document itself never needs to scroll. Clipping it removes the phantom
|
||||
full-height page scrollbar and disables the browser pull-to-refresh
|
||||
gesture that reloads the page when scrolling up on mobile. */
|
||||
overflow: hidden;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
/* Root element with safe area padding for PWA */
|
||||
|
||||
Reference in New Issue
Block a user