feat(EasySwitch): enhance Easy Switch UI with new styles and layout adjustments for improved user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m5s

This commit is contained in:
2026-02-24 11:46:30 +07:00
parent 71685ad310
commit 02a45fce71
2 changed files with 119 additions and 160 deletions
+76
View File
@@ -697,6 +697,82 @@ body.sidebar-overlay-open {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* Easy Switch: bring visuals in line with Tabler pages */
.easy-switch-page .card-tabs .nav-link {
font-weight: 500;
}
.easy-switch-server-header {
background-color: rgba(32, 107, 196, 0.05);
cursor: pointer;
user-select: none;
}
.easy-switch-flag {
font-size: 1.5rem;
line-height: 1;
}
.easy-switch-gateway-card {
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
border: 1px solid var(--tblr-border-color);
position: relative;
}
.easy-switch-gateway-card:hover {
transform: translateY(-1px);
box-shadow: 0 0.125rem 0.375rem rgba(32, 41, 57, 0.1);
}
.easy-switch-gateway-card-active {
border: 1px solid var(--tblr-primary) !important;
box-shadow: 0 0.25rem 0.75rem rgba(32, 107, 196, 0.25);
}
.easy-switch-active-check {
width: 20px;
height: 20px;
position: absolute;
top: 8px;
right: 8px;
}
.easy-switch-fast-slot {
min-height: 18px;
}
.easy-switch-fast-badge {
font-size: 0.65rem;
padding: 2px 6px;
}
.easy-switch-gateway-flag {
width: 28px;
height: 28px;
font-size: 1.25rem;
line-height: 1;
background-color: rgba(0, 0, 0, 0.06);
}
.easy-switch-gateway-flag-active {
background-color: rgba(255, 255, 255, 0.25);
}
.easy-switch-gateway-title {
font-size: 0.85rem;
line-height: 1.2;
word-break: break-word;
}
.easy-switch-gateway-ip {
font-size: 0.75rem;
}
.easy-switch-gateway-ping {
font-size: 1.1rem;
}
/* Ripple effect для кнопок */
@keyframes ripple {
to {
+43 -160
View File
@@ -505,7 +505,7 @@ function EasySwitchManager() {
if (loading && servers.length === 0) {
return (
<div className="container-xl">
<div className="easy-switch-page">
<PageHeader
title="Easy Switch"
icon={<IconWorld size={24} />}
@@ -525,7 +525,7 @@ function EasySwitchManager() {
}
return (
<div className="container-xl">
<div className="easy-switch-page">
<PageHeader
title="Easy Switch"
icon={<IconWorld size={24} />}
@@ -554,7 +554,7 @@ function EasySwitchManager() {
/>
{/* Вкладки */}
<div className="mb-3">
<div className="card card-tabs mb-3">
<ul className="nav nav-tabs" role="tablist">
<li className="nav-item" role="presentation">
<button
@@ -599,8 +599,9 @@ function EasySwitchManager() {
)}
{/* Панель фильтров */}
<div className="mb-3">
<div className="row g-2 align-items-center">
<div className="card mb-3">
<div className="card-body py-3">
<div className="row g-2 align-items-center">
{/* Переключатель "Только настроенные" */}
<div className="col-auto">
<label className="form-check form-switch mb-0">
@@ -679,17 +680,18 @@ function EasySwitchManager() {
</button>
</div>
<div className="col-auto">
<button className="btn btn-icon" title="Настройки">
<button className="btn btn-outline-secondary btn-icon" title="Настройки">
<IconSettings size={18} />
</button>
</div>
<div className="col-auto">
<button className="btn btn-icon" title="Быстрые действия">
<button className="btn btn-outline-secondary btn-icon" title="Быстрые действия">
<IconFilter size={18} />
</button>
</div>
</div>
</div>
</div>
{/* Список серверов с communities и шлюзами */}
{filteredServers.length === 0 ? (
@@ -726,12 +728,7 @@ function EasySwitchManager() {
<div key={server.id} className="card mb-3">
{/* Заголовок сервера (кликабельный) */}
<div
className="card-header py-2"
style={{
backgroundColor: 'rgba(32, 107, 196, 0.05)',
cursor: 'pointer',
userSelect: 'none'
}}
className="card-header py-2 easy-switch-server-header"
onClick={() => toggleServerExpanded(server.id)}
>
<div className="d-flex align-items-center">
@@ -742,7 +739,7 @@ function EasySwitchManager() {
<IconChevronDown size={20} className="me-2 text-muted" />
)}
{flag && <span className="me-2" style={{ fontSize: '1.5rem' }}>{flag}</span>}
{flag && <span className="me-2 easy-switch-flag">{flag}</span>}
<IconServer size={20} className="me-2 text-primary" />
<h3 className="card-title mb-0 me-2">{server.name}</h3>
{server.description && (
@@ -793,7 +790,7 @@ function EasySwitchManager() {
<div key={tag} className="mb-4">
{/* Заголовок группы тегов */}
<div className="mb-2 pb-2 border-bottom">
<span className="badge bg-purple-lt text-purple me-2" style={{ fontSize: '0.85rem', padding: '6px 10px' }}>
<span className="badge bg-purple-lt text-purple me-2">
{tag}
</span>
<span className="text-muted small">
@@ -813,12 +810,12 @@ function EasySwitchManager() {
<div className="mb-2">
{/* Заголовок community */}
<div className="mb-2">
<span className="badge bg-blue-lt text-blue me-2" style={{ fontSize: '0.8rem' }}>
<span className="badge bg-blue-lt text-blue me-2">
{comm.value}
</span>
<span className="fw-medium">{comm.name || comm.description || ''}</span>
{hasFilterForComm && (
<span className="badge bg-green-lt text-green ms-2" style={{ fontSize: '0.7rem' }}>
<span className="badge bg-green-lt text-green ms-2">
настроен
</span>
)}
@@ -842,50 +839,21 @@ function EasySwitchManager() {
return (
<div key={gw.name} className="col-12 col-sm-4">
<div
className={`card cursor-pointer h-100 ${isActive ? 'bg-primary text-white' : ''}`}
style={{
cursor: 'pointer',
transition: 'all 0.15s ease',
border: isActive ? '2px solid var(--tblr-primary)' : '1px solid rgba(0,0,0,0.1)',
borderRadius: '6px',
boxShadow: isActive ? '0 3px 8px rgba(32, 107, 196, 0.3)' : 'none',
position: 'relative'
}}
className={`card h-100 easy-switch-gateway-card ${isActive ? 'easy-switch-gateway-card-active bg-primary text-white' : ''}`}
onClick={() => handleGatewaySelect(server.id, comm.value, gw.name)}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
e.currentTarget.style.transform = 'translateY(-1px)';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.transform = 'translateY(0)';
}
}}
>
<div className="card-body p-2">
{/* Галочка (абсолютное позиционирование) */}
{isActive && (
<div
className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center"
style={{
width: '20px',
height: '20px',
position: 'absolute',
top: '8px',
right: '8px'
}}
>
<div className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center easy-switch-active-check">
<IconCheck size={12} />
</div>
)}
{/* Метка Fastest */}
<div className="mb-1" style={{ minHeight: '18px' }}>
<div className="mb-1 easy-switch-fast-slot">
{isFastest && !isActive && (
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center" style={{ fontSize: '0.65rem', padding: '2px 6px' }}>
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center easy-switch-fast-badge">
<IconBolt size={10} className="me-1" />
Fast
</span>
@@ -895,22 +863,14 @@ function EasySwitchManager() {
{/* Флаг и название (комментарий) шлюза */}
<div className="d-flex align-items-center gap-2 mb-1">
<span
className="d-flex align-items-center justify-content-center flex-shrink-0 rounded"
style={{
width: '28px',
height: '28px',
fontSize: '1.25rem',
lineHeight: 1,
backgroundColor: isActive ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.06)',
}}
className={`d-flex align-items-center justify-content-center flex-shrink-0 rounded easy-switch-gateway-flag ${isActive ? 'easy-switch-gateway-flag-active' : ''}`}
title={gw.country || country || ''}
>
{countryToFlag(gw.country || country || '') || '🌐'}
</span>
<div className="flex-grow-1 min-w-0">
<div
className={`fw-bold ${isActive ? 'text-white' : ''}`}
style={{ fontSize: '0.85rem', lineHeight: '1.2', wordBreak: 'break-word' }}
className={`fw-bold easy-switch-gateway-title ${isActive ? 'text-white' : ''}`}
>
{gw.comment || gw.name}
</div>
@@ -919,17 +879,16 @@ function EasySwitchManager() {
{/* IP шлюза ниже */}
{gw.ip && (
<div
className={`small text-truncate ${
className={`small text-truncate easy-switch-gateway-ip ${
isActive ? 'text-white-75' : 'text-muted'
}`}
style={{ fontSize: '0.75rem' }}
>
{gw.ip}
</div>
)}
{/* Метрика (пинг) */}
<div className={`fw-bold mt-1 ${isActive ? 'text-white' : 'text-success'}`} style={{ fontSize: '1.1rem' }}>
<div className={`fw-bold mt-1 easy-switch-gateway-ping ${isActive ? 'text-white' : 'text-success'}`}>
{ping != null ? ping : '—'}
</div>
</div>
@@ -952,7 +911,7 @@ function EasySwitchManager() {
<div className="mb-4">
{/* Заголовок группы без тегов */}
<div className="mb-2 pb-2 border-bottom">
<span className="badge bg-gray-lt text-gray me-2" style={{ fontSize: '0.85rem', padding: '6px 10px' }}>
<span className="badge bg-gray-lt text-gray me-2">
Без тегов
</span>
<span className="text-muted small">
@@ -972,12 +931,12 @@ function EasySwitchManager() {
<div className="mb-2">
{/* Заголовок community */}
<div className="mb-2">
<span className="badge bg-blue-lt text-blue me-2" style={{ fontSize: '0.8rem' }}>
<span className="badge bg-blue-lt text-blue me-2">
{comm.value}
</span>
<span className="fw-medium">{comm.name || comm.description || ''}</span>
{hasFilterForComm && (
<span className="badge bg-green-lt text-green ms-2" style={{ fontSize: '0.7rem' }}>
<span className="badge bg-green-lt text-green ms-2">
настроен
</span>
)}
@@ -1001,50 +960,21 @@ function EasySwitchManager() {
return (
<div key={gw.name} className="col-12 col-sm-4">
<div
className={`card cursor-pointer h-100 ${isActive ? 'bg-primary text-white' : ''}`}
style={{
cursor: 'pointer',
transition: 'all 0.15s ease',
border: isActive ? '2px solid var(--tblr-primary)' : '1px solid rgba(0,0,0,0.1)',
borderRadius: '6px',
boxShadow: isActive ? '0 3px 8px rgba(32, 107, 196, 0.3)' : 'none',
position: 'relative'
}}
className={`card h-100 easy-switch-gateway-card ${isActive ? 'easy-switch-gateway-card-active bg-primary text-white' : ''}`}
onClick={() => handleGatewaySelect(server.id, comm.value, gw.name)}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
e.currentTarget.style.transform = 'translateY(-1px)';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.transform = 'translateY(0)';
}
}}
>
<div className="card-body p-2">
{/* Галочка (абсолютное позиционирование) */}
{isActive && (
<div
className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center"
style={{
width: '20px',
height: '20px',
position: 'absolute',
top: '8px',
right: '8px'
}}
>
<div className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center easy-switch-active-check">
<IconCheck size={12} />
</div>
)}
{/* Метка Fastest */}
<div className="mb-1" style={{ minHeight: '18px' }}>
<div className="mb-1 easy-switch-fast-slot">
{isFastest && !isActive && (
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center" style={{ fontSize: '0.65rem', padding: '2px 6px' }}>
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center easy-switch-fast-badge">
<IconBolt size={10} className="me-1" />
Fast
</span>
@@ -1054,22 +984,14 @@ function EasySwitchManager() {
{/* Флаг и название (комментарий) шлюза */}
<div className="d-flex align-items-center gap-2 mb-1">
<span
className="d-flex align-items-center justify-content-center flex-shrink-0 rounded"
style={{
width: '28px',
height: '28px',
fontSize: '1.25rem',
lineHeight: 1,
backgroundColor: isActive ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.06)',
}}
className={`d-flex align-items-center justify-content-center flex-shrink-0 rounded easy-switch-gateway-flag ${isActive ? 'easy-switch-gateway-flag-active' : ''}`}
title={gw.country || country || ''}
>
{countryToFlag(gw.country || country || '') || '🌐'}
</span>
<div className="flex-grow-1 min-w-0">
<div
className={`fw-bold ${isActive ? 'text-white' : ''}`}
style={{ fontSize: '0.85rem', lineHeight: '1.2', wordBreak: 'break-word' }}
className={`fw-bold easy-switch-gateway-title ${isActive ? 'text-white' : ''}`}
>
{gw.comment || gw.name}
</div>
@@ -1078,17 +1000,16 @@ function EasySwitchManager() {
{/* IP шлюза ниже */}
{gw.ip && (
<div
className={`small text-truncate ${
className={`small text-truncate easy-switch-gateway-ip ${
isActive ? 'text-white-75' : 'text-muted'
}`}
style={{ fontSize: '0.75rem' }}
>
{gw.ip}
</div>
)}
{/* Метрика (пинг) */}
<div className={`fw-bold mt-1 ${isActive ? 'text-white' : 'text-success'}`} style={{ fontSize: '1.1rem' }}>
<div className={`fw-bold mt-1 easy-switch-gateway-ping ${isActive ? 'text-white' : 'text-success'}`}>
{ping != null ? ping : '—'}
</div>
</div>
@@ -1120,12 +1041,12 @@ function EasySwitchManager() {
<div className="mb-2">
{/* Заголовок community */}
<div className="mb-2">
<span className="badge bg-blue-lt text-blue me-2" style={{ fontSize: '0.8rem' }}>
<span className="badge bg-blue-lt text-blue me-2">
{comm.value}
</span>
<span className="fw-medium">{comm.name || comm.description || ''}</span>
{hasFilterForComm && (
<span className="badge bg-green-lt text-green ms-2" style={{ fontSize: '0.7rem' }}>
<span className="badge bg-green-lt text-green ms-2">
настроен
</span>
)}
@@ -1149,50 +1070,21 @@ function EasySwitchManager() {
return (
<div key={gw.name} className="col-12 col-sm-4">
<div
className={`card cursor-pointer h-100 ${isActive ? 'bg-primary text-white' : ''}`}
style={{
cursor: 'pointer',
transition: 'all 0.15s ease',
border: isActive ? '2px solid var(--tblr-primary)' : '1px solid rgba(0,0,0,0.1)',
borderRadius: '6px',
boxShadow: isActive ? '0 3px 8px rgba(32, 107, 196, 0.3)' : 'none',
position: 'relative'
}}
className={`card h-100 easy-switch-gateway-card ${isActive ? 'easy-switch-gateway-card-active bg-primary text-white' : ''}`}
onClick={() => handleGatewaySelect(server.id, comm.value, gw.name)}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
e.currentTarget.style.transform = 'translateY(-1px)';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.transform = 'translateY(0)';
}
}}
>
<div className="card-body p-2">
{/* Галочка (абсолютное позиционирование) */}
{isActive && (
<div
className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center"
style={{
width: '20px',
height: '20px',
position: 'absolute',
top: '8px',
right: '8px'
}}
>
<div className="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center easy-switch-active-check">
<IconCheck size={12} />
</div>
)}
{/* Метка Fastest */}
<div className="mb-1" style={{ minHeight: '18px' }}>
<div className="mb-1 easy-switch-fast-slot">
{isFastest && !isActive && (
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center" style={{ fontSize: '0.65rem', padding: '2px 6px' }}>
<span className="badge bg-yellow-lt text-yellow d-inline-flex align-items-center easy-switch-fast-badge">
<IconBolt size={10} className="me-1" />
Fast
</span>
@@ -1202,22 +1094,14 @@ function EasySwitchManager() {
{/* Флаг и название (комментарий) шлюза */}
<div className="d-flex align-items-center gap-2 mb-1">
<span
className="d-flex align-items-center justify-content-center flex-shrink-0 rounded"
style={{
width: '28px',
height: '28px',
fontSize: '1.25rem',
lineHeight: 1,
backgroundColor: isActive ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.06)',
}}
className={`d-flex align-items-center justify-content-center flex-shrink-0 rounded easy-switch-gateway-flag ${isActive ? 'easy-switch-gateway-flag-active' : ''}`}
title={gw.country || country || ''}
>
{countryToFlag(gw.country || country || '') || '🌐'}
</span>
<div className="flex-grow-1 min-w-0">
<div
className={`fw-bold ${isActive ? 'text-white' : ''}`}
style={{ fontSize: '0.85rem', lineHeight: '1.2', wordBreak: 'break-word' }}
className={`fw-bold easy-switch-gateway-title ${isActive ? 'text-white' : ''}`}
>
{gw.comment || gw.name}
</div>
@@ -1226,15 +1110,14 @@ function EasySwitchManager() {
{/* IP шлюза ниже */}
{gw.ip && (
<div
className={`small text-truncate mb-1 ${isActive ? 'text-white-75' : 'text-muted'}`}
style={{ fontSize: '0.75rem' }}
className={`small text-truncate mb-1 easy-switch-gateway-ip ${isActive ? 'text-white-75' : 'text-muted'}`}
>
{gw.ip}
</div>
)}
{/* Метрика (пинг) */}
<div className={`fw-bold mt-1 ${isActive ? 'text-white' : 'text-success'}`} style={{ fontSize: '1.1rem' }}>
<div className={`fw-bold mt-1 easy-switch-gateway-ping ${isActive ? 'text-white' : 'text-success'}`}>
{ping != null ? ping : '—'}
</div>
</div>