diff --git a/src/shared/versions.ts b/src/shared/versions.ts index b3b8bcf..ab56394 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.23.3', - gatewayClient: '0.24.3', + macClient: '0.23.4', + gatewayClient: '0.24.4', gatewayBackend: '0.24.0', }); diff --git a/src/web/features/subscription/SubscriptionFeature.tsx b/src/web/features/subscription/SubscriptionFeature.tsx index 90fafc1..a496b35 100644 --- a/src/web/features/subscription/SubscriptionFeature.tsx +++ b/src/web/features/subscription/SubscriptionFeature.tsx @@ -46,18 +46,6 @@ interface SubscriptionFeatureOptions { onDismissError: () => void; } -function operationText(key: OperationKey) { - return ({ - profileAdd: 'Добавляем подписку…', - profileRename: 'Переименовываем подписку…', - profileSelect: 'Сохраняем выбор сервера…', - profileActivate: 'Переключаем подписку…', - profileRefresh: 'Обновляем подписку…', - profileDelete: 'Удаляем подписку…', - serverApply: 'Применяем сервер…', - } as Partial>)[key] || ''; -} - function profileServer(profile: ProfileSnapshot | undefined, serverId: string) { return profile?.servers.find((server) => server.id === serverId) || null; } @@ -99,11 +87,6 @@ export function useSubscriptionFeature({ : isSubscriptionUrlValid(normalizedUrl) ? 'valid' : 'invalid'; const deleteProfile = profiles.find((profile) => profile.id === deleteId) || null; const deleteStopsVpn = connected && selection.appliedProfileId === deleteId; - const activeOperation = (Object.entries(operations) as Array<[ - OperationKey, - OperationRegistrySnapshot[OperationKey], - ]>).find(([, operation]) => operation?.status === 'running'); - useEffect(() => { const ids = new Set(profiles.map((profile) => profile.id)); setExpanded((current) => current.filter((id) => ids.has(id))); @@ -220,7 +203,6 @@ export function useSubscriptionFeature({ addError, deleteProfile, deleteStopsVpn, - activeOperation, refreshingIds, revealVersions, panelRef, @@ -301,9 +283,7 @@ function AddProfileForm({ feature }: { feature: SubscriptionFeatureController }) onChange={(event) => feature.setUrl(event.target.value)} /> -
- {message || '\u00a0'} -
+ {message &&
{message}
}
{feature.profiles.length > 0 && } -
- {feature.activeOperation ? operationText(feature.activeOperation[0]) : '\u00a0'} -
{statusSlot}
{feature.profiles.map((profile) => span { @@ -172,10 +135,7 @@ } .client-profile-title em { - padding: 3px 6px; - border: 1px solid color-mix(in oklch, var(--client-accent) 30%, transparent); - border-radius: 3px; - background: color-mix(in oklch, var(--client-accent) 8%, transparent); + padding: 0; color: var(--client-accent); font-size: 7px; font-style: normal; @@ -225,11 +185,19 @@ .client-profile-refresh:hover:not(:disabled), .client-profile-refresh:focus-visible, -.client-profile-delete:hover:not(:disabled), -.client-profile-delete:focus-visible { +.client-profile-delete:hover:not(:disabled) { color: var(--client-accent); } +.client-profile-delete, +.client-profile-delete:focus-visible { + color: oklch(0.62 0.16 28); +} + +.client-profile-delete:hover:not(:disabled) { + color: oklch(0.7 0.18 28); +} + .client-profile-refresh:hover:not(:disabled) svg, .client-profile-refresh:focus-visible:not(.is-refreshing) svg { transform: rotate(90deg); @@ -255,21 +223,11 @@ } .client-profile-body { - max-height: 3200px; padding: 0 0 4px; - overflow: hidden; - opacity: 1; - visibility: visible; - transition: max-height 560ms cubic-bezier(0.16, 1, 0.3, 1), opacity 260ms ease, padding 420ms ease, visibility 0s; } .client-profile-body[aria-hidden='true'] { - max-height: 0; - padding-top: 0; - padding-bottom: 0; - opacity: 0; - visibility: hidden; - transition: max-height 420ms cubic-bezier(0.16, 1, 0.3, 1), opacity 180ms ease, padding 320ms ease, visibility 0s 420ms; + display: none; } .client-profile-body .client-servers { @@ -279,14 +237,13 @@ .client-profile-selected-server { width: 100%; - min-height: 52px; + min-height: 44px; display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 0 8px; border: 0; - border-top: 1px solid var(--client-border); background: transparent; color: var(--client-text); font: inherit; @@ -319,7 +276,7 @@ .client-profile-selected-server:hover, .client-profile-selected-server:focus-visible { - background: color-mix(in oklch, var(--client-accent) 5%, transparent); + background: color-mix(in oklch, var(--client-accent) 2%, transparent); } .client-profile-selected-server:focus-visible { @@ -341,17 +298,7 @@ } .client-subscription-sheet > .client-profile-add { - margin-top: 12px; - border-bottom: 0; - animation: client-profile-add-in 520ms cubic-bezier(0.16, 1, 0.3, 1) both; -} - -@keyframes client-profile-add-in { - from { - opacity: 0; - filter: blur(5px); - transform: translateY(8px); - } + margin-top: 8px; } .client-profile-activate { @@ -365,13 +312,12 @@ .client-profile-add-trigger { width: 100%; - margin-top: 24px; + margin-top: 10px; text-align: left; } .client-profile-server-hint, -.client-profile-local-status, -.client-profile-form-status { +.client-profile-local-status { min-height: 18px; margin: 0; color: var(--client-muted); @@ -388,8 +334,7 @@ .client-profile-add { display: grid; gap: 10px; - padding: 16px 0 20px; - border-bottom: 1px solid var(--client-border); + padding: 10px 0 4px; } .client-profile-add label { @@ -405,8 +350,9 @@ .client-profile-add input { min-width: 0; height: 40px; - padding: 0 10px; - border: 1px solid var(--client-border); + padding: 0 2px; + border: 0; + border-bottom: 1px solid color-mix(in oklch, var(--client-border) 72%, transparent); border-radius: 0; outline: 0; background: transparent; @@ -415,11 +361,11 @@ } .client-profile-add input:focus { - border: 1px solid var(--client-accent); + border-bottom-color: var(--client-accent); } .client-profile-add input[aria-invalid='true'] { - border: 1px solid oklch(0.68 0.15 28); + border-bottom-color: oklch(0.68 0.15 28); } .client-profile-form-actions { @@ -487,12 +433,10 @@ @media (prefers-reduced-motion: reduce) { .client-profile-chevron, - .client-profile-body, .client-profile-refresh, .client-profile-refresh svg, .client-profile-delete-lid, - .client-profile-group, - .client-subscription-sheet > .client-profile-add { + .client-profile-group { transition: none; animation: none; } diff --git a/test/web/server-picker.test.js b/test/web/server-picker.test.js index e7c9845..0577ae4 100644 --- a/test/web/server-picker.test.js +++ b/test/web/server-picker.test.js @@ -133,7 +133,7 @@ test('server picker starts simple and reveals advanced controls on demand', () = assert.match(picker, /\.slice\(0, SIMPLE_SERVER_LIMIT\)/); assert.match(picker, /\{selected && crypto.createHash('sha256').update(value).digest('hex'); const acceptedLedger = { counts: { - cascadeEdges: 792, + cascadeEdges: 790, customProperties: 31, - declarations: 2977, + declarations: 2941, important: 0, - keyframes: 51, + keyframes: 49, media: 13, - rules: 910, - variableReferences: 338, + rules: 906, + variableReferences: 328, }, hashes: { - cascadeEdges: 'f83d896540885ddaf09e55551a2f1ba5b587dcebfc285cc305efe80136154d93', + cascadeEdges: '1f4fd4cc495ac8d430f0d3891a7d63db34ee44313b4f865b58de7c587378b261', customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545', - declarations: '14728723e4a95d6031b60eb39c0864da0ccc061e24798091cd093b6354a5d906', + declarations: 'c17ab95f3bccd3fca26b2707f07e5298deda0f6343e80d77dbc5df8e97ef0ecb', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d', - keyframes: '9e87e997d0b776462ac3e495c7ced584871a5d9512226e5008eb84b4d311530a', - ruleDeclarationSequences: 'eac60ada2de39eff87713f71f5ca533cd831ee7d400d27eaa09562c29dfbfadd', - selectors: 'e79f61996bcd2a806828406c0a626e87d60112f1b24264d60b8670f9b8f831af', - variableReferences: '5c56197ab72968fc007f333c0cc58f90b6ec90465d6273bc757b7c77503dd26e', - witnesses: '7066589e7b69a5b3c728ff70430a8d6d3c6c6d562cea91377244c0754e18b8da', + keyframes: 'bde9c628dc86cb8a4299d5397eb7a81c5fbc9481136a073130991d3182d0799d', + ruleDeclarationSequences: 'a0e34d9639e87e77a2a095503196f4472a95ac875e398908f184b64b5bf5feba', + selectors: '7051c489e7dac8bd04f3cdeb44bda8c069793cff4232ea730d40a19545ea0475', + variableReferences: '37d0cec8ffda35b4b3823322c0e21802ad0d3252258deade0e2124bdc6eb6ca0', + witnesses: '1b0e3d6fb35dde7acd7b3597f290c2e8b2f25ca569e74b047457121967affc6f', }, }; @@ -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, 734); 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-pEpt1ReD.css']); + assert.deepEqual(assets, ['index-hHmAphe7.css']); const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0])); - assert.equal(built.byteLength, 111837); - assert.equal(sha256(built), 'f797af224ecae5e0435c7fb165cc158a237d11654c48a8cb287b32b2230e6b0c'); + assert.equal(built.byteLength, 110356); + assert.equal(sha256(built), '67a5df63d5ee0624c0e58f7fc9751c279a0c009c37abc8ef77567892c93da931'); }); diff --git a/test/web/subscription-feature-contract.test.js b/test/web/subscription-feature-contract.test.js index 2bc5495..d61f067 100644 --- a/test/web/subscription-feature-contract.test.js +++ b/test/web/subscription-feature-contract.test.js @@ -87,7 +87,8 @@ test('validation rejection parser preserves structured errors and normalizes non test('feature keeps exact slots, truthy closes and subscription DOM order', () => { assert.match(feature, /const host = new URL\(normalizedUrl\)\.hostname;[\s\S]*if \(!await onAdd\(label, normalizedUrl\)\) return;[\s\S]*resetAdd\(\)/); assert.match(feature, /if \(!await onForget\(deleteProfile\.id, deleteStopsVpn \? 'stop-and-delete' : 'delete'\)\) return;[\s\S]*setDeleteId\(''\)/); - assert.match(feature, /client-profiles-operation[\s\S]*\{statusSlot\}[\s\S]*client-profile-list[\s\S]*feature\.adding[\s\S]*AddProfileForm[\s\S]*client-profile-add-trigger/); + assert.match(feature, /\{statusSlot\}[\s\S]*client-profile-list[\s\S]*feature\.adding[\s\S]*AddProfileForm[\s\S]*client-profile-add-trigger/); + assert.doesNotMatch(feature, /client-profiles-operation|activeOperation|operationText/); assert.doesNotMatch(feature, /client-profiles-current|aria-label="Добавить подписку"/); assert.match(feature, /const drawerOpen = feature\.open && feature\.profiles\.length > 0/); assert.match(feature, /feature\.profiles\.length === 0 &&