feat: full responsive layout — mobile sidebar, stacked panels, fullscreen drawers

- AppShell: permanent sidebar on desktop, temporary drawer + hamburger on mobile (<md)
- TopBar: hamburger icon on mobile, AI-Assistent als Icon-Button auf kleinen Screens
- MatchDetail: Sidebar stacks below content on mobile, key facts wrap, px responsive
- Properties: Drawer fullscreen (100vw) on mobile, 650px on desktop
- ActiveInquiriesTab: mobile shows list → tap opens detail with back button
- LatentInquiriesTab: mobile sequential navigation (list → detail → own properties), tablet 2-col, desktop 3-col
- PublicNeedList: fullWidth prop for mobile layout
- InquiryDetailPanel: property card stacks below chat on mobile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-18 21:24:00 +02:00
parent 259b4e3c68
commit a75818b983
7 changed files with 220 additions and 105 deletions
+68 -19
View File
@@ -1,16 +1,19 @@
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router'
import { useLayoutStore } from '../../stores/layoutStore'
import { useSessionStore } from '../../stores/sessionStore'
import { WorkspaceType } from '../../domain/enums'
import {
Box,
Drawer,
Typography,
Avatar,
IconButton,
Tooltip,
Chip,
Button,
useTheme,
useMediaQuery,
} from '@mui/material'
import {
LayoutDashboard,
@@ -33,6 +36,7 @@ import {
ServerCog,
GitBranch,
MessageSquare,
Menu,
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { OrganizationContextBadge } from './OrganizationContextBadge'
@@ -169,6 +173,7 @@ interface SidebarProps {
allowedWorkspaces: WorkspaceType[]
onWorkspaceClick: (workspace: WorkspaceType) => void
onToggle: () => void
onClose?: () => void
userName: string
orgName: string
}
@@ -179,6 +184,7 @@ function Sidebar({
allowedWorkspaces,
onWorkspaceClick,
onToggle,
onClose,
userName,
orgName,
}: SidebarProps) {
@@ -260,7 +266,7 @@ function Sidebar({
const tabContent = (
<Box
onClick={() => onWorkspaceClick(ws)}
onClick={() => { onWorkspaceClick(ws); onClose?.() }}
sx={{
display: 'flex',
alignItems: 'center',
@@ -313,6 +319,7 @@ function Sidebar({
<NavLink
to={item.path}
style={{ textDecoration: 'none', display: 'block' }}
onClick={() => onClose?.()}
>
{({ isActive }) => (
<Box
@@ -434,9 +441,11 @@ function Sidebar({
interface TopBarProps {
activeWorkspace: WorkspaceType
pathname: string
onMenuClick?: () => void
isMobile?: boolean
}
function TopBar({ activeWorkspace, pathname }: TopBarProps) {
function TopBar({ activeWorkspace, pathname, onMenuClick, isMobile }: TopBarProps) {
const config = WORKSPACE_CONFIG[activeWorkspace]
const pageName = getPageNameFromPath(pathname)
const openAssistant = useAssistantStore(s => s.open)
@@ -452,11 +461,16 @@ function TopBar({ activeWorkspace, pathname }: TopBarProps) {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 3,
px: { xs: 1.5, sm: 3 },
}}
>
{/* Left side */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{isMobile && (
<IconButton size="small" onClick={onMenuClick} sx={{ mr: 0.5 }}>
<Menu size={20} />
</IconButton>
)}
<Chip
label={config.label}
size="small"
@@ -470,24 +484,29 @@ function TopBar({ activeWorkspace, pathname }: TopBarProps) {
/>
<Typography
variant="body1"
sx={{ fontWeight: 500, color: '#1e293b', fontSize: '0.9375rem' }}
sx={{ fontWeight: 500, color: '#1e293b', fontSize: { xs: '0.85rem', sm: '0.9375rem' } }}
>
{pageName}
</Typography>
</Box>
{/* Right side */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<OrganizationContextBadge />
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.5, sm: 1 } }}>
<Box sx={{ display: { xs: 'none', sm: 'flex' } }}>
<OrganizationContextBadge />
</Box>
<Button
variant="outlined"
size="small"
startIcon={<Sparkles size={14} />}
onClick={openAssistant}
sx={{ textTransform: 'none', fontSize: '0.8125rem' }}
sx={{ textTransform: 'none', fontSize: '0.8125rem', display: { xs: 'none', md: 'flex' } }}
>
AI Assistent
</Button>
<IconButton size="small" onClick={openAssistant} sx={{ display: { xs: 'flex', md: 'none' } }}>
<Sparkles size={18} />
</IconButton>
<NotificationButton />
<UserMenu />
</Box>
@@ -505,6 +524,12 @@ export function AppShell() {
const { currentUser } = useSessionStore()
const navigate = useNavigate()
const location = useLocation()
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const [mobileOpen, setMobileOpen] = useState(false)
// Close mobile menu on route change
useEffect(() => { setMobileOpen(false) }, [location.pathname])
// Sync active workspace with URL
useEffect(() => {
@@ -523,20 +548,44 @@ export function AppShell() {
const orgName = currentUser?.organizationName ?? ''
const allowedWorkspaces = currentUser?.allowedWorkspaces ?? WORKSPACE_ORDER
const sidebarContent = (
<Sidebar
collapsed={isMobile ? false : sidebarCollapsed}
activeWorkspace={activeWorkspace}
allowedWorkspaces={allowedWorkspaces}
onWorkspaceClick={handleWorkspaceClick}
onToggle={isMobile ? () => setMobileOpen(false) : toggleSidebar}
onClose={isMobile ? () => setMobileOpen(false) : undefined}
userName={userName}
orgName={orgName}
/>
)
return (
<Box sx={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
<Sidebar
collapsed={sidebarCollapsed}
activeWorkspace={activeWorkspace}
allowedWorkspaces={allowedWorkspaces}
onWorkspaceClick={handleWorkspaceClick}
onToggle={toggleSidebar}
userName={userName}
orgName={orgName}
/>
{/* Desktop: permanent sidebar */}
{!isMobile && sidebarContent}
{/* Mobile: temporary drawer */}
{isMobile && (
<Drawer
open={mobileOpen}
onClose={() => setMobileOpen(false)}
variant="temporary"
ModalProps={{ keepMounted: true }}
slotProps={{ paper: { sx: { width: 240, bgcolor: 'transparent', boxShadow: 'none' } } }}
>
{sidebarContent}
</Drawer>
)}
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<TopBar activeWorkspace={activeWorkspace} pathname={location.pathname} />
<TopBar
activeWorkspace={activeWorkspace}
pathname={location.pathname}
isMobile={isMobile}
onMenuClick={() => setMobileOpen(true)}
/>
<Box component="main" sx={{ flex: 1, overflowY: 'auto' }}>
<Outlet />