-
-
{renderTabs()}
{renderActivePanel()}
@@ -2111,6 +2082,57 @@ function connectionCheckView(input: ConnectionCheckInput): ConnectionCheckView {
};
}
+function summaryRouteFlow(input: RouteChainInput): SummaryRouteFlow {
+ if (input.isDetectingComponents || input.appCount <= 0) return 'idle';
+ if (!input.proxyfier?.running) return 'direct';
+ return 'proxy';
+}
+
+function summaryRouteChainSegments(input: RouteChainInput, flow: SummaryRouteFlow): RouteChainSegment[] {
+ if (flow === 'proxy') return routeChainSegments(input);
+
+ const appSegment: RouteChainSegment = {
+ id: 'apps',
+ label: 'Приложения',
+ value: appCountCompact(input.appCount),
+ tone: input.appCount > 0 ? 'ok' : 'warning',
+ details: [
+ input.appCount > 0 ? appCountText(input.appCount) : 'Добавь хотя бы одно приложение на вкладке ProxiFyre.',
+ ],
+ };
+
+ if (flow === 'idle') {
+ return [
+ appSegment,
+ {
+ id: 'idle',
+ label: 'Трафик',
+ value: input.isDetectingComponents ? 'проверяю' : 'ожидает приложения',
+ tone: input.isDetectingComponents ? 'checking' : 'warning',
+ details: input.isDetectingComponents
+ ? ['Проверяю ProxiFyre и текущий маршрут.']
+ : ['Пока нет выбранных приложений, ProxyWarden ничего не маршрутизирует.'],
+ },
+ ];
+ }
+
+ return [
+ appSegment,
+ {
+ id: 'direct',
+ label: 'Интернет',
+ value: 'напрямую',
+ tone: 'warning',
+ details: [
+ input.proxyfier?.installed
+ ? 'ProxiFyre остановлен, поэтому выбранные приложения не перехватываются.'
+ : 'ProxiFyre не найден, поэтому выбранные приложения не перехватываются.',
+ 'Пакеты идут обычным системным маршрутом без SOCKS5.',
+ ],
+ },
+ ];
+}
+
function routeChainSegments(input: RouteChainInput): RouteChainSegment[] {
const proxyValidation = input.routeMode === 'external' && input.proxyInput.trim()
? safeProxyError(input.proxyInput)
diff --git a/src/assets/proxywarden-toggle-off.png b/src/assets/proxywarden-toggle-off.png
new file mode 100644
index 0000000..8965175
Binary files /dev/null and b/src/assets/proxywarden-toggle-off.png differ
diff --git a/src/assets/proxywarden-toggle-on.png b/src/assets/proxywarden-toggle-on.png
new file mode 100644
index 0000000..a14cc08
Binary files /dev/null and b/src/assets/proxywarden-toggle-on.png differ
diff --git a/src/styles/app.css b/src/styles/app.css
index ac27f41..f9f97b9 100644
--- a/src/styles/app.css
+++ b/src/styles/app.css
@@ -3,7 +3,7 @@
--app-change-dock-height: 0px;
--app-admin-prompt-height: 0px;
--change-row-count: 1;
- --app-header-row-height: 50px;
+ --app-header-row-height: 0px;
--app-tab-height: 46px;
--app-header-height: calc(var(--app-header-row-height) + var(--app-tab-height));
--motion-fast: 120ms;
@@ -776,7 +776,8 @@ button:disabled {
.simple-panel {
display: grid;
- align-content: start;
+ grid-template-rows: minmax(0, 1fr);
+ align-content: stretch;
min-height: 0;
height: calc(100vh - var(--app-header-height) - var(--app-footer-height) - var(--app-change-dock-height) - var(--app-admin-prompt-height));
width: 100%;
@@ -973,6 +974,8 @@ button:disabled {
.tab-panel-frame {
min-width: 0;
+ min-height: 100%;
+ height: 100%;
will-change: opacity, transform;
}
@@ -1054,98 +1057,133 @@ button:disabled {
}
.summary-panel {
- gap: 12px;
+ grid-template-rows: auto minmax(0, 1fr);
+ align-content: stretch;
+ justify-items: center;
+ gap: 0;
+ min-height: 100%;
+ height: 100%;
+ width: 100%;
+ padding-top: clamp(12px, 3vh, 28px);
+}
+
+.summary-brand-title {
+ margin: 0;
+ color: #f8fafc;
+ font-size: clamp(22px, 4vw, 34px);
+ font-weight: 800;
+ line-height: 1.05;
+ text-align: center;
+}
+
+.summary-main {
+ align-self: stretch;
+ display: grid;
+ grid-template-columns: minmax(220px, 1fr) minmax(220px, 288px);
+ gap: clamp(22px, 5vw, 54px);
+ align-items: center;
+ width: min(100%, 760px);
+ min-height: 0;
+ padding: 0 18px clamp(34px, 10vh, 92px);
}
.summary-status-control {
position: relative;
isolation: isolate;
display: grid;
- grid-template-columns: auto minmax(0, 1fr) auto;
- gap: 12px;
- align-items: center;
- border: 1px solid #2b3342;
- border-radius: 4px;
- background: #151923;
- padding: 13px 14px;
+ align-content: center;
+ align-self: center;
+ justify-items: center;
+ gap: 14px;
+ width: 100%;
+ overflow: visible;
+ padding: 0;
+ text-align: center;
}
-.summary-status-control > :not(.ui-busy-ring) {
- position: relative;
- z-index: 1;
-}
-
-.summary-status-control.ok {
- border-color: rgba(34, 197, 94, 0.44);
-}
-
-.summary-status-control.warning {
- border-color: rgba(245, 158, 11, 0.48);
-}
-
-.summary-status-control.error {
- border-color: rgba(239, 68, 68, 0.52);
-}
-
-.summary-status-control.checking {
- border-color: rgba(59, 130, 246, 0.54);
-}
-
-.summary-status-dot {
- width: 14px;
- height: 14px;
- border-radius: 999px;
- background: #64748b;
- margin-top: 5px;
-}
-
-.summary-status-dot.ok {
- background: #22c55e;
- box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.12);
-}
-
-.summary-status-dot.warning {
- background: #f59e0b;
- box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.12);
-}
-
-.summary-status-dot.error {
- background: #ef4444;
- box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.12);
-}
-
-.summary-status-dot.checking {
- background: #60a5fa;
- box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.14);
-}
-
-.summary-status-copy span,
.summary-card-label {
color: #8d99ae;
font-size: 12px;
font-weight: 700;
}
-.summary-status-copy {
- min-width: 0;
+.summary-toggle-button {
+ appearance: none;
+ position: relative;
+ isolation: isolate;
+ display: grid;
+ place-items: center;
+ width: clamp(180px, 30vw, 268px);
+ aspect-ratio: 1;
+ overflow: hidden;
+ border: 0;
+ border-radius: 50%;
+ background: transparent;
+ box-shadow:
+ 0 22px 54px rgba(0, 0, 0, 0.48),
+ 0 0 38px rgba(59, 130, 246, 0.1);
+ cursor: pointer;
+ padding: 0;
+ transition:
+ border-color var(--motion-fast) var(--ease-out),
+ box-shadow var(--motion-fast) var(--ease-out),
+ opacity var(--motion-fast) var(--ease-out),
+ transform var(--motion-fast) var(--ease-out);
}
-.summary-status-copy strong {
+.summary-toggle-button:hover:not(:disabled) {
+ box-shadow:
+ 0 26px 62px rgba(0, 0, 0, 0.54),
+ 0 0 52px rgba(59, 130, 246, 0.18);
+ transform: translateY(-2px);
+}
+
+.summary-toggle-button:active:not(:disabled) {
+ transform: scale(0.97);
+}
+
+.summary-toggle-button:focus-visible {
+ outline: 0;
+ box-shadow:
+ 0 0 0 2px rgba(59, 130, 246, 0.72),
+ 0 22px 54px rgba(0, 0, 0, 0.48);
+}
+
+.summary-toggle-button > :not(.ui-busy-ring) {
+ position: relative;
+ z-index: 1;
+}
+
+.summary-toggle-button img {
display: block;
- color: #f8fafc;
- font-size: 22px;
- line-height: 1.15;
- margin-top: 2px;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ object-fit: cover;
+ pointer-events: none;
+ transform: scale(1.18);
+ user-select: none;
}
-.summary-status-copy p {
- color: #b6c2d4;
- margin: 4px 0 0;
- overflow-wrap: anywhere;
+.summary-state-label {
+ color: #e5e7eb;
+ font-size: clamp(22px, 3.4vw, 34px);
+ font-weight: 800;
+ line-height: 1.1;
+ text-align: center;
}
-.summary-status-button {
- min-width: 148px;
+.summary-state-label.ok {
+ color: #bbf7d0;
+}
+
+.summary-state-label.warning,
+.summary-state-label.checking {
+ color: #fcd34d;
+}
+
+.summary-state-label.error {
+ color: #fecaca;
}
.summary-readout {
@@ -1983,12 +2021,90 @@ button.summary-card:hover {
}
.route-chain {
+ position: relative;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 7px;
margin-top: 8px;
}
+.route-chain--vertical {
+ grid-template-columns: 1fr;
+ gap: 10px;
+ width: 100%;
+ max-width: 288px;
+ margin-top: 0;
+}
+
+.route-chain-rail {
+ position: absolute;
+ z-index: 0;
+ top: 28px;
+ bottom: 28px;
+ left: 13px;
+ width: 2px;
+ overflow: hidden;
+ border-radius: 999px;
+ background: linear-gradient(
+ 180deg,
+ rgba(96, 165, 250, 0.08),
+ rgba(96, 165, 250, 0.34) 28%,
+ rgba(34, 197, 94, 0.28) 72%,
+ rgba(96, 165, 250, 0.08)
+ );
+ pointer-events: none;
+}
+
+.route-chain--direct .route-chain-rail {
+ background: linear-gradient(
+ 180deg,
+ rgba(245, 158, 11, 0.08),
+ rgba(245, 158, 11, 0.38) 32%,
+ rgba(245, 158, 11, 0.3) 68%,
+ rgba(245, 158, 11, 0.08)
+ );
+}
+
+.route-chain--idle .route-chain-rail {
+ background: linear-gradient(
+ 180deg,
+ rgba(100, 116, 139, 0.06),
+ rgba(100, 116, 139, 0.24) 48%,
+ rgba(100, 116, 139, 0.06)
+ );
+}
+
+.route-chain-packet {
+ position: absolute;
+ top: -8px;
+ left: 50%;
+ width: 6px;
+ height: 6px;
+ border-radius: 999px;
+ background: #93c5fd;
+ box-shadow:
+ 0 0 8px rgba(96, 165, 250, 0.95),
+ 0 0 16px rgba(34, 197, 94, 0.35);
+ transform: translateX(-50%);
+ animation: route-packet-flow 2.7s linear infinite;
+ animation-delay: -0.15s;
+}
+
+.route-chain--direct .route-chain-packet {
+ background: #fcd34d;
+ box-shadow:
+ 0 0 8px rgba(245, 158, 11, 0.9),
+ 0 0 16px rgba(245, 158, 11, 0.28);
+}
+
+.route-chain-packet--two {
+ animation-delay: -1.05s;
+}
+
+.route-chain-packet--three {
+ animation-delay: -1.95s;
+}
+
.route-chain-segment {
position: relative;
isolation: isolate;
@@ -2010,6 +2126,17 @@ button.summary-card:hover {
width: 100%;
}
+.route-chain--vertical .route-chain-segment {
+ z-index: 1;
+ grid-template-columns: auto minmax(0, 1fr);
+ grid-template-areas:
+ "dot label"
+ "dot value";
+ min-height: 58px;
+ background: rgba(17, 23, 32, 0.94);
+ padding: 9px 10px 9px 9px;
+}
+
.route-chain-segment > :not(.ui-busy-ring) {
position: relative;
z-index: 1;
@@ -2052,6 +2179,22 @@ button.summary-card:hover {
font-weight: 800;
}
+.route-chain--vertical .route-chain-arrow {
+ position: absolute;
+ z-index: 2;
+ bottom: -17px;
+ left: 9px;
+ color: #60a5fa;
+ font-size: 13px;
+ line-height: 1;
+ text-shadow: 0 0 8px rgba(96, 165, 250, 0.52);
+}
+
+.route-chain--direct .route-chain-arrow {
+ color: #f59e0b;
+ text-shadow: 0 0 8px rgba(245, 158, 11, 0.42);
+}
+
.route-chain-segment.ok .route-chain-dot {
background: #22c55e;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
@@ -2774,6 +2917,23 @@ button.summary-card:hover {
}
}
+@keyframes route-packet-flow {
+ 0% {
+ top: -8px;
+ opacity: 0;
+ }
+
+ 8%,
+ 88% {
+ opacity: 1;
+ }
+
+ 100% {
+ top: calc(100% + 8px);
+ opacity: 0;
+ }
+}
+
@media (prefers-reduced-motion: reduce) {
.tab-panel-frame,
.tab-panel,
@@ -2784,7 +2944,8 @@ button.summary-card:hover {
.finder-border-glow-segment,
.ui-service-border-glow-segment,
.status-light,
- .summary-status-dot.checking {
+ .summary-toggle-button,
+ .route-chain-packet {
animation: none;
transition: none;
}
@@ -2900,19 +3061,22 @@ button.summary-card:hover {
gap: 4px;
}
- .summary-status-control {
+ .summary-main {
grid-template-columns: 1fr;
- align-items: stretch;
+ align-content: center;
+ justify-items: center;
+ gap: 22px;
+ padding: 0 10px clamp(24px, 8vh, 64px);
}
- .summary-status-dot {
- display: none;
- }
-
- .summary-status-button {
+ .summary-status-control {
width: 100%;
}
+ .summary-toggle-button {
+ width: min(202px, 64vw);
+ }
+
.setup-strip {
grid-template-columns: 1fr;
align-items: stretch;
@@ -2962,6 +3126,11 @@ button.summary-card:hover {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+ .route-chain--vertical {
+ grid-template-columns: 1fr;
+ width: min(100%, 300px);
+ }
+
.add-toolbar {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));