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
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Box, IconButton, Tooltip, Typography } from '@mui/material' import { Box, IconButton, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material'
import { LayoutGrid, List as ListIcon, Inbox } from 'lucide-react' import { ArrowLeft, LayoutGrid, List as ListIcon, Inbox } from 'lucide-react'
import { useActiveInquiries } from '../../hooks/useInquiries' import { useActiveInquiries } from '../../hooks/useInquiries'
import { EmptyState } from '../ui' import { EmptyState } from '../ui'
import { InquiryList } from './InquiryList' import { InquiryList } from './InquiryList'
@@ -21,6 +21,8 @@ export function ActiveInquiriesTab() {
const { data: inquiries = [], isLoading } = useActiveInquiries() const { data: inquiries = [], isLoading } = useActiveInquiries()
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const [view, setView] = useState<ViewMode>(loadViewMode()) const [view, setView] = useState<ViewMode>(loadViewMode())
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -28,12 +30,12 @@ export function ActiveInquiriesTab() {
} }
}, [view]) }, [view])
// Auto-select first inquiry when data loads // Auto-select first inquiry on desktop only
useEffect(() => { useEffect(() => {
if (!selectedId && inquiries.length > 0) { if (!isMobile && !selectedId && inquiries.length > 0) {
setSelectedId(inquiries[0].id) setSelectedId(inquiries[0].id)
} }
}, [inquiries, selectedId]) }, [inquiries, selectedId, isMobile])
if (!isLoading && inquiries.length === 0) { if (!isLoading && inquiries.length === 0) {
return ( return (
@@ -45,15 +47,31 @@ export function ActiveInquiriesTab() {
) )
} }
// Mobile: show detail panel when an inquiry is selected
if (isMobile && selectedId) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<Box sx={{ px: 1.5, py: 1, borderBottom: '1px solid #e2e8f0', bgcolor: 'white', flexShrink: 0 }}>
<IconButton size="small" onClick={() => setSelectedId(null)}>
<ArrowLeft size={18} />
</IconButton>
</Box>
<Box sx={{ flex: 1, overflow: 'hidden' }}>
<InquiryDetailPanel inquiryId={selectedId} />
</Box>
</Box>
)
}
return ( return (
<Box sx={{ display: 'flex', height: '100%', overflow: 'hidden' }}> <Box sx={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
{/* Left column: list/grid */} {/* Left column: list/grid */}
<Box <Box
sx={{ sx={{
width: view === 'list' ? 380 : 720, width: isMobile ? '100%' : (view === 'list' ? 380 : 720),
minWidth: view === 'list' ? 380 : 480, minWidth: isMobile ? 'unset' : (view === 'list' ? 380 : 480),
flexShrink: 0, flexShrink: 0,
borderRight: '1px solid #e2e8f0', borderRight: isMobile ? 'none' : '1px solid #e2e8f0',
bgcolor: 'white', bgcolor: 'white',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -75,44 +93,18 @@ export function ActiveInquiriesTab() {
<Typography variant="body2" sx={{ fontWeight: 600, color: '#0f172a' }}> <Typography variant="body2" sx={{ fontWeight: 600, color: '#0f172a' }}>
Anfragen Anfragen
</Typography> </Typography>
<Box <Box sx={{ px: 1, py: 0.125, borderRadius: 1, bgcolor: '#1e3a5f', color: 'white', fontWeight: 600, fontSize: '0.7rem' }}>
sx={{
px: 1,
py: 0.125,
borderRadius: 1,
bgcolor: '#1e3a5f',
color: 'white',
fontWeight: 600,
fontSize: '0.7rem',
}}
>
{inquiries.length} {inquiries.length}
</Box> </Box>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 0.5 }}> <Box sx={{ display: 'flex', gap: 0.5 }}>
<Tooltip title="Listenansicht"> <Tooltip title="Listenansicht">
<IconButton <IconButton size="small" onClick={() => setView('list')} sx={{ bgcolor: view === 'list' ? '#e0e7ff' : 'transparent', color: view === 'list' ? '#1e3a5f' : '#64748b' }}>
size="small"
onClick={() => setView('list')}
sx={{
bgcolor: view === 'list' ? '#e0e7ff' : 'transparent',
color: view === 'list' ? '#1e3a5f' : '#64748b',
'&:hover': { bgcolor: view === 'list' ? '#c7d2fe' : '#f1f5f9' },
}}
>
<ListIcon size={16} /> <ListIcon size={16} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title="Kartenansicht"> <Tooltip title="Kartenansicht">
<IconButton <IconButton size="small" onClick={() => setView('grid')} sx={{ bgcolor: view === 'grid' ? '#e0e7ff' : 'transparent', color: view === 'grid' ? '#1e3a5f' : '#64748b' }}>
size="small"
onClick={() => setView('grid')}
sx={{
bgcolor: view === 'grid' ? '#e0e7ff' : 'transparent',
color: view === 'grid' ? '#1e3a5f' : '#64748b',
'&:hover': { bgcolor: view === 'grid' ? '#c7d2fe' : '#f1f5f9' },
}}
>
<LayoutGrid size={16} /> <LayoutGrid size={16} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -120,32 +112,26 @@ export function ActiveInquiriesTab() {
</Box> </Box>
{view === 'list' ? ( {view === 'list' ? (
<InquiryList <InquiryList inquiries={inquiries} selectedId={selectedId} onSelect={setSelectedId} />
inquiries={inquiries}
selectedId={selectedId}
onSelect={setSelectedId}
/>
) : ( ) : (
<InquiryCardGrid <InquiryCardGrid inquiries={inquiries} selectedId={selectedId} onSelect={setSelectedId} />
inquiries={inquiries}
selectedId={selectedId}
onSelect={setSelectedId}
/>
)} )}
</Box> </Box>
{/* Right column: detail */} {/* Right column: detail (desktop only) */}
<Box sx={{ flex: 1, overflow: 'hidden', bgcolor: '#f8fafc' }}> {!isMobile && (
{selectedId ? ( <Box sx={{ flex: 1, overflow: 'hidden', bgcolor: '#f8fafc' }}>
<InquiryDetailPanel inquiryId={selectedId} /> {selectedId ? (
) : ( <InquiryDetailPanel inquiryId={selectedId} />
<EmptyState ) : (
icon={<Inbox size={40} />} <EmptyState
title="Anfrage auswählen" icon={<Inbox size={40} />}
description="Wählen Sie eine Anfrage aus der Liste, um Details anzuzeigen und zu antworten." title="Anfrage auswählen"
/> description="Wählen Sie eine Anfrage aus der Liste, um Details anzuzeigen und zu antworten."
)} />
</Box> )}
</Box>
)}
</Box> </Box>
) )
} }
@@ -109,9 +109,9 @@ export function InquiryDetailPanel({ inquiryId }: InquiryDetailPanelProps) {
</Select> </Select>
</Box> </Box>
<Box sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}> <Box sx={{ flex: 1, display: 'flex', flexDirection: { xs: 'column', md: 'row' }, overflow: 'hidden' }}>
<InquiryChat inquiry={inquiry} /> <InquiryChat inquiry={inquiry} />
<Box sx={{ width: 300, flexShrink: 0 }}> <Box sx={{ width: { xs: '100%', md: 300 }, flexShrink: 0, borderTop: { xs: '1px solid #e2e8f0', md: 'none' }, maxHeight: { xs: 280, md: 'none' }, overflowY: 'auto' }}>
<RelatedPropertyCardPanel propertyId={inquiry.propertyId} /> <RelatedPropertyCardPanel propertyId={inquiry.propertyId} />
</Box> </Box>
</Box> </Box>
@@ -1,41 +1,117 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Box } from '@mui/material' import { Box, IconButton, Typography, useMediaQuery, useTheme } from '@mui/material'
import { Sparkles } from 'lucide-react' import { ArrowLeft, Sparkles } from 'lucide-react'
import { usePublicNeeds, useLatentNeedById } from '../../hooks/useLatentNeeds' import { usePublicNeeds, useLatentNeedById } from '../../hooks/useLatentNeeds'
import { EmptyState } from '../ui' import { EmptyState } from '../ui'
import { PublicNeedList } from './PublicNeedList' import { PublicNeedList } from './PublicNeedList'
import { PublicNeedDetail } from './PublicNeedDetail' import { PublicNeedDetail } from './PublicNeedDetail'
import { OwnPropertyMatchList } from './OwnPropertyMatchList' import { OwnPropertyMatchList } from './OwnPropertyMatchList'
type MobileView = 'list' | 'detail' | 'properties'
export function LatentInquiriesTab() { export function LatentInquiriesTab() {
const { data: needs = [] } = usePublicNeeds() const { data: needs = [] } = usePublicNeeds()
const [selectedNeedId, setSelectedNeedId] = useState<string | null>(null) const [selectedNeedId, setSelectedNeedId] = useState<string | null>(null)
const { data: selectedNeed } = useLatentNeedById(selectedNeedId) const { data: selectedNeed } = useLatentNeedById(selectedNeedId)
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const isTablet = useMediaQuery(theme.breakpoints.between('md', 'lg'))
const [mobileView, setMobileView] = useState<MobileView>('list')
useEffect(() => { useEffect(() => {
if (!selectedNeedId && needs.length > 0) { if (!isMobile && !selectedNeedId && needs.length > 0) {
setSelectedNeedId(needs[0].id) setSelectedNeedId(needs[0].id)
} }
}, [needs, selectedNeedId]) }, [needs, selectedNeedId, isMobile])
const handleSelectNeed = (id: string) => {
setSelectedNeedId(id)
if (isMobile) setMobileView('detail')
}
// Mobile: sequential single-panel navigation
if (isMobile) {
if (mobileView === 'detail' && selectedNeed) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<Box sx={{ px: 1.5, py: 1, borderBottom: '1px solid #e2e8f0', bgcolor: 'white', flexShrink: 0, display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton size="small" onClick={() => setMobileView('list')}>
<ArrowLeft size={18} />
</IconButton>
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.85rem' }} noWrap>
{selectedNeed.title}
</Typography>
</Box>
<Box sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<PublicNeedDetail need={selectedNeed} />
<Box sx={{ px: 2, py: 1.5, borderTop: '1px solid #e2e8f0', flexShrink: 0 }}>
<Box
onClick={() => setMobileView('properties')}
sx={{ p: 1.25, bgcolor: '#1e3a5f', color: 'white', borderRadius: 1, textAlign: 'center', cursor: 'pointer', fontSize: '0.85rem', fontWeight: 600 }}
>
Eigene Objekte anzeigen
</Box>
</Box>
</Box>
</Box>
)
}
if (mobileView === 'properties' && selectedNeed) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<Box sx={{ px: 1.5, py: 1, borderBottom: '1px solid #e2e8f0', bgcolor: 'white', flexShrink: 0, display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton size="small" onClick={() => setMobileView('detail')}>
<ArrowLeft size={18} />
</IconButton>
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.85rem' }}>
Eigene Objekte
</Typography>
</Box>
<Box sx={{ flex: 1, overflow: 'hidden' }}>
<OwnPropertyMatchList need={selectedNeed} />
</Box>
</Box>
)
}
// list view
return (
<Box sx={{ height: '100%', overflow: 'hidden' }}>
<PublicNeedList selectedNeedId={selectedNeedId} onSelect={handleSelectNeed} fullWidth />
</Box>
)
}
// Tablet: hide own-property column, show it as button in detail
if (isTablet) {
return (
<Box sx={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
<PublicNeedList selectedNeedId={selectedNeedId} onSelect={setSelectedNeedId} />
<Box sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
{selectedNeed ? (
<PublicNeedDetail need={selectedNeed} />
) : (
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<EmptyState icon={<Sparkles size={40} />} title="Bedarf auswählen" description="Wählen Sie einen latenten Bedarf, um Details zu sehen." />
</Box>
)}
</Box>
</Box>
)
}
// Desktop: full 3-column layout
return ( return (
<Box sx={{ display: 'flex', height: '100%', overflow: 'hidden' }}> <Box sx={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
<PublicNeedList selectedNeedId={selectedNeedId} onSelect={setSelectedNeedId} /> <PublicNeedList selectedNeedId={selectedNeedId} onSelect={setSelectedNeedId} />
<Box sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}> <Box sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
{selectedNeed ? ( {selectedNeed ? (
<PublicNeedDetail need={selectedNeed} /> <PublicNeedDetail need={selectedNeed} />
) : ( ) : (
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<EmptyState <EmptyState icon={<Sparkles size={40} />} title="Bedarf auswählen" description="Wählen Sie einen latenten Bedarf, um Details und passende Objekte zu sehen." />
icon={<Sparkles size={40} />}
title="Bedarf auswählen"
description="Wählen Sie einen latenten Bedarf, um Details und passende Objekte zu sehen."
/>
</Box> </Box>
)} )}
</Box> </Box>
{selectedNeed && <OwnPropertyMatchList need={selectedNeed} />} {selectedNeed && <OwnPropertyMatchList need={selectedNeed} />}
</Box> </Box>
) )
@@ -7,18 +7,19 @@ import { EmptyState } from '../ui'
interface PublicNeedListProps { interface PublicNeedListProps {
selectedNeedId: string | null selectedNeedId: string | null
onSelect: (id: string) => void onSelect: (id: string) => void
fullWidth?: boolean
} }
export function PublicNeedList({ selectedNeedId, onSelect }: PublicNeedListProps) { export function PublicNeedList({ selectedNeedId, onSelect, fullWidth }: PublicNeedListProps) {
const { data: needs = [], isLoading, error } = usePublicNeeds() const { data: needs = [], isLoading, error } = usePublicNeeds()
return ( return (
<Box <Box
sx={{ sx={{
width: 280, width: fullWidth ? '100%' : 280,
minWidth: 280, minWidth: fullWidth ? 'unset' : 280,
flexShrink: 0, flexShrink: 0,
borderRight: '1px solid #e2e8f0', borderRight: fullWidth ? 'none' : '1px solid #e2e8f0',
bgcolor: 'white', bgcolor: 'white',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
+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 { Outlet, NavLink, useNavigate, useLocation } from 'react-router'
import { useLayoutStore } from '../../stores/layoutStore' import { useLayoutStore } from '../../stores/layoutStore'
import { useSessionStore } from '../../stores/sessionStore' import { useSessionStore } from '../../stores/sessionStore'
import { WorkspaceType } from '../../domain/enums' import { WorkspaceType } from '../../domain/enums'
import { import {
Box, Box,
Drawer,
Typography, Typography,
Avatar, Avatar,
IconButton, IconButton,
Tooltip, Tooltip,
Chip, Chip,
Button, Button,
useTheme,
useMediaQuery,
} from '@mui/material' } from '@mui/material'
import { import {
LayoutDashboard, LayoutDashboard,
@@ -33,6 +36,7 @@ import {
ServerCog, ServerCog,
GitBranch, GitBranch,
MessageSquare, MessageSquare,
Menu,
} from 'lucide-react' } from 'lucide-react'
import type { LucideIcon } from 'lucide-react' import type { LucideIcon } from 'lucide-react'
import { OrganizationContextBadge } from './OrganizationContextBadge' import { OrganizationContextBadge } from './OrganizationContextBadge'
@@ -169,6 +173,7 @@ interface SidebarProps {
allowedWorkspaces: WorkspaceType[] allowedWorkspaces: WorkspaceType[]
onWorkspaceClick: (workspace: WorkspaceType) => void onWorkspaceClick: (workspace: WorkspaceType) => void
onToggle: () => void onToggle: () => void
onClose?: () => void
userName: string userName: string
orgName: string orgName: string
} }
@@ -179,6 +184,7 @@ function Sidebar({
allowedWorkspaces, allowedWorkspaces,
onWorkspaceClick, onWorkspaceClick,
onToggle, onToggle,
onClose,
userName, userName,
orgName, orgName,
}: SidebarProps) { }: SidebarProps) {
@@ -260,7 +266,7 @@ function Sidebar({
const tabContent = ( const tabContent = (
<Box <Box
onClick={() => onWorkspaceClick(ws)} onClick={() => { onWorkspaceClick(ws); onClose?.() }}
sx={{ sx={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -313,6 +319,7 @@ function Sidebar({
<NavLink <NavLink
to={item.path} to={item.path}
style={{ textDecoration: 'none', display: 'block' }} style={{ textDecoration: 'none', display: 'block' }}
onClick={() => onClose?.()}
> >
{({ isActive }) => ( {({ isActive }) => (
<Box <Box
@@ -434,9 +441,11 @@ function Sidebar({
interface TopBarProps { interface TopBarProps {
activeWorkspace: WorkspaceType activeWorkspace: WorkspaceType
pathname: string pathname: string
onMenuClick?: () => void
isMobile?: boolean
} }
function TopBar({ activeWorkspace, pathname }: TopBarProps) { function TopBar({ activeWorkspace, pathname, onMenuClick, isMobile }: TopBarProps) {
const config = WORKSPACE_CONFIG[activeWorkspace] const config = WORKSPACE_CONFIG[activeWorkspace]
const pageName = getPageNameFromPath(pathname) const pageName = getPageNameFromPath(pathname)
const openAssistant = useAssistantStore(s => s.open) const openAssistant = useAssistantStore(s => s.open)
@@ -452,11 +461,16 @@ function TopBar({ activeWorkspace, pathname }: TopBarProps) {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
px: 3, px: { xs: 1.5, sm: 3 },
}} }}
> >
{/* Left side */} {/* 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 <Chip
label={config.label} label={config.label}
size="small" size="small"
@@ -470,24 +484,29 @@ function TopBar({ activeWorkspace, pathname }: TopBarProps) {
/> />
<Typography <Typography
variant="body1" variant="body1"
sx={{ fontWeight: 500, color: '#1e293b', fontSize: '0.9375rem' }} sx={{ fontWeight: 500, color: '#1e293b', fontSize: { xs: '0.85rem', sm: '0.9375rem' } }}
> >
{pageName} {pageName}
</Typography> </Typography>
</Box> </Box>
{/* Right side */} {/* Right side */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0.5, sm: 1 } }}>
<OrganizationContextBadge /> <Box sx={{ display: { xs: 'none', sm: 'flex' } }}>
<OrganizationContextBadge />
</Box>
<Button <Button
variant="outlined" variant="outlined"
size="small" size="small"
startIcon={<Sparkles size={14} />} startIcon={<Sparkles size={14} />}
onClick={openAssistant} onClick={openAssistant}
sx={{ textTransform: 'none', fontSize: '0.8125rem' }} sx={{ textTransform: 'none', fontSize: '0.8125rem', display: { xs: 'none', md: 'flex' } }}
> >
AI Assistent AI Assistent
</Button> </Button>
<IconButton size="small" onClick={openAssistant} sx={{ display: { xs: 'flex', md: 'none' } }}>
<Sparkles size={18} />
</IconButton>
<NotificationButton /> <NotificationButton />
<UserMenu /> <UserMenu />
</Box> </Box>
@@ -505,6 +524,12 @@ export function AppShell() {
const { currentUser } = useSessionStore() const { currentUser } = useSessionStore()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() 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 // Sync active workspace with URL
useEffect(() => { useEffect(() => {
@@ -523,20 +548,44 @@ export function AppShell() {
const orgName = currentUser?.organizationName ?? '' const orgName = currentUser?.organizationName ?? ''
const allowedWorkspaces = currentUser?.allowedWorkspaces ?? WORKSPACE_ORDER 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 ( return (
<Box sx={{ display: 'flex', height: '100vh', overflow: 'hidden' }}> <Box sx={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
<Sidebar {/* Desktop: permanent sidebar */}
collapsed={sidebarCollapsed} {!isMobile && sidebarContent}
activeWorkspace={activeWorkspace}
allowedWorkspaces={allowedWorkspaces} {/* Mobile: temporary drawer */}
onWorkspaceClick={handleWorkspaceClick} {isMobile && (
onToggle={toggleSidebar} <Drawer
userName={userName} open={mobileOpen}
orgName={orgName} 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' }}> <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' }}> <Box component="main" sx={{ flex: 1, overflowY: 'auto' }}>
<Outlet /> <Outlet />
+9 -8
View File
@@ -207,8 +207,8 @@ export default function MatchDetail() {
{/* Property header — white section below hero */} {/* Property header — white section below hero */}
<Box sx={{ bgcolor: 'white', borderBottom: '1px solid #e2e8f0' }}> <Box sx={{ bgcolor: 'white', borderBottom: '1px solid #e2e8f0' }}>
<Box sx={{ px: 3, pt: 2.5, pb: 2 }}> <Box sx={{ px: { xs: 2, sm: 3 }, pt: 2.5, pb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, alignItems: { xs: 'flex-start', sm: 'flex-start' }, justifyContent: 'space-between', gap: 2 }}>
{/* Left: title + address + chips */} {/* Left: title + address + chips */}
<Box sx={{ flex: 1, minWidth: 0 }}> <Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="h5" sx={{ fontWeight: 700, mb: 0.25, lineHeight: 1.3 }}> <Typography variant="h5" sx={{ fontWeight: 700, mb: 0.25, lineHeight: 1.3 }}>
@@ -264,12 +264,13 @@ export default function MatchDetail() {
{/* Key facts strip */} {/* Key facts strip */}
<Divider /> <Divider />
<Box sx={{ display: 'flex', px: 3, py: 1.5, gap: 0 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', px: { xs: 2, sm: 3 }, py: 1.5, gap: 0 }}>
{keyFacts.map((fact, i) => ( {keyFacts.map((fact, i) => (
<Box key={fact.label} sx={{ <Box key={fact.label} sx={{
flex: 1, flex: '1 1 120px',
pl: i === 0 ? 0 : 2, pl: i === 0 ? 0 : { xs: 1.5, sm: 2 },
pr: 2, pr: { xs: 1.5, sm: 2 },
py: { xs: 0.5, sm: 0 },
borderLeft: i === 0 ? 'none' : '1px solid #e2e8f0', borderLeft: i === 0 ? 'none' : '1px solid #e2e8f0',
}}> }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', lineHeight: 1.2 }}> <Typography variant="caption" color="text.secondary" sx={{ display: 'block', lineHeight: 1.2 }}>
@@ -284,7 +285,7 @@ export default function MatchDetail() {
</Box> </Box>
{/* Main content */} {/* Main content */}
<Box sx={{ px: 3, py: 3, display: 'flex', gap: 3, alignItems: 'flex-start' }}> <Box sx={{ px: { xs: 1.5, sm: 3 }, py: { xs: 2, sm: 3 }, display: 'flex', flexDirection: { xs: 'column', lg: 'row' }, gap: 3, alignItems: 'flex-start' }}>
{/* Main column */} {/* Main column */}
<Box sx={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 2 }}>
@@ -340,7 +341,7 @@ export default function MatchDetail() {
</Box> </Box>
{/* Sidebar */} {/* Sidebar */}
<Box sx={{ width: 320, flexShrink: 0, position: 'sticky', top: 64, display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ width: { xs: '100%', lg: 320 }, flexShrink: 0, position: { xs: 'static', lg: 'sticky' }, top: 64, display: 'flex', flexDirection: 'column', gap: 2 }}>
<ScoreBreakdownPanel match={match} taxCalculatorUrl={taxCalculatorUrl} /> <ScoreBreakdownPanel match={match} taxCalculatorUrl={taxCalculatorUrl} />
<NextActionsPanel <NextActionsPanel
match={match} match={match}
+4 -2
View File
@@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { Box, Drawer } from '@mui/material' import { Box, Drawer, useMediaQuery, useTheme } from '@mui/material'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import { PageHeader } from '../../components/layout' import { PageHeader } from '../../components/layout'
import { DecisionContextPanel } from '../../components/ui' import { DecisionContextPanel } from '../../components/ui'
@@ -49,6 +49,8 @@ function applyFilters(properties: Property[], filters: PropertyTableFilters): Pr
export default function Properties() { export default function Properties() {
const navigate = useNavigate() const navigate = useNavigate()
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const [filters, setFilters] = useState<PropertyTableFilters>({}) const [filters, setFilters] = useState<PropertyTableFilters>({})
const [view, setView] = useState<'list' | 'grid'>(() => const [view, setView] = useState<'list' | 'grid'>(() =>
@@ -155,7 +157,7 @@ export default function Properties() {
anchor="right" anchor="right"
open={!!selectedId} open={!!selectedId}
onClose={() => setSelectedId(null)} onClose={() => setSelectedId(null)}
slotProps={{ paper: { sx: { width: 650, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }} slotProps={{ paper: { sx: { width: isMobile ? '100vw' : 650, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }}
> >
{selectedId && ( {selectedId && (
<PropertyDetailView propertyId={selectedId} onClose={() => setSelectedId(null)} /> <PropertyDetailView propertyId={selectedId} onClose={() => setSelectedId(null)} />