fix(geocheck): сквозные клики через портал и safe-зоны в полноэкранном режиме

Три поломки, замеченные на dev.

Клики внутри модалки срабатывали и на том, что под ней. Модалка рендерилась
внутри `<div onClick>` карточки ноды: портал уносит её в document.body только
по DOM, а события React прогоняет по дереву компонентов, поэтому каждый клик
всплывал в обработчик аккордеона. Теперь модалка — сосед кликабельного блока,
а не его потомок. Проверено A/B на настоящей модалке: в старой структуре клик
по вкладке внутри модалки поднимал счётчик карточки до 1, в новой остаётся 0.
По существу правка в 6 строк, остальное в дифе — переотступы от обёртки.

В полноэкранном режиме отступы обнулялись целиком, и в Mini App панель
инструментов уезжала под шапку Telegram, а кнопки зума — под home indicator.
Теперь safe-зоны соблюдаются и в полноэкранном режиме, просто без лишнего
сантиметра. Замер при отступах 96/34 на экране 844: диалог занимает 96..810,
низ зум-бара на 784 — оба внутри безопасной зоны, было наоборот.

Высота диалога считалась от `100dvh` мимо safe-зон, из-за чего он мог вылезти
за них даже в обычном режиме. Заменено на `max-h-full`: отступы контейнера
уже урезали доступное место.

Заодно в полноэкранном режиме шапка ужата в одну строку с именем ноды —
вертикальное место ровно то, ради чего его включают.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
c0mrade
2026-08-19 17:23:30 +03:00
parent 28e25a9292
commit debe26ae35
2 changed files with 280 additions and 249 deletions

View File

@@ -65,11 +65,20 @@ export function GeoCheckModal({ node, onClose }: GeoCheckModalProps) {
return createPortal( return createPortal(
<div <div
className="fixed inset-0 z-[9999] flex items-center justify-center" className="fixed inset-0 z-[9999] flex items-center justify-center"
// Safe-зоны обязательны и в полноэкранном режиме: в Mini App сверху
// висит шапка Telegram, снизу — home indicator, и обнулённые отступы
// прятали под ними панель инструментов и кнопки зума.
style={{ style={{
paddingTop: fullscreen ? 0 : 'max(1rem, env(safe-area-inset-top))', paddingTop: fullscreen ? 'env(safe-area-inset-top)' : 'max(1rem, env(safe-area-inset-top))',
paddingBottom: fullscreen ? 0 : 'max(1rem, env(safe-area-inset-bottom))', paddingBottom: fullscreen
paddingLeft: fullscreen ? 0 : 'max(1rem, env(safe-area-inset-left))', ? 'env(safe-area-inset-bottom)'
paddingRight: fullscreen ? 0 : 'max(1rem, env(safe-area-inset-right))', : 'max(1rem, env(safe-area-inset-bottom))',
paddingLeft: fullscreen
? 'env(safe-area-inset-left)'
: 'max(1rem, env(safe-area-inset-left))',
paddingRight: fullscreen
? 'env(safe-area-inset-right)'
: 'max(1rem, env(safe-area-inset-right))',
}} }}
> >
<div <div
@@ -85,23 +94,40 @@ export function GeoCheckModal({ node, onClose }: GeoCheckModalProps) {
tabIndex={-1} tabIndex={-1}
className={cn( className={cn(
'relative flex w-full flex-col border border-dark-700 bg-dark-900 shadow-2xl', 'relative flex w-full flex-col border border-dark-700 bg-dark-900 shadow-2xl',
// max-h-full, а не 100dvh: отступы safe-зон уже урезали контейнер,
// и абсолютная высота вылезала бы за них.
fullscreen fullscreen
? 'h-full max-h-none max-w-none rounded-none p-3' ? 'h-full max-h-full max-w-none rounded-none p-3'
: 'max-h-[calc(100dvh-2rem)] overflow-y-auto rounded-2xl p-5', : 'max-h-full overflow-y-auto rounded-2xl p-5',
// Отчёт заметно шире формы запуска — под него модалка расширяется. // Отчёт заметно шире формы запуска — под него модалка расширяется.
!fullscreen && (job.phase === 'done' ? 'max-w-4xl' : 'max-w-2xl'), !fullscreen && (job.phase === 'done' ? 'max-w-4xl' : 'max-w-2xl'),
)} )}
> >
<div className="mb-4 flex items-center justify-between gap-3"> {/* В полноэкранном режиме шапка ужимается в одну строку: место по
вертикали — ровно то, ради чего его и включают. */}
<div
className={cn('flex items-center justify-between gap-3', fullscreen ? 'mb-2' : 'mb-4')}
>
<div className="flex min-w-0 items-center gap-2.5"> <div className="flex min-w-0 items-center gap-2.5">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-accent-500/15 text-accent-400"> <span
<GeoCheckIcon className="h-5 w-5" /> className={cn(
'flex shrink-0 items-center justify-center rounded-xl bg-accent-500/15 text-accent-400',
fullscreen ? 'h-7 w-7' : 'h-9 w-9',
)}
>
<GeoCheckIcon className={fullscreen ? 'h-4 w-4' : 'h-5 w-5'} />
</span> </span>
<div className="min-w-0"> <div className="min-w-0">
<h3 id="geocheck-modal-title" className="text-lg font-semibold text-dark-100"> <h3
{t('admin.remnawave.geoCheck.title', 'GeoCheck')} id="geocheck-modal-title"
className={cn(
'font-semibold text-dark-100',
fullscreen ? 'truncate text-sm' : 'text-lg',
)}
>
{fullscreen ? node.name : t('admin.remnawave.geoCheck.title', 'GeoCheck')}
</h3> </h3>
<p className="truncate text-xs text-dark-400">{node.name}</p> {!fullscreen && <p className="truncate text-xs text-dark-400">{node.name}</p>}
</div> </div>
</div> </div>
{!isRunning && ( {!isRunning && (

View File

@@ -201,7 +201,11 @@ function NodeCard({ node, providerName, realtime, onAction, isLoading }: NodeCar
? 'text-warning-400' ? 'text-warning-400'
: 'text-dark-400'; : 'text-dark-400';
// Модалка — сосед кликабельного блока, а не его потомок: портал уносит её
// в document.body только по DOM, а события React прогоняет по дереву
// компонентов, и клики внутри неё всплывали бы в onClick карточки.
return ( return (
<>
<div <div
className={`rounded-xl border border-dark-700 bg-dark-800/50 p-3.5 transition-colors hover:border-dark-600 ${ className={`rounded-xl border border-dark-700 bg-dark-800/50 p-3.5 transition-colors hover:border-dark-600 ${
hasBreakdown ? 'cursor-pointer' : '' hasBreakdown ? 'cursor-pointer' : ''
@@ -453,9 +457,10 @@ function NodeCard({ node, providerName, realtime, onAction, isLoading }: NodeCar
)} )}
</div> </div>
)} )}
</div>
{geoCheckOpen && <GeoCheckModal node={node} onClose={() => setGeoCheckOpen(false)} />} {geoCheckOpen && <GeoCheckModal node={node} onClose={() => setGeoCheckOpen(false)} />}
</div> </>
); );
} }