fix(mobile): refactor headers and orientation choose UI to avoid layout break
continuous-integration/webhook Deploy concluído (VPS4)

This commit is contained in:
VPS 4 Deploy Agent
2026-05-31 21:27:03 +02:00
parent 4fb6181289
commit 3700f5c493
6 changed files with 127 additions and 137 deletions
@@ -1 +1 @@
2.1.48
2.1.49
+20 -4
View File
@@ -762,10 +762,10 @@ body {
/* ---- Header da página: empilha, busca full-width ---- */
.header { padding: 12px 16px; }
.header-content { flex-direction: column; align-items: stretch; gap: 12px; }
.header-left { width: 100%; }
.header h1 { font-size: 1.25rem; }
.header-actions { width: 100%; flex-wrap: wrap; gap: 8px; }
.header-content { flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
.header-left { flex: 1; min-width: 50%; }
.header h1 { font-size: 1.15rem; }
.header-actions { width: auto; flex: none; flex-wrap: nowrap; gap: 8px; align-items: center; }
.search-input { min-width: 0; flex: 1 1 100%; width: 100%; font-size: 16px; } /* 16px evita zoom iOS */
.client-filter { flex: 1 1 100%; min-width: 0; width: 100%; }
@@ -773,6 +773,22 @@ body {
.hide-mobile { display: none !important; }
.show-mobile { display: inline-flex !important; }
.mobile-icon-actions { display: flex !important; gap: 6px; align-items: center; }
.btn-icon {
width: 40px !important;
height: 40px !important;
border-radius: 50% !important;
display: flex !important;
align-items: center;
justify-content: center;
padding: 0 !important;
font-size: 1.2rem;
background: var(--bg-surface);
border: 1px solid var(--border-color);
color: var(--text-primary);
}
.btn-icon:active { background: var(--hover-color); transform: scale(0.95); }
/* ---- Grid de cards: thumbnails menores ---- */
.images-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
.image-preview { height: 150px; }
@@ -311,34 +311,11 @@ export default function DashboardPage() {
🔄
</button>
</span>
{/* Mobile: ões em dropdown */}
<span className="show-mobile">
<DropdownMenu
label="Ações"
buttonClassName="btn btn-primary"
items={[
{ label: 'Pesquisar', icon: '🔍', onClick: () => setIsSearchMobileOpen(!isSearchMobileOpen) },
{ label: showDisabled ? 'Ver Ativas' : 'Ver Ocultas', icon: '👁️', onClick: () => setShowDisabled(!showDisabled) },
{ label: 'Atualizar', icon: '🔄', onClick: () => loadPatients() },
]}
>
{/* Filtro de clientes dentro do dropdown */}
<div className="dropdown-custom">
<select
className="client-filter"
value={selectedClient}
onChange={(e) => setSelectedClient(e.target.value)}
style={{ height: 38, fontSize: '0.85rem', width: '100%' }}
>
<option value="">📋 Todos os Clientes</option>
{clientsList.map((c) => (
<option key={c.name} value={c.name}>
{c.name === 'Upload Web' ? 'Upload Web 💻' : `${c.name}${c.status === 'identified' ? ' ✅' : c.status === 'historic' ? ' 📚' : ''}`}
</option>
))}
</select>
</div>
</DropdownMenu>
{/* Mobile: botões de ação em linha */}
<span className="show-mobile mobile-icon-actions">
<button className="btn-icon" onClick={() => setIsSearchMobileOpen(!isSearchMobileOpen)} title="Pesquisar">🔍</button>
<button className="btn-icon" onClick={() => setShowDisabled(!showDisabled)} title={showDisabled ? 'Ver Ativas' : 'Ver Ocultas'}>👁</button>
<button className="btn-icon" onClick={() => loadPatients()} title="Atualizar">🔄</button>
</span>
</div>
)}
@@ -348,17 +325,34 @@ export default function DashboardPage() {
</div>
)}
</div>
{view === 'patients' && isSearchMobileOpen && (
<div className="show-mobile" style={{ padding: '0 20px 15px', width: '100%' }}>
<input
type="search"
className="form-control"
placeholder="🔍 Pesquisar paciente..."
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ width: '100%', height: 44, fontSize: '1rem' }}
autoFocus
/>
{/* Linha adicional Mobile para Filtros e Busca */}
{view === 'patients' && isMobile && (
<div className="show-mobile" style={{ padding: '0 20px 10px', width: '100%', display: 'flex', flexDirection: 'column', gap: 10 }}>
<select
className="client-filter form-control"
value={selectedClient}
onChange={(e) => setSelectedClient(e.target.value)}
style={{ width: '100%', height: 44 }}
>
<option value="">📋 Todos os Clientes</option>
{clientsList.map((c) => (
<option key={c.name} value={c.name}>
{c.name === 'Upload Web' ? 'Upload Web 💻' : `${c.name}${c.status === 'identified' ? ' ✅' : c.status === 'historic' ? ' 📚' : ''}`}
</option>
))}
</select>
{isSearchMobileOpen && (
<input
type="search"
className="form-control"
placeholder="🔍 Pesquisar paciente..."
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ width: '100%', height: 44, fontSize: '1rem' }}
autoFocus
/>
)}
</div>
)}
</header>
@@ -351,16 +351,11 @@ export default function GtosPage() {
<button className="btn btn-secondary" style={{ height:42, padding:'10px 14px', marginLeft:8 }} onClick={reload}>🔄</button>
</span>
{/* Mobile: ações em menu flutuante */}
<span className="show-mobile">
<DropdownMenu
label="Ações"
buttonClassName="btn btn-primary"
items={[
{ label:'Pesquisar', icon:'🔍', onClick:() => setIsSearchMobileOpen(!isSearchMobileOpen) },
{ label:'Nova GTO', icon:'', onClick:() => setShowCreate(true) },
{ label:'Atualizar lista', icon:'🔄', onClick:reload },
]}
/>
{/* Mobile: ações com icon buttons */}
<span className="show-mobile mobile-icon-actions">
<button className="btn-icon" onClick={() => setIsSearchMobileOpen(!isSearchMobileOpen)} title="Pesquisar">🔍</button>
<button className="btn-icon" onClick={() => setShowCreate(true)} title="Nova GTO"></button>
<button className="btn-icon" onClick={reload} title="Atualizar">🔄</button>
</span>
</div>
</div>
@@ -281,64 +281,57 @@ export default function ImageDetailsPage() {
}
};
// Gerencia opções (4 no total). Se mobile + landscape(horizontal) divide em 2 páginas.
// Gerencia opções (4 no total). No mobile exibe APENAS UMA GIGANTE por vez.
const allRotations = [0, 90, 180, 270];
const useTwoCards = isMobile && !isPortrait;
const currentRotations = useTwoCards
? allRotations.slice(orientationSubpage * 2, (orientationSubpage * 2) + 2)
: allRotations;
const useOneCard = isMobile;
const currentRotations = useOneCard ? [allRotations[orientationSubpage]] : allRotations;
return (
<div className="layout">
<Sidebar />
<main className="main-content">
{/* Top Header para navegação / Voltar */}
<header className="header" style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<button className="btn btn-secondary" onClick={() => navigate('/patients')} title="Voltar ao Painel">
Voltar
</button>
<div>
<h1 style={{ fontSize: '1.1rem', margin: 0, color: 'var(--text-primary)' }}>
{image.patient_name || 'Desconhecido'}
</h1>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
{formatDate(image.created_at)}
{/* Top Header para navegação / Voltar */}
<header className="header" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%' }}>
<button className="btn btn-secondary" onClick={() => navigate('/patients')} style={{ padding: '8px 12px' }} title="Voltar ao Painel">
{isMobile ? '←' : '← Voltar'}
</button>
<div style={{ flex: 1, minWidth: 0 }}>
<h1 style={{ fontSize: '1.1rem', margin: 0, color: 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{image.patient_name || 'Desconhecido'}
</h1>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
{formatDate(image.created_at)}
</div>
</div>
</div>
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>
{activeView === 'transform' && step === 'finetune' && (
<button
type="button"
className="btn btn-secondary btn-small"
onClick={() => setStep('choose')}
title="Voltar para escolher a orientação"
> Posições</button>
)}
<button
className={`btn btn-small ${activeView === 'transform' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setActiveView('transform')}
title="Ajustar Imagem"
>🔄 Ajustar</button>
<button
className={`btn btn-small ${activeView === 'gto' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setActiveView('gto')}
title="Gerenciar GTOs"
>📋 GTOs</button>
<button
className={`btn btn-small ${showInfo ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setShowInfo(!showInfo)}
title="Informações"
> Info</button>
{image.original_image_id && (
<button
className="btn btn-small btn-danger"
onClick={resetToOriginal}
disabled={resetting}
title="Resetar para Original"
> Reset</button>
{!isMobile && (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{activeView === 'transform' && step === 'finetune' && (
<button type="button" className="btn btn-secondary btn-small" onClick={() => setStep('choose')} title="Voltar para escolher a orientação"> Posições</button>
)}
<button className={`btn btn-small ${activeView === 'transform' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setActiveView('transform')} title="Ajustar Imagem">🔄 Ajustar</button>
<button className={`btn btn-small ${activeView === 'gto' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setActiveView('gto')} title="Gerenciar GTOs">📋 GTOs</button>
<button className={`btn btn-small ${showInfo ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setShowInfo(!showInfo)} title="Informações"> Info</button>
{image.original_image_id && (
<button className="btn btn-small btn-danger" onClick={resetToOriginal} disabled={resetting} title="Resetar para Original"> Reset</button>
)}
</div>
)}
</div>
{isMobile && (
<div style={{ display: 'flex', gap: 8, width: '100%', overflowX: 'auto', paddingBottom: 4 }}>
{activeView === 'transform' && step === 'finetune' && (
<button type="button" className="btn btn-secondary btn-small" onClick={() => setStep('choose')} style={{ flexShrink: 0 }}> Posições</button>
)}
<button className={`btn btn-small ${activeView === 'transform' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setActiveView('transform')} style={{ flex: 1, flexShrink: 0 }}>🔄 Ajustar</button>
<button className={`btn btn-small ${activeView === 'gto' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setActiveView('gto')} style={{ flex: 1, flexShrink: 0 }}>📋 GTOs</button>
<button className={`btn btn-small ${showInfo ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setShowInfo(!showInfo)} style={{ flex: 1, flexShrink: 0 }}> Info</button>
{image.original_image_id && (
<button className="btn btn-small btn-danger" onClick={resetToOriginal} disabled={resetting} style={{ flexShrink: 0 }}> Reset</button>
)}
</div>
)}
</header>
<div className="content-scroll" style={{ padding: isMobile ? '10px' : '20px' }}>
@@ -378,52 +371,50 @@ export default function ImageDetailsPage() {
>Categoria </button>
</div>
{/* Controles de Sub-página (quando horizontal no mobile para mostrar só 2) */}
{useTwoCards && (
{/* Controles de Rotação (Mobile exibe 1 por vez) */}
{useOneCard && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 16, marginBottom: 16 }}>
<button
className="btn btn-primary btn-small"
disabled={orientationSubpage === 0}
onClick={() => setOrientationSubpage(0)}
> Opções Anteriores</button>
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{orientationSubpage + 1}/2</span>
onClick={() => setOrientationSubpage((p) => (p - 1 + 4) % 4)}
> Opção Anterior</button>
<span style={{ fontSize: '0.9rem', color: 'var(--text-primary)', fontWeight: 600 }}>{orientationSubpage + 1} de 4</span>
<button
className="btn btn-primary btn-small"
disabled={orientationSubpage === 1}
onClick={() => setOrientationSubpage(1)}
>Mais Opções </button>
onClick={() => setOrientationSubpage((p) => (p + 1) % 4)}
>Próxima Opção </button>
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: useTwoCards ? '1fr' : (isMobile ? 'repeat(2, 1fr)' : 'repeat(4, 1fr)'), gap: 16 }}>
<div style={{ display: 'grid', gridTemplateColumns: useOneCard ? '1fr' : 'repeat(4, 1fr)', gap: 16 }}>
{currentRotations.map((deg) => {
const pg = choosePages[choosePage];
return (
<button
<div
key={deg}
type="button"
onClick={() => pickOrientation(deg)}
style={{
border: '2px solid var(--glass-border)', borderRadius: 10, background: '#0b1220',
padding: useTwoCards ? 20 : 16, cursor: 'pointer', display: 'flex', flexDirection: 'column',
alignItems: 'center', gap: 12, transition: 'all .2s ease'
border: '1px solid var(--border-color)', borderRadius: 10, background: 'var(--bg-color)',
padding: useOneCard ? 0 : 16, display: 'flex', flexDirection: 'column',
alignItems: 'center', transition: 'all .2s ease', overflow: 'hidden'
}}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--primary-color)'; e.currentTarget.style.transform = 'translateY(-2px)'; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--glass-border)'; e.currentTarget.style.transform = 'translateY(0)'; }}
title={`Usar ${deg}° ${pg.flipH ? '(espelho H)' : pg.flipV ? '(espelho V)' : ''}`}
>
<div style={{ width: '100%', aspectRatio: useTwoCards ? 'auto' : '1 / 1', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: '100%', aspectRatio: useOneCard ? 'auto' : '1 / 1', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: useOneCard ? 20 : 0, background: '#0b1220' }}>
<img
src={imageUrl}
alt={`${deg}°`}
loading="lazy"
style={{ width: useTwoCards ? '90%' : '80%', maxHeight: useTwoCards ? 300 : '100%', objectFit: 'contain', transform: cssTransform(deg, pg.flipH, pg.flipV), transformOrigin: 'center' }}
style={{ width: useOneCard ? '100%' : '80%', maxHeight: useOneCard ? '50vh' : '100%', objectFit: 'contain', transform: cssTransform(deg, pg.flipH, pg.flipV), transformOrigin: 'center' }}
/>
</div>
<span style={{ fontSize: '0.9rem', fontWeight: 600, color: 'var(--text-secondary)' }}>
{deg}°{pg.flipH ? ' ⇆' : pg.flipV ? ' ⇅' : ''}
</span>
</button>
<div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center', width: '100%', background: 'var(--bg-surface)' }}>
<span style={{ fontSize: '1rem', fontWeight: 600, color: 'var(--text-primary)' }}>
{deg}°{pg.flipH ? ' ⇆' : pg.flipV ? ' ⇅' : ''}
</span>
<button className="btn btn-primary" style={{ width: '100%' }} onClick={() => pickOrientation(deg)}>
Escolher esta
</button>
</div>
</div>
);
})}
</div>
@@ -127,16 +127,10 @@ export default function PatientsPage() {
🔄
</button>
</span>
<span className="show-mobile">
<DropdownMenu
label="Ações"
buttonClassName="btn btn-primary"
items={[
{ label: 'Pesquisar', icon: '🔍', onClick: () => setIsSearchMobileOpen(!isSearchMobileOpen) },
{ label: 'Novo Paciente', icon: '', onClick: () => setShowCreate(true) },
{ label: 'Atualizar lista', icon: '🔄', onClick: () => loadPatients(true) },
]}
/>
<span className="show-mobile mobile-icon-actions">
<button className="btn-icon" onClick={() => setIsSearchMobileOpen(!isSearchMobileOpen)} title="Pesquisar">🔍</button>
<button className="btn-icon" onClick={() => setShowCreate(true)} title="Novo Paciente"></button>
<button className="btn-icon" onClick={() => loadPatients(true)} title="Atualizar">🔄</button>
</span>
</div>
</div>