diff --git a/src/web/App.mobile-layout.test.tsx b/src/web/App.mobile-layout.test.tsx new file mode 100644 index 0000000..a80cada --- /dev/null +++ b/src/web/App.mobile-layout.test.tsx @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +describe('App mobile layout', () => { + it('sets data-layout attribute when switching layouts', () => { + const source = readFileSync(resolve(process.cwd(), 'src/web/App.tsx'), 'utf8'); + + expect(source).toContain('useIsMobile(768)'); + expect(source).toContain("document.documentElement.setAttribute('data-layout'"); + }); +}); diff --git a/src/web/App.tsx b/src/web/App.tsx index d7b723f..ebdbec2 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -16,6 +16,7 @@ import { I18nProvider, useI18n } from './i18n.js'; import { resolveLoginErrorMessage } from './loginError.js'; import { SITE_DOCS_URL } from './docsLink.js'; import { useAnimatedVisibility } from './components/useAnimatedVisibility.js'; +import { useIsMobile } from './components/useIsMobile.js'; const Dashboard = lazy(() => import('./pages/Dashboard.js')); const Sites = lazy(() => import('./pages/Sites.js')); const Accounts = lazy(() => import('./pages/Accounts.js')); @@ -383,6 +384,7 @@ function AppShell() { const notifBtnRef = useRef(null); const latestTaskEventIdRef = useRef(0); const toast = useToast(); + const isMobile = useIsMobile(768); const resolvedTheme: 'light' | 'dark' = themeMode === 'system' ? (systemPrefersDark ? 'dark' : 'light') : themeMode; @@ -415,6 +417,10 @@ function AppShell() { } }, [resolvedTheme, themeMode]); + useEffect(() => { + document.documentElement.setAttribute('data-layout', isMobile ? 'mobile' : 'desktop'); + }, [isMobile]); + useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { diff --git a/src/web/components/useIsMobile.ts b/src/web/components/useIsMobile.ts new file mode 100644 index 0000000..38161c3 --- /dev/null +++ b/src/web/components/useIsMobile.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from 'react'; + +export function useIsMobile(breakpoint = 768) { + const [isMobile, setIsMobile] = useState(() => ( + typeof window !== 'undefined' ? window.innerWidth < breakpoint : false + )); + + useEffect(() => { + const update = () => setIsMobile(window.innerWidth < breakpoint); + update(); + window.addEventListener('resize', update); + return () => window.removeEventListener('resize', update); + }, [breakpoint]); + + return isMobile; +}