feat: implement mobile-responsive tab navigation with select dropdown
- Replace tabs with select dropdown on mobile devices for better UX - Add responsive Layout.Header for mobile with proper styling - Implement hybrid CSS approach: static styles in CSS, dynamic in inline - Add mobile-specific select styling with transparent background - Remove unused CaretDownOutlined import, use EllipsisOutlined for dropdown - Optimize mobile header layout with compact zoom controls - Separate mobile and desktop rendering logic for better maintainability - Add CSS class for mobile project header with proper positioning
This commit is contained in:
@@ -13,12 +13,12 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-tabs > .ant-tabs-nav button.ant-btn-variant-outlined:disabled {
|
button.ant-btn-variant-outlined:disabled {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-tabs > .ant-tabs-nav button.ant-tabs-nav-more {
|
.ant-tabs-nav button.ant-tabs-nav-more {
|
||||||
color: #fff;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-layout-header.overview-header {
|
.ant-layout-header.overview-header {
|
||||||
@@ -42,3 +42,34 @@
|
|||||||
padding: 0px 16px; /* Reduced padding on mobile */
|
padding: 0px 16px; /* Reduced padding on mobile */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile project header */
|
||||||
|
.mobile-project-header {
|
||||||
|
padding: 0px 16px !important;
|
||||||
|
position: sticky !important;
|
||||||
|
top: 0 !important;
|
||||||
|
z-index: 100 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
gap: 12px !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 64px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile select styling */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ant-select .ant-select-selector {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select .ant-select-selection-item {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select .ant-select-arrow {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Tabs, Button, Spin, Skeleton, Result, Grid } from 'antd';
|
import { Tabs, Button, Spin, Skeleton, Result, Grid, Select, Layout } from 'antd';
|
||||||
import { ArrowLeftOutlined, LoadingOutlined, LockOutlined, MinusOutlined, PlusOutlined, EllipsisOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined, LoadingOutlined, LockOutlined, MinusOutlined, PlusOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||||
import UserMenu from '../components/UserMenu';
|
import UserMenu from '../components/UserMenu';
|
||||||
import FilePreview, { formatFileSize, formatDuration } from '../components/FilePreview';
|
import FilePreview, { formatFileSize, formatDuration } from '../components/FilePreview';
|
||||||
@@ -243,45 +243,93 @@ function ProjectDetail({ user, darkMode, onLogout, onToggleDarkMode, overridePar
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Finde den aktuellen Tab-Content
|
||||||
|
const activeTabContent = tabs.find(tab => tab.key === activeKey)?.children;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Spin spinning={loading} indicator={<LoadingOutlined spin />} size="large" tip="Lade Projektdaten..." fullscreen />
|
<Spin spinning={loading} indicator={<LoadingOutlined spin />} size="large" tip="Lade Projektdaten..." fullscreen />
|
||||||
{!loading && (
|
{!loading && (
|
||||||
tabs.length > 0 ? (
|
tabs.length > 0 ? (
|
||||||
<Tabs
|
isMobile ? (
|
||||||
items={tabs}
|
// Mobile Darstellung mit Select und separatem Content
|
||||||
more={{ icon: <EllipsisOutlined />, trigger: 'click' }}
|
<Layout>
|
||||||
activeKey={activeKey}
|
<Layout.Header className="mobile-project-header" style={{
|
||||||
onChange={handleTabChange}
|
background: darkMode ? '#001f1e' : '#001f1e', // Dynamisch, falls später unterschiedliche Themes gewünscht
|
||||||
tabBarExtraContent={{
|
color: darkMode ? '#fff' : '#fff'
|
||||||
left: (
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
icon={<ArrowLeftOutlined style={{ color: '#001f1e' }} />}
|
icon={<ArrowLeftOutlined style={{ color: '#001f1e' }} />}
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
style={{ marginRight: projectLogo ? 12 : 0 }}
|
size="small"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={activeKey}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 120,
|
||||||
|
height: 26
|
||||||
|
}}
|
||||||
|
size="middle"
|
||||||
|
suffixIcon={<EllipsisOutlined style={{ color: '#fff' }} />}
|
||||||
|
options={tabs.map(tab => ({
|
||||||
|
value: tab.key,
|
||||||
|
label: tab.label
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
{projectLogo && !isMobile && (
|
|
||||||
<img
|
|
||||||
src={projectLogo}
|
|
||||||
alt="Logo"
|
|
||||||
style={{ height: 38, marginRight: 32, objectFit: 'contain' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
),
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
right: (
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<Button size="small" shape="circle" icon={<MinusOutlined style={{ color: '#001f1e' }} />} onClick={() => handleZoom('out')} disabled={zoom <= ZOOM_LEVELS[0]}></Button>
|
<Button size="small" shape="circle" icon={<MinusOutlined style={{ color: '#001f1e' }} />} onClick={() => handleZoom('out')} disabled={zoom <= ZOOM_LEVELS[0]}></Button>
|
||||||
<span style={{ minWidth: 48, textAlign: 'center' }}>{Math.round(zoom * 100)}%</span>
|
<span style={{ minWidth: 36, textAlign: 'center', fontSize: 12 }}>{Math.round(zoom * 100)}%</span>
|
||||||
<Button size="small" shape="circle" icon={<PlusOutlined style={{ color: '#001f1e' }} />} onClick={() => handleZoom('in')} disabled={zoom >= ZOOM_LEVELS[ZOOM_LEVELS.length - 1]}></Button>
|
<Button size="small" shape="circle" icon={<PlusOutlined style={{ color: '#001f1e' }} />} onClick={() => handleZoom('in')} disabled={zoom >= ZOOM_LEVELS[ZOOM_LEVELS.length - 1]}></Button>
|
||||||
</div>
|
</div>
|
||||||
<UserMenu user={user} onLogout={onLogout} darkMode={darkMode} onToggleDarkMode={onToggleDarkMode} />
|
<UserMenu user={user} onLogout={onLogout} darkMode={darkMode} onToggleDarkMode={onToggleDarkMode} />
|
||||||
</div>
|
</div>
|
||||||
)
|
</Layout.Header>
|
||||||
}}
|
<Layout.Content style={{ padding: '0px 16px' }}>
|
||||||
/>
|
{activeTabContent}
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout>
|
||||||
|
) : (
|
||||||
|
// Desktop Darstellung mit Tabs
|
||||||
|
<Tabs
|
||||||
|
items={tabs}
|
||||||
|
more={{ icon: <EllipsisOutlined />, trigger: 'click' }}
|
||||||
|
activeKey={activeKey}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
tabBarExtraContent={{
|
||||||
|
left: (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Button
|
||||||
|
icon={<ArrowLeftOutlined style={{ color: '#001f1e' }} />}
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
style={{ marginRight: projectLogo ? 12 : 0 }}
|
||||||
|
/>
|
||||||
|
{projectLogo && (
|
||||||
|
<img
|
||||||
|
src={projectLogo}
|
||||||
|
alt="Logo"
|
||||||
|
style={{ height: 38, marginRight: 32, objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
right: (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<Button size="small" shape="circle" icon={<MinusOutlined style={{ color: '#001f1e' }} />} onClick={() => handleZoom('out')} disabled={zoom <= ZOOM_LEVELS[0]}></Button>
|
||||||
|
<span style={{ minWidth: 48, textAlign: 'center' }}>{Math.round(zoom * 100)}%</span>
|
||||||
|
<Button size="small" shape="circle" icon={<PlusOutlined style={{ color: '#001f1e' }} />} onClick={() => handleZoom('in')} disabled={zoom >= ZOOM_LEVELS[ZOOM_LEVELS.length - 1]}></Button>
|
||||||
|
</div>
|
||||||
|
<UserMenu user={user} onLogout={onLogout} darkMode={darkMode} onToggleDarkMode={onToggleDarkMode} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : error ? (
|
) : error ? (
|
||||||
// Spezifische Fehlerbehandlung
|
// Spezifische Fehlerbehandlung
|
||||||
<Result
|
<Result
|
||||||
|
|||||||
Reference in New Issue
Block a user