cc-haha/desktop/src/hooks/useMobileViewport.ts
程序员阿江(Relakkes) cc9bcb2017 Make H5 chat usable on phone-sized browsers
The browser H5 surface now switches to a phone-oriented shell: the sidebar becomes a closed-by-default drawer, chat stays primary, workspace and terminal panels stay off the mobile chat surface, composer controls use larger touch targets, and mobile menus avoid desktop-only widths and keyboard hints. Desktop and Tauri sidebar behavior remain on the existing store-driven path.

Constraint: H5 is personal/team browser access layered on the existing desktop web UI, so the normal desktop app must keep its current layout behavior.

Rejected: Share the global sidebarOpen default for mobile first paint | it can flash the drawer open before effects run on a phone.

Confidence: high

Scope-risk: moderate

Directive: Keep mobile-only layout branching behind useMobileViewport() && !isTauriRuntime() unless a future task explicitly redesigns the native desktop shell.

Tested: cd desktop && bunx vitest run src/hooks/useMobileViewport.test.tsx src/components/layout/AppShell.test.tsx src/components/layout/Sidebar.test.tsx src/pages/ActiveSession.test.tsx src/components/chat/ChatInput.test.tsx src/components/controls/PermissionModeSelector.test.tsx

Tested: cd desktop && bun run lint

Tested: cd desktop && bun run build
2026-05-10 01:11:24 +08:00

39 lines
1.1 KiB
TypeScript

import { useEffect, useState } from 'react'
const MOBILE_VIEWPORT_QUERY = '(max-width: 767px)'
function getInitialMobileViewport() {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false
return window.matchMedia(MOBILE_VIEWPORT_QUERY).matches
}
export function useMobileViewport() {
const [isMobile, setIsMobile] = useState(getInitialMobileViewport)
useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return
const mediaQuery = window.matchMedia(MOBILE_VIEWPORT_QUERY)
const handleChange = (event: MediaQueryListEvent) => {
setIsMobile(event.matches)
}
setIsMobile(mediaQuery.matches)
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', handleChange)
} else {
mediaQuery.addListener(handleChange)
}
return () => {
if (typeof mediaQuery.removeEventListener === 'function') {
mediaQuery.removeEventListener('change', handleChange)
} else {
mediaQuery.removeListener(handleChange)
}
}
}, [])
return isMobile
}