From 396c5d19172811d35e05f505c092bd6033117d71 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Tue, 11 Aug 2026 08:10:15 +0300 Subject: [PATCH] Improve server picker scrolling and subscription input layout --- src/shared/versions.ts | 4 +-- src/web/features/servers/ServerPicker.tsx | 9 ++---- .../subscription/SubscriptionFeature.tsx | 22 +++++++-------- src/web/styles/features/servers.css | 19 ++++++------- src/web/styles/features/subscription.css | 18 +++--------- test/web/responsive-layout-contract.test.js | 5 ++-- test/web/server-picker.test.js | 7 +++-- test/web/style-boundaries.test.js | 28 +++++++++---------- .../web/subscription-feature-contract.test.js | 2 ++ 9 files changed, 51 insertions(+), 63 deletions(-) diff --git a/src/shared/versions.ts b/src/shared/versions.ts index b19d110..1a5627f 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.23.6', - gatewayClient: '0.24.6', + macClient: '0.23.7', + gatewayClient: '0.24.7', gatewayBackend: '0.24.0', }); diff --git a/src/web/features/servers/ServerPicker.tsx b/src/web/features/servers/ServerPicker.tsx index bbe16a3..87d3a5c 100644 --- a/src/web/features/servers/ServerPicker.tsx +++ b/src/web/features/servers/ServerPicker.tsx @@ -22,7 +22,7 @@ type PickerServer = HarborServer & { const FAVORITES_KEY = 'harbor-server-favorites'; const RECENT_KEY = 'harbor-server-recent'; const AUTO_KEY = 'harbor-server-auto'; -const SIMPLE_SERVER_LIMIT = 5; +const INLINE_SERVER_ROWS = 8; interface PingResult { id?: string; @@ -314,9 +314,9 @@ export function ServerPicker({ const simpleServers = [ ...(selected && selected.id !== anchorServerId ? [selected] : []), ...servers.filter(({ id }) => id !== selectedServerId && id !== anchorServerId), - ].slice(0, SIMPLE_SERVER_LIMIT); + ]; - return
+ return
{prompt && Выберите сервер}
Список серверов @@ -352,9 +352,6 @@ export function ServerPicker({ onSelect={select} />)}
- {servers.length > simpleServers.length &&

- Ещё {servers.length - simpleServers.length} — доступны через поиск -

} diff --git a/src/web/features/subscription/SubscriptionFeature.tsx b/src/web/features/subscription/SubscriptionFeature.tsx index 52cc222..3d47009 100644 --- a/src/web/features/subscription/SubscriptionFeature.tsx +++ b/src/web/features/subscription/SubscriptionFeature.tsx @@ -338,18 +338,16 @@ function AddProfileForm({ feature }: { feature: SubscriptionFeatureController }) feature.addProfile(); }} > - + feature.setUrl(event.target.value)} + /> {message &&
{message}
}
{feature.profiles.length > 0 && } diff --git a/src/web/styles/features/servers.css b/src/web/styles/features/servers.css index 5746063..0d76160 100644 --- a/src/web/styles/features/servers.css +++ b/src/web/styles/features/servers.css @@ -348,13 +348,6 @@ transition-delay: 210ms; } -.client-server-overflow-note { - margin: 10px 0 0; - color: var(--client-muted); - font-size: 9px; - text-align: center; -} - .client-server-tools input { width: 100%; height: 36px; @@ -450,9 +443,15 @@ } .client-server-mode-panel.is-simple .client-server-scroll { - max-height: none; - overflow: visible; - padding-right: 0; + max-height: 352px; + overflow-y: auto; + padding-right: 4px; + scrollbar-width: thin; +} + +.client-server-mode-panel.is-simple .client-server-scroll::-webkit-scrollbar { + display: block; + width: 4px; } .client-server-scroll.is-leaving { diff --git a/src/web/styles/features/subscription.css b/src/web/styles/features/subscription.css index 49de525..9433190 100644 --- a/src/web/styles/features/subscription.css +++ b/src/web/styles/features/subscription.css @@ -328,7 +328,7 @@ } .client-subscription-sheet > .client-profile-add { - margin-top: 8px; + margin-top: 10px; animation: client-profile-add-open 420ms cubic-bezier(0.16, 1, 0.3, 1) both; } @@ -389,23 +389,13 @@ .client-profile-add { display: grid; - gap: 10px; - padding: 10px 0 4px; -} - -.client-profile-add label { - display: grid; - gap: 6px; - color: var(--client-muted); - font-size: 8px; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; + gap: 8px; + padding: 0 0 4px; } .client-profile-add input { min-width: 0; - height: 40px; + height: 36px; padding: 0 2px; border: 0; border-bottom: 1px solid color-mix(in oklch, var(--client-border) 72%, transparent); diff --git a/test/web/responsive-layout-contract.test.js b/test/web/responsive-layout-contract.test.js index be92495..3220e0a 100644 --- a/test/web/responsive-layout-contract.test.js +++ b/test/web/responsive-layout-contract.test.js @@ -75,8 +75,9 @@ test('server rows scroll inside the subscription drawer without moving the main assert.match(scroll, /overflow-y:\s*auto/); assert.match(scroll, /scrollbar-width:\s*none/); assert.match(styles, /\.client-server-scroll::-webkit-scrollbar\s*\{[\s\S]*display:\s*none/); - assert.match(simpleScroll, /max-height:\s*none/); - assert.match(simpleScroll, /overflow:\s*visible/); + assert.match(simpleScroll, /max-height:\s*352px/); + assert.match(simpleScroll, /overflow-y:\s*auto/); + assert.match(simpleScroll, /scrollbar-width:\s*thin/); assert.match(grid, /width:\s*min\(100%, 220px\)/); assert.match(rule('.client-drawer'), /overflow-y:\s*auto/); assert.match(subscription, /client-profile-list[\s\S]*feature\.profiles\.map\(\(profile\) => \{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(picker, /is-scalable\$\{simpleServers\.length <= INLINE_SERVER_ROWS \? ' 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,8 +129,9 @@ test('server picker starts simple and reveals advanced controls on demand', () = assert.match(picker, /client-server-mode-panel is-advanced/); assert.match(picker, /inert={advanced \? true : undefined}/); assert.match(picker, /inert={!advanced \? true : undefined}/); - assert.match(picker, /const SIMPLE_SERVER_LIMIT = 5/); - assert.match(picker, /\.slice\(0, SIMPLE_SERVER_LIMIT\)/); + assert.match(picker, /const INLINE_SERVER_ROWS = 8/); + assert.doesNotMatch(picker, /simpleServers[\s\S]*\.slice\(0,/); + assert.doesNotMatch(picker, /client-server-overflow-note|доступны через поиск/); assert.match(picker, /selected && selected\.id !== anchorServerId && crypto.createHash('sha256').update(value).digest('hex'); const acceptedLedger = { counts: { - cascadeEdges: 820, + cascadeEdges: 819, customProperties: 31, - declarations: 2983, + declarations: 2975, important: 0, keyframes: 56, media: 13, - rules: 920, - variableReferences: 330, + rules: 919, + variableReferences: 328, }, hashes: { - cascadeEdges: '1590610791a755c9ec3c64da5b56da82764c9906344819fae414bf871bab1bad', + cascadeEdges: '1a3882545e488b80ac5454cc8fa236fd242d15eb8a27ac3371876af866b6c6e8', customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545', - declarations: '0bde16ab93623a2546daf42e83f037dc00d65dbf08da5851bf433d22697338b8', + declarations: '3972e7fd7da24d872a16cfa9dad9e9355861f489708fd9643a8610422e712825', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d', keyframes: '39b6fc9c9ab451d63bb77afd90caf63d2d74a30cb351a8f86503de380d76a936', - ruleDeclarationSequences: '883a3996d9d4ccd4ab6124237b6e0c00f17505dc8ff7314a8c0069fa828a2475', - selectors: 'ec10ef7ce8f812c52c7ff6b6952ae66f5bbeebbc61e582ceddcb54c7112bbf2b', - variableReferences: '8cb18a63664f73e6186e1730d3cd97c6e9f11bbe6b0f5cf04900948494a71f78', - witnesses: '204a3ad7cb5b3681ea78a2e4ef1265515c33ca97ebfe560c082d51303cd3bedb', + ruleDeclarationSequences: '99469fb4eeb3af72a12cc4365f135e988ab95ebcf194572779d62e5185778d20', + selectors: 'b2564f9e6c078233b388e84680181cbd5f21d55e963907990b237b14c5bc250f', + variableReferences: 'c8348faf26b2174f02b9760aaa5812e22e02a72da7902fb5897f0c23e7f4e939', + witnesses: '3c44c91b2cc2a03049b328b0f7a974b1c189c66d2a778dbb386601eed82994e8', }, }; @@ -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, 735); + assert.equal(witnesses.length, 730); assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0); const ledger = createStyleLedger(readStyleSource(root), { witnesses }); assert.deepEqual(ledger.counts, acceptedLedger.counts); @@ -306,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-DgGVjCSB.css']); + assert.deepEqual(assets, ['index-kNiIGd5E.css']); const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0])); - assert.equal(built.byteLength, 112427); - assert.equal(sha256(built), '70e5b945a85ac965b31f43c25cf5766a3860453090678c44fbcdffa9951d624e'); + assert.equal(built.byteLength, 112296); + assert.equal(sha256(built), 'cbda70aff44215a69dc7c87ceaa4166ae573131bc53aaea7ddd935950b2c983f'); }); diff --git a/test/web/subscription-feature-contract.test.js b/test/web/subscription-feature-contract.test.js index 340521b..970629d 100644 --- a/test/web/subscription-feature-contract.test.js +++ b/test/web/subscription-feature-contract.test.js @@ -55,6 +55,8 @@ test('local validation, scoped refresh and drawer focus remain feature-owned', ( assert.match(feature, /feature\.adding \|\| feature\.addClosing/); assert.doesNotMatch(feature, /addInvokerRef/); assert.match(feature, /className="client-profile-delete"[\s\S]*client-profile-delete-lid/); + assert.match(feature, /placeholder="https:\/\/…"[\s\S]*aria-label="Ссылка подписки"/); + assert.doesNotMatch(feature, /Ссылка подписки<\/span>/); assert.doesNotMatch(feature, /cancelRename|client-profile-menu|onRename/); });