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:
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user