Improve server picker layout and profile activation flow
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
export const HARBOR_VERSIONS = Object.freeze({
|
||||
macClient: '0.23.0',
|
||||
gatewayClient: '0.24.0',
|
||||
macClient: '0.23.1',
|
||||
gatewayClient: '0.24.1',
|
||||
gatewayBackend: '0.24.0',
|
||||
});
|
||||
|
||||
|
||||
@@ -138,8 +138,9 @@ function ServerRow({
|
||||
onClick={() => onSelect(server.id)}
|
||||
>
|
||||
<strong>{server.label}</strong>
|
||||
{(server.city || server.country) && <small>{[server.city, server.country].filter(Boolean).join(' · ')}</small>}
|
||||
</button>
|
||||
{(ping || onFavorite) && <div className="client-server-meta">
|
||||
<div className="client-server-meta">
|
||||
<ServerHealth ping={ping} />
|
||||
{onFavorite && <button
|
||||
className={`client-server-favorite${favorite ? ' is-active' : ''}`}
|
||||
@@ -148,7 +149,7 @@ function ServerRow({
|
||||
aria-label={`${favorite ? 'Убрать из избранного' : 'Добавить в избранное'}: ${server.label}`}
|
||||
onClick={() => onFavorite(server.id)}
|
||||
>★</button>}
|
||||
</div>}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -308,7 +309,7 @@ export function ServerPicker({
|
||||
...servers.filter(({ id }) => id !== selectedServerId),
|
||||
].slice(0, SIMPLE_SERVER_LIMIT);
|
||||
|
||||
return <section className="client-servers is-scalable" aria-label="Выберите сервер">
|
||||
return <section className={`client-servers is-scalable${servers.length <= SIMPLE_SERVER_LIMIT ? ' is-short' : ''}`} aria-label="Выберите сервер">
|
||||
{prompt && <span className="client-server-prompt">Выберите сервер</span>}
|
||||
<div className="client-server-toolbar">
|
||||
<span className="client-server-toolbar-title">Список серверов</span>
|
||||
|
||||
@@ -408,6 +408,12 @@ function ProfileGroup({
|
||||
|| feature.operations.profileRefresh?.target === profile.id;
|
||||
const controlsBlocked = operationBlocked(feature.operations, 'profileSelect');
|
||||
const selectedServer = profileServer(profile, profile.desiredServerId);
|
||||
const canActivate = !applied && feature.selection.desiredProfileId !== profile.id;
|
||||
const localStatus = profileError?.message || (profile.subscription.status === 'stale'
|
||||
? `Последнее обновление не удалось. Данные от ${profile.subscription.fetchedAt
|
||||
? new Date(profile.subscription.fetchedAt).toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' })
|
||||
: 'предыдущей загрузки'}.`
|
||||
: '');
|
||||
return <section className={`client-profile-group${expanded ? ' is-expanded' : ''}${applied ? ' is-active' : ''}`}>
|
||||
<div className="client-profile-header">
|
||||
<button
|
||||
@@ -426,7 +432,12 @@ function ProfileGroup({
|
||||
<small>{subscriptionDomain(profile.subscription.host)}</small>
|
||||
</span>
|
||||
</button>
|
||||
<ProfileUsage profile={profile} />
|
||||
{canActivate ? <button
|
||||
className="client-profile-activate"
|
||||
type="button"
|
||||
disabled={feature.activateBlocked || !selectedServer}
|
||||
onClick={() => feature.onActivate(profile.id)}
|
||||
>Сделать активной</button> : <ProfileUsage profile={profile} />}
|
||||
<button
|
||||
className={`client-profile-refresh${refreshing ? ' is-refreshing' : ''}`}
|
||||
type="button"
|
||||
@@ -472,13 +483,7 @@ function ProfileGroup({
|
||||
<span role="status">{feature.renameDuplicate ? 'Такое имя уже используется.' : ''}</span>
|
||||
</form>}
|
||||
|
||||
<div className="client-profile-local-status" role="status">
|
||||
{profileError?.message || (profile.subscription.status === 'stale'
|
||||
? `Последнее обновление не удалось. Данные от ${profile.subscription.fetchedAt
|
||||
? new Date(profile.subscription.fetchedAt).toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' })
|
||||
: 'предыдущей загрузки'}.`
|
||||
: '\u00a0')}
|
||||
</div>
|
||||
{localStatus && <div className="client-profile-local-status" role="status">{localStatus}</div>}
|
||||
|
||||
{visited && <div
|
||||
id={`client-profile-${profile.id}`}
|
||||
@@ -486,12 +491,6 @@ function ProfileGroup({
|
||||
aria-hidden={!expanded}
|
||||
inert={!expanded ? true : undefined}
|
||||
>
|
||||
{!applied && feature.selection.desiredProfileId !== profile.id && <button
|
||||
className="client-profile-activate"
|
||||
type="button"
|
||||
disabled={feature.activateBlocked || !selectedServer}
|
||||
onClick={() => feature.onActivate(profile.id)}
|
||||
>Сделать активной</button>}
|
||||
{!profile.desiredServerId && <p className="client-profile-server-hint">Выберите сервер этой подписки</p>}
|
||||
{renderServerPicker(profile, {
|
||||
disabled: controlsBlocked,
|
||||
|
||||
@@ -554,3 +554,121 @@
|
||||
.client-server:disabled {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
/* The drawer uses the same picker behavior with a flat, full-width list. */
|
||||
.client-profile-body .client-servers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-mode-panels {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-toolbar {
|
||||
order: 2;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: 40px;
|
||||
margin-top: 6px;
|
||||
border-top: 1px solid var(--client-border);
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-toolbar-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-check {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
justify-self: start;
|
||||
width: auto;
|
||||
gap: 8px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-check::after {
|
||||
content: 'ПРОВЕРИТЬ ПИНГ';
|
||||
font: 600 9px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-mode-toggle {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.client-profile-body .client-servers.is-short .client-server-mode-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-grid,
|
||||
.client-profile-body .client-server-row,
|
||||
.client-profile-body .client-server-auto,
|
||||
.client-profile-body .client-server-row .client-server {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-row {
|
||||
grid-template-columns: minmax(0, 1fr) 64px;
|
||||
min-height: 52px;
|
||||
border-bottom: 1px solid var(--client-border);
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-row.is-selected {
|
||||
background: color-mix(in oklch, var(--client-accent) 7%, transparent);
|
||||
box-shadow: inset 2px 0 var(--client-accent);
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-row .client-server {
|
||||
grid-column: 1;
|
||||
min-height: 51px;
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, 1fr) auto;
|
||||
place-items: center start;
|
||||
gap: 8px;
|
||||
padding: 0 8px;
|
||||
border: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-row .client-server::before {
|
||||
content: '';
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 1px solid var(--client-muted);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-row .client-server.is-selected::before {
|
||||
border: 1px solid var(--client-accent);
|
||||
background: var(--client-accent);
|
||||
box-shadow: inset 0 0 0 2px color-mix(in oklch, var(--client-bg) 88%, transparent);
|
||||
}
|
||||
|
||||
.client-profile-body .client-server strong,
|
||||
.client-profile-body .client-server small {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server small {
|
||||
grid-column: 3;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-meta {
|
||||
grid-column: 2;
|
||||
justify-self: stretch;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.client-profile-body .client-server-meta .client-server-health {
|
||||
width: auto;
|
||||
place-items: center end;
|
||||
padding: 0 10px 0 0;
|
||||
color: var(--client-accent);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,13 @@
|
||||
padding: 32px 30px 76px;
|
||||
}
|
||||
|
||||
@media (min-width: 921px) {
|
||||
.client-subscription-drawer {
|
||||
right: 64px;
|
||||
width: min(480px, calc(100vw - 64px));
|
||||
}
|
||||
}
|
||||
|
||||
.client-profiles-header {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
@@ -115,6 +122,10 @@
|
||||
border-bottom: 1px solid var(--client-border);
|
||||
}
|
||||
|
||||
.client-profile-group.is-active .client-profile-title strong {
|
||||
color: var(--client-accent);
|
||||
}
|
||||
|
||||
.client-profile-header {
|
||||
min-height: 72px;
|
||||
display: grid;
|
||||
@@ -269,7 +280,7 @@
|
||||
|
||||
.client-profile-body {
|
||||
max-height: 3200px;
|
||||
padding: 0 0 20px 18px;
|
||||
padding: 0 0 20px;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
@@ -304,8 +315,7 @@
|
||||
}
|
||||
|
||||
.client-profile-activate {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.client-profile-activate:disabled {
|
||||
@@ -331,6 +341,7 @@
|
||||
|
||||
.client-profile-local-status {
|
||||
padding-left: 18px;
|
||||
padding-bottom: 8px;
|
||||
color: oklch(0.68 0.14 72);
|
||||
}
|
||||
|
||||
@@ -447,18 +458,10 @@
|
||||
padding: 24px 18px 64px;
|
||||
}
|
||||
|
||||
.client-profile-header {
|
||||
grid-template-columns: minmax(0, 1fr) 36px 36px;
|
||||
}
|
||||
|
||||
.client-profile-usage {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.client-profile-body {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.client-profile-disclosure,
|
||||
.client-profile-refresh,
|
||||
.client-profile-menu-toggle,
|
||||
|
||||
@@ -742,10 +742,6 @@
|
||||
padding: 40px 58px 60px 18px;
|
||||
}
|
||||
|
||||
.client-subscription-sheet {
|
||||
padding: 70px 58px 60px 18px;
|
||||
}
|
||||
|
||||
.client-local-rules-sheet {
|
||||
padding: 40px 58px 60px 18px;
|
||||
}
|
||||
|
||||
@@ -494,12 +494,6 @@
|
||||
width: min(480px, 100vw);
|
||||
}
|
||||
|
||||
.client-subscription-sheet {
|
||||
min-height: 100%;
|
||||
align-content: start;
|
||||
padding: 82px 72px 72px 42px;
|
||||
}
|
||||
|
||||
.client-confirmation-popup {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
Reference in New Issue
Block a user