diff --git a/src/shared/versions.ts b/src/shared/versions.ts index 05b2efa..ce8a389 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -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', }); diff --git a/src/web/features/servers/ServerPicker.tsx b/src/web/features/servers/ServerPicker.tsx index a239d4f..2b9f14f 100644 --- a/src/web/features/servers/ServerPicker.tsx +++ b/src/web/features/servers/ServerPicker.tsx @@ -138,8 +138,9 @@ function ServerRow({ onClick={() => onSelect(server.id)} > {server.label} + {(server.city || server.country) && {[server.city, server.country].filter(Boolean).join(' · ')}} - {(ping || onFavorite) &&
+
{onFavorite && } -
} +
; } @@ -308,7 +309,7 @@ export function ServerPicker({ ...servers.filter(({ id }) => id !== selectedServerId), ].slice(0, SIMPLE_SERVER_LIMIT); - return
+ return
{prompt && Выберите сервер}
Список серверов diff --git a/src/web/features/subscription/SubscriptionFeature.tsx b/src/web/features/subscription/SubscriptionFeature.tsx index 0e13f1b..097543b 100644 --- a/src/web/features/subscription/SubscriptionFeature.tsx +++ b/src/web/features/subscription/SubscriptionFeature.tsx @@ -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
- + {canActivate ? : } } {!profile.desiredServerId &&

Выберите сервер этой подписки

} {renderServerPicker(profile, { disabled: controlsBlocked, diff --git a/src/web/styles/features/servers.css b/src/web/styles/features/servers.css index d2dcbcf..ea3afb7 100644 --- a/src/web/styles/features/servers.css +++ b/src/web/styles/features/servers.css @@ -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); +} diff --git a/src/web/styles/features/subscription.css b/src/web/styles/features/subscription.css index de9b4fa..5919ac1 100644 --- a/src/web/styles/features/subscription.css +++ b/src/web/styles/features/subscription.css @@ -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, diff --git a/src/web/styles/layout.css b/src/web/styles/layout.css index 4ed0f52..e7a1d18 100644 --- a/src/web/styles/layout.css +++ b/src/web/styles/layout.css @@ -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; } diff --git a/src/web/styles/primitives.css b/src/web/styles/primitives.css index 2a9ddf9..00ff962 100644 --- a/src/web/styles/primitives.css +++ b/src/web/styles/primitives.css @@ -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; diff --git a/test/web/server-picker.test.js b/test/web/server-picker.test.js index 844667b..39f8957 100644 --- a/test/web/server-picker.test.js +++ b/test/web/server-picker.test.js @@ -98,6 +98,8 @@ test('manual ping uses plain language and keeps results beside server names', () assert.match(picker, //); assert.doesNotMatch(picker, /TCP|Не проверен/); assert.match(picker, /\{server\.label\}<\/strong>[\s\S]*?<\/button>[\s\S]*?client-server-meta/); + assert.match(picker, /\[server\.city, server\.country\]\.filter\(Boolean\)\.join\(' · '\)/); + assert.match(picker, /is-scalable\$\{servers\.length <= SIMPLE_SERVER_LIMIT \? ' is-short' : ''\}/); assert.match(styles, /\.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 120px minmax\(0, 1fr\);/); assert.match(styles, /\.client-server-row \.client-server \{[\s\S]*?width: 120px;/); assert.match(styles, /\.client-server-health \{[\s\S]*?font-size: 9px;[\s\S]*?font-variant-numeric: tabular-nums;/); @@ -129,4 +131,7 @@ test('server picker starts simple and reveals advanced controls on demand', () = assert.match(picker, /const SIMPLE_SERVER_LIMIT = 5/); assert.match(picker, /\.slice\(0, SIMPLE_SERVER_LIMIT\)/); assert.match(picker, /\{selected && crypto.createHash('sha256').update(value).digest('hex'); const acceptedLedger = { counts: { - cascadeEdges: 780, + cascadeEdges: 793, customProperties: 31, - declarations: 2912, + declarations: 2968, important: 0, keyframes: 49, - media: 12, - rules: 889, - variableReferences: 330, + media: 13, + rules: 905, + variableReferences: 340, }, hashes: { - cascadeEdges: '12a578504877454d14c734b8ce608766d061d3f8f809a5abf13513a645d8b6aa', + cascadeEdges: '78b7ac8a4e6e39376dd811c0b66b7562b8562c800a332be259478e391996278b', customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545', - declarations: '297c4387ebbccec796e80474c4d040eba20941d316a77a29d1e72c31d19c02d6', + declarations: '9935ad971bc1804c0f6bf50f26b6057f1f75fa66321af90cae8f8132a3275fac', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', - duplicateSelectors: 'bea7ed28b2e90fbaa3ae50aaee2346fc9063996fc9b7c585a3a47693a055b783', + duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d', keyframes: 'bde9c628dc86cb8a4299d5397eb7a81c5fbc9481136a073130991d3182d0799d', - ruleDeclarationSequences: '7386a7be7efa9f3527d167b885fee0f208f236f6947ba3878a0f4acabd31e7a8', - selectors: '845bd57c761ad3767b5a908fa004623d61dd4cdb838616ae0320093ffa1718eb', - variableReferences: '6755baede1580eefde410ea7237ede30af629fbbabd5bc13fc83012a7e4132b2', - witnesses: 'e5869e1f591f6a559ad9aa1942edcbb67b84ebe5ca6d22b4f6108dcba584a9f5', + ruleDeclarationSequences: 'a62dc2ad887bd5fdba773e462e137eda397fc13b7b151bfc2ca16352d325ec60', + selectors: '529b528153c3f4e0c565aa80ed9fefea52a8c02c4ae10a5e807a961eb81ebefd', + variableReferences: '0189ffef214066db38a0ed0b8bfba18150353da5a09589ef1f606e5651cd25f7', + witnesses: '770af3da2a817c9bc2bd593aeb1c21fe81b36fd7f86bf24e47d8ae844719feaa', }, }; @@ -110,7 +110,7 @@ test('tokens, shared primitives, and feature styles have one explicit owner', () test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => { const witnesses = readStyleWitnesses(root); - assert.equal(witnesses.length, 740); + assert.equal(witnesses.length, 745); assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0); const ledger = createStyleLedger(readStyleSource(root), { witnesses }); assert.deepEqual(ledger.counts, acceptedLedger.counts); @@ -123,7 +123,6 @@ test('every live production selector has an expanded DOM witness', () => { '.client-diagnostics-section-title button', '.client-diagnostics-section-title button:disabled', '.client-diagnostics-section-title button:focus-visible', - '.client-server small', ]); }); @@ -307,8 +306,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1); const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css')); - assert.deepEqual(assets, ['index-COUqegc1.css']); + assert.deepEqual(assets, ['index-Bl9ybRZ0.css']); const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0])); - assert.equal(built.byteLength, 108951); - assert.equal(sha256(built), '059653e70cd17fb84953e19b8280dcb9b926760c1781fb19382820434f9d1f01'); + assert.equal(built.byteLength, 111207); + assert.equal(sha256(built), '236deba70723e5bfe8e49fc0514f2c8b8fc0c1354664a37d5b37e6b22ac0c156'); });