diff --git a/src/web/components/ClientOverviewPage.jsx b/src/web/components/ClientOverviewPage.jsx
index b55ba72..83e95e3 100644
--- a/src/web/components/ClientOverviewPage.jsx
+++ b/src/web/components/ClientOverviewPage.jsx
@@ -133,6 +133,30 @@ export function ClientOverviewPage({
{canStart ? 'Нажмите, чтобы подключиться' : 'Добавьте ссылку и выберите сервер'}
)}
+
+
+ Адрес
+
+ {proxyUrls.http.replace(/^https?:\/\//, '')}
+
+
+ {[
+ ['socks5', 'SOCKS5'],
+ ['http', 'HTTP'],
+ ].map(([kind, label]) => (
+
+ ))}
+
+
@@ -151,6 +175,7 @@ export function ClientOverviewPage({
-
- {[
- ['socks5', 'SOCKS5'],
- ['http', 'HTTP'],
- ].map(([kind, label]) => (
-
- ))}
-
);
}
diff --git a/src/web/styles.css b/src/web/styles.css
index 69921b4..ed9bfd9 100644
--- a/src/web/styles.css
+++ b/src/web/styles.css
@@ -866,7 +866,7 @@ code, .mono {
}
.client-shell {
- width: min(100%, 440px);
+ width: min(100%, 1100px);
display: grid;
gap: 12px;
font-family: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
@@ -874,8 +874,10 @@ code, .mono {
}
.client-panel {
+ position: relative;
display: grid;
- gap: 30px;
+ place-items: center;
+ min-height: 520px;
padding: 24px 0;
}
@@ -891,29 +893,35 @@ code, .mono {
text-align: center;
}
+.client-power-section > div {
+ min-height: 54px;
+}
+
.client-power {
width: 96px;
height: 96px;
display: grid;
place-items: center;
- border: 1px solid var(--client-border);
+ border: 0;
border-radius: 50%;
- background: var(--client-control);
+ background: transparent;
color: var(--client-muted);
cursor: pointer;
- transition: color 180ms ease, background 180ms ease, border-color 180ms ease, transform 180ms ease;
+ transition: color 600ms cubic-bezier(0.16, 1, 0.3, 1), transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-power:hover:not(:disabled) {
- transform: translateY(-2px);
- color: var(--client-text);
- border-color: var(--client-muted);
+ transform: translateY(-3px) scale(1.08);
+ color: var(--client-accent);
}
.client-power[aria-checked='true'] {
color: var(--client-accent);
- background: var(--client-accent-soft);
- border-color: var(--client-accent);
+}
+
+.client-power:active:not(:disabled) {
+ transform: translateY(0) scale(0.92);
+ transition-duration: 220ms;
}
.client-power:disabled {
@@ -928,6 +936,22 @@ code, .mono {
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
+ filter: drop-shadow(0 0 0 transparent);
+ transition: filter 700ms cubic-bezier(0.16, 1, 0.3, 1), transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.client-power:hover:not(:disabled) svg {
+ filter: drop-shadow(0 0 6px color-mix(in oklch, var(--client-accent) 75%, transparent)) drop-shadow(0 0 18px color-mix(in oklch, var(--client-accent) 35%, transparent));
+}
+
+.client-power:active:not(:disabled) svg {
+ transform: scale(0.88);
+ filter: drop-shadow(0 0 10px var(--client-accent)) drop-shadow(0 0 26px color-mix(in oklch, var(--client-accent) 55%, transparent));
+ transition-duration: 220ms;
+}
+
+.client-power[aria-checked='true'] svg {
+ filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-accent) 65%, transparent)) drop-shadow(0 0 14px color-mix(in oklch, var(--client-accent) 28%, transparent));
}
.client-power-section h2 {
@@ -950,6 +974,9 @@ code, .mono {
}
.client-form {
+ position: absolute;
+ left: calc(50% + 130px);
+ width: min(360px, calc(50% - 154px));
display: grid;
gap: 36px;
}
@@ -1028,6 +1055,33 @@ code, .mono {
color: var(--client-muted);
}
+.client-subscription-edit input.has-value {
+ color: transparent;
+ caret-color: transparent;
+}
+
+.client-subscription-edit input:-webkit-autofill,
+.client-subscription-edit input:-webkit-autofill:hover,
+.client-subscription-edit input:-webkit-autofill:focus {
+ box-shadow: 0 0 0 1000px var(--client-bg) inset;
+ -webkit-text-fill-color: transparent;
+}
+
+.client-subscription-domain {
+ position: absolute;
+ top: 50%;
+ left: 42px;
+ right: 42px;
+ overflow: hidden;
+ color: var(--client-text);
+ font-size: 12px;
+ text-align: center;
+ text-overflow: ellipsis;
+ transform: translateY(-50%);
+ white-space: nowrap;
+ pointer-events: none;
+}
+
.client-subscription-edit button {
position: absolute;
top: 4px;
@@ -1076,11 +1130,12 @@ code, .mono {
letter-spacing: -0.055em;
opacity: 0.88;
text-shadow: 0 0 20px oklch(0.62 0.05 151 / 0.24);
- transition: opacity 300ms cubic-bezier(0.16, 1, 0.3, 1), text-shadow 300ms cubic-bezier(0.16, 1, 0.3, 1);
+ transition: opacity 300ms cubic-bezier(0.16, 1, 0.3, 1), text-shadow 300ms cubic-bezier(0.16, 1, 0.3, 1), transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-subscription-summary:hover strong {
opacity: 1;
+ transform: translateY(-2px);
text-shadow: 0 0 28px oklch(0.7 0.08 151 / 0.42);
}
@@ -1090,13 +1145,15 @@ code, .mono {
.client-server-grid {
display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
+ grid-template-columns: 1fr;
gap: 8px;
+ width: min(100%, 210px);
+ margin-inline: auto;
}
.client-server {
min-width: 0;
- min-height: 56px;
+ min-height: 50px;
display: flex;
flex-direction: column;
justify-content: center;
@@ -1113,9 +1170,13 @@ code, .mono {
}
.client-server:hover:not(:disabled) {
- transform: none;
+ transform: translateY(-2px);
background: transparent;
- opacity: 0.82;
+ opacity: 1;
+}
+
+.client-server:active:not(:disabled) {
+ transform: translateY(0) scale(0.97);
}
.client-server.is-selected {
@@ -1157,52 +1218,64 @@ code, .mono {
.client-proxies {
display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 24px;
- margin-top: 12px;
+ justify-items: center;
+ gap: 5px;
+ width: 220px;
+ margin-top: 6px;
}
-.client-proxies button {
- min-width: 0;
- display: grid;
- gap: 4px;
- padding: 6px 2px;
- border: 0;
- background: transparent;
- color: var(--client-text);
- text-align: left;
- cursor: pointer;
- opacity: 0.76;
- transition: opacity 180ms ease, transform 180ms ease;
-}
-
-.client-proxies button:hover {
- transform: translateY(-1px);
- opacity: 1;
-}
-
-.client-proxies button.is-copied {
- opacity: 1;
-}
-
-.client-proxies span {
+.client-proxy-label {
color: var(--client-muted);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
+ text-transform: uppercase;
}
-.client-proxies strong {
- overflow: hidden;
+.client-proxy-address {
color: var(--client-text);
font-size: 12px;
font-weight: 500;
- text-overflow: ellipsis;
text-shadow: 0 0 12px oklch(0.62 0.05 151 / 0.12);
- white-space: nowrap;
}
-.client-proxies button.is-copied span {
+.client-proxies > div {
+ display: flex;
+ gap: 8px;
+ margin-top: 7px;
+}
+
+.client-proxies button {
+ position: relative;
+ width: 86px;
+ padding: 6px 10px;
+ border: 0;
+ background: transparent;
+ color: var(--client-muted);
+ font: 700 9px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
+ letter-spacing: 0.06em;
+ cursor: pointer;
+ opacity: 0.76;
+ transition: color 220ms ease, opacity 220ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.client-copy-check {
+ position: absolute;
+ right: 10px;
+}
+
+.client-proxies button:hover {
+ color: var(--client-text);
+ opacity: 1;
+ transform: translateY(-2px);
+}
+
+.client-proxies button:active {
+ transform: translateY(0) scale(0.98);
+}
+
+.client-proxies button.is-copied {
+ opacity: 1;
color: var(--client-accent);
}
@@ -1231,21 +1304,23 @@ code, .mono {
}
.client-panel {
+ position: static;
+ grid-template-columns: 1fr;
gap: 26px;
+ min-height: 0;
padding: 20px 6px;
}
- .client-server-grid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
+ .client-form {
+ position: static;
+ width: 100%;
}
- .client-proxies {
- grid-template-columns: 1fr;
- }
}
@media (prefers-reduced-motion: reduce) {
.client-power,
+ .client-power svg,
.client-server,
.client-proxies button,
.client-subscription-edit,
diff --git a/src/web/utils/clientControls.js b/src/web/utils/clientControls.js
index 662e1ef..0a5f385 100644
--- a/src/web/utils/clientControls.js
+++ b/src/web/utils/clientControls.js
@@ -20,7 +20,12 @@ export function formatConnectionDuration(startedAt, now = Date.now()) {
}
export function subscriptionDomain(subscriptionHost) {
- return String(subscriptionHost || '').split('/')[0];
+ const value = String(subscriptionHost || '');
+ try {
+ return new URL(value).host;
+ } catch {
+ return value.split('/')[0];
+ }
}
export function localProxyUrls(port = 8082) {