Refine client overview proxy and subscription display

This commit is contained in:
2026-07-11 05:42:22 +03:00
parent d3b7f0d613
commit 6bc7840fb1
3 changed files with 165 additions and 75 deletions

View File

@@ -133,6 +133,30 @@ export function ClientOverviewPage({
<p>{canStart ? 'Нажмите, чтобы подключиться' : 'Добавьте ссылку и выберите сервер'}</p>
)}
</div>
<section className="client-proxies" aria-label="Локальный прокси">
<span className="client-proxy-label">Адрес</span>
<strong className="client-proxy-address">
{proxyUrls.http.replace(/^https?:\/\//, '')}
</strong>
<div>
{[
['socks5', 'SOCKS5'],
['http', 'HTTP'],
].map(([kind, label]) => (
<button
className={copiedProxy === kind ? 'is-copied' : ''}
type="button"
key={kind}
aria-label={`Скопировать ${label}: ${proxyUrls[kind]}`}
onClick={() => copyProxy(kind)}
>
<span>{label}</span>
{copiedProxy === kind && <span className="client-copy-check"></span>}
</button>
))}
</div>
</section>
</section>
<div className="client-form">
@@ -151,6 +175,7 @@ export function ClientOverviewPage({
<form
className="client-subscription-edit"
autoComplete="off"
aria-hidden={!editingSubscription}
inert={!editingSubscription ? true : undefined}
onSubmit={submitSubscription}
@@ -160,10 +185,11 @@ export function ClientOverviewPage({
id="subscription-url"
type="url"
inputMode="url"
autoComplete="url"
autoComplete="off"
tabIndex={editingSubscription ? 0 : -1}
aria-label="Ссылка подписки"
placeholder="Вставьте ссылку подписки"
className={subscriptionUrl ? 'has-value' : ''}
value={subscriptionUrl}
onChange={(event) => setSubscriptionUrl(event.target.value)}
onKeyDown={(event) => {
@@ -173,6 +199,11 @@ export function ClientOverviewPage({
}
}}
/>
{subscriptionUrl && (
<span className="client-subscription-domain">
{subscriptionDomain(subscriptionUrl)}
</span>
)}
{subscriptionUrl.trim() && (
<button type="submit" aria-label="Сохранить подписку" disabled={busy}></button>
)}
@@ -210,27 +241,6 @@ export function ClientOverviewPage({
</div>
</main>
<section className="client-proxies" aria-label="Локальный прокси">
{[
['socks5', 'SOCKS5'],
['http', 'HTTP'],
].map(([kind, label]) => (
<button
className={copiedProxy === kind ? 'is-copied' : ''}
type="button"
key={kind}
aria-label={`Скопировать ${label}: ${proxyUrls[kind]}`}
onClick={() => copyProxy(kind)}
>
<span>
{copiedProxy === kind
? 'Скопировано ✓'
: copiedProxy === 'error' ? 'Ошибка копирования' : label}
</span>
<strong>{proxyUrls[kind]}</strong>
</button>
))}
</section>
</div>
);
}

View File

@@ -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,

View File

@@ -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) {