diff --git a/src/shared/versions.ts b/src/shared/versions.ts index 492caad..f9ff9bb 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.26.4', - gatewayClient: '0.27.4', + macClient: '0.26.5', + gatewayClient: '0.27.5', gatewayBackend: '0.27.0', }); diff --git a/src/web/features/devices/DevicesFeature.tsx b/src/web/features/devices/DevicesFeature.tsx index a420dc4..460859c 100644 --- a/src/web/features/devices/DevicesFeature.tsx +++ b/src/web/features/devices/DevicesFeature.tsx @@ -249,8 +249,6 @@ export function DevicesToggle({ - - ; } diff --git a/src/web/features/diagnostics/DiagnosticsFeature.tsx b/src/web/features/diagnostics/DiagnosticsFeature.tsx index 044c04a..44b0199 100644 --- a/src/web/features/diagnostics/DiagnosticsFeature.tsx +++ b/src/web/features/diagnostics/DiagnosticsFeature.tsx @@ -61,7 +61,6 @@ export function DiagnosticsToggle({ >

Правила маршрутизации

-

Проверяются сверху вниз. Первое совпадение выбирает маршрут.

{!feature.editable && (

Обновите Harbor, чтобы редактировать правила. @@ -912,14 +912,25 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature; Правила сохранены и начнут работать после запуска Harbor.

)} -
- Как работают правила -
-

Правила проверяются сверху вниз. Первое совпадение выбирает маршрут: щит означает VPN, стрелка — прямое подключение.

-

Правила применяются только к трафику внутри маршрутизации Harbor. Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список.

-

Можно вставить полный URL: Harbor сразу оставит только домен. Путь и параметры HTTPS для маршрутизации недоступны.

+
+ +
+
+
+

Правила проверяются сверху вниз. Первое совпадение выбирает маршрут: щит означает VPN, стрелка — прямое подключение.

+

Правила применяются только к трафику внутри маршрутизации Harbor. Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список.

+

Можно вставить полный URL: Harbor сразу оставит только домен. Путь и параметры HTTPS для маршрутизации недоступны.

+
+
-
+
diff --git a/src/web/features/subscription/SubscriptionFeature.tsx b/src/web/features/subscription/SubscriptionFeature.tsx index 3c866ae..a76de4e 100644 --- a/src/web/features/subscription/SubscriptionFeature.tsx +++ b/src/web/features/subscription/SubscriptionFeature.tsx @@ -322,7 +322,6 @@ export function SubscriptionToggle({ - ; diff --git a/src/web/styles/features/routing.css b/src/web/styles/features/routing.css index 51ec126..5b9bf46 100644 --- a/src/web/styles/features/routing.css +++ b/src/web/styles/features/routing.css @@ -537,8 +537,11 @@ margin-top: 4px; } -.client-local-rules-help summary { +.client-local-rules-help > button { width: fit-content; + padding: 0; + border: 0; + background: transparent; color: var(--client-muted); font: var(--type-label); letter-spacing: var(--type-label-tracking); @@ -547,14 +550,31 @@ transition: color 220ms ease, text-shadow 300ms ease; } -.client-local-rules-help summary:hover, -.client-local-rules-help summary:focus-visible { +.client-local-rules-help > button:hover, +.client-local-rules-help > button:focus-visible { outline: 0; color: var(--client-accent); text-shadow: 0 0 9px color-mix(in oklch, var(--client-accent) 34%, transparent); } -.client-local-rules-help > div { +.client-local-rules-help-reveal { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + transition: grid-template-rows 420ms cubic-bezier(0.16, 1, 0.3, 1), opacity 220ms ease; +} + +.client-local-rules-help.is-open .client-local-rules-help-reveal { + grid-template-rows: 1fr; + opacity: 1; +} + +.client-local-rules-help-content { + min-height: 0; + overflow: hidden; +} + +.client-local-rules-help-copy { max-width: 48ch; display: grid; gap: 8px; @@ -562,10 +582,15 @@ padding: 10px 12px; border-radius: 10px; background: color-mix(in oklch, var(--client-accent) 5%, transparent); - animation: client-local-rules-notice 280ms ease both; + transform: translateY(-6px); + transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1); } -.client-local-rules-help > div p { +.client-local-rules-help.is-open .client-local-rules-help-copy { + transform: translateY(0); +} + +.client-local-rules-help-copy p { max-width: 48ch; margin: 0; color: var(--client-muted); diff --git a/src/web/styles/primitives.css b/src/web/styles/primitives.css index 0dc0589..a718318 100644 --- a/src/web/styles/primitives.css +++ b/src/web/styles/primitives.css @@ -8,7 +8,6 @@ } .client-rail-action { - --client-rail-state-duration: 340ms; position: relative; width: 42px; height: 48px; @@ -19,7 +18,6 @@ background: transparent; color: var(--client-muted); cursor: pointer; - transition: color 240ms ease, transform 440ms cubic-bezier(0.16, 1, 0.3, 1); } .client-rail-action svg { @@ -31,12 +29,6 @@ stroke-linecap: round; stroke-linejoin: round; overflow: visible; - transition: color 240ms ease, filter 360ms ease, transform 180ms cubic-bezier(0.16, 1, 0.3, 1); -} - -.client-rail-action:active:not(:disabled) svg { - transform: scale(0.94); - transition: color 120ms ease, filter 120ms ease, transform 120ms cubic-bezier(0.16, 1, 0.3, 1); } .client-rail-action > span { @@ -58,7 +50,6 @@ .client-rail-action:focus-visible { outline: 0; color: var(--client-accent); - transform: translateX(-3px); } .client-rail-action:hover > span, @@ -74,157 +65,19 @@ filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-accent) 52%, transparent)); } -.client-rail-subscription-back, -.client-rail-subscription-front, -.client-rail-book-page-position, -.client-rail-book-spine, -.client-rail-device-link, -.client-rail-device-bridge, -.client-rail-diagnostics-position, -.client-rail-rule-knob-position { - transform-box: fill-box; - transform-origin: center; - transition: transform var(--client-rail-state-duration) cubic-bezier(0.16, 1, 0.3, 1), opacity 260ms ease, stroke-dasharray var(--client-rail-state-duration) cubic-bezier(0.16, 1, 0.3, 1), stroke-dashoffset var(--client-rail-state-duration) cubic-bezier(0.16, 1, 0.3, 1); -} - -.client-rail-book-page, -.client-rail-diagnostics-glass, -.client-rail-rule-knob { - transform-box: fill-box; - transform-origin: center; -} - -.client-rail-book-page.is-right { - transform-origin: left center; -} - .client-rail-action .client-rail-subscription-card, .client-rail-action .client-rail-rule-knob { fill: var(--client-bg); } -.client-rail-subscription-lines { - opacity: 1; -} - -.client-subscription-toggle.is-open .client-rail-subscription-back { - transform: translate(1.5px, -1px); -} - -.client-subscription-toggle.is-open .client-rail-subscription-front { - transform: translate(-2px, 1px); -} - -.client-instructions-toggle.is-open .client-rail-book-page-position.is-left { - transform: translateX(-1.25px) rotate(-2.5deg); -} - -.client-instructions-toggle.is-open .client-rail-book-page-position.is-right { - transform: translateX(1.25px) rotate(2.5deg); -} - -.client-instructions-toggle.is-open .client-rail-book-spine { - transform: scaleY(1.06); -} - .client-rail-device-link { opacity: 0.68; } -.client-rail-device-bridge { - opacity: 0; - transform: scaleX(0); -} - -.client-rail-subscription-scan, -.client-rail-device-packet, -.client-rail-diagnostics-scan { - stroke-width: 2.4; - opacity: 0; - stroke-dasharray: 0.18 0.82; - stroke-dashoffset: 1; -} - -.client-rail-device-packet { - stroke-dasharray: 0.36 0.64; -} - -.client-devices-toggle.is-open .client-rail-device-link { - opacity: 1; -} - -.client-devices-toggle.is-open .client-rail-device-bridge { - opacity: 1; - transform: scaleX(1); -} - .client-rail-diagnostics-trace { opacity: 0.72; } -.client-diagnostics-toggle.is-open .client-rail-diagnostics-position { - transform: translate(2px, -1px) rotate(-7deg); -} - -.client-local-rules-toggle.is-open .client-rail-rule-knob-position.is-top { - transform: translateX(4px); -} - -.client-local-rules-toggle.is-open .client-rail-rule-knob-position.is-bottom { - transform: translateX(-4px); -} - -.client-subscription-toggle.is-open .client-rail-subscription-scan, -.client-devices-toggle.is-open .client-rail-device-packet, -.client-diagnostics-toggle.is-open .client-rail-diagnostics-scan { - animation: client-rail-ambient-trace 10s 1.1s linear infinite; -} - -.client-instructions-toggle.is-open .client-rail-book-page.is-right { - animation: client-rail-book-turn 10s 1.1s ease-in-out infinite; -} - -.client-diagnostics-toggle.is-open .client-rail-diagnostics-glass { - animation: client-rail-glass-scan 10s 1.1s ease-in-out infinite; -} - -.client-local-rules-toggle.is-open .client-rail-rule-knob.is-top { - animation: client-rail-rule-tune-top 10s 1.1s ease-in-out infinite; -} - -.client-local-rules-toggle.is-open .client-rail-rule-knob.is-bottom { - animation: client-rail-rule-tune-bottom 10s 1.28s ease-in-out infinite; -} - -@keyframes client-rail-ambient-trace { - 0% { opacity: 0; stroke-dashoffset: 1; } - 2% { opacity: 1; } - 8% { opacity: 1; stroke-dashoffset: 0; } - 10%, 100% { opacity: 0; stroke-dashoffset: 0; } -} - -@keyframes client-rail-book-turn { - 0%, 2%, 10%, 100% { transform: translateX(0) scaleX(1); } - 5% { transform: translateX(-1px) scaleX(0.18); } - 7% { transform: translateX(-0.5px) scaleX(0.55); } -} - -@keyframes client-rail-glass-scan { - 0%, 2%, 10%, 100% { transform: translateX(0) rotate(0); } - 4% { transform: translateX(-3px) rotate(6deg); } - 7% { transform: translateX(1.5px) rotate(-3deg); } -} - -@keyframes client-rail-rule-tune-top { - 0%, 2%, 10%, 100% { transform: translateX(0); } - 6% { transform: translateX(-2px); } -} - -@keyframes client-rail-rule-tune-bottom { - 0%, 2%, 10%, 100% { transform: translateX(0); } - 6% { transform: translateX(2px); } -} - .client-local-rules-toggle:disabled { cursor: default; color: var(--client-muted); @@ -244,7 +97,6 @@ } .client-rail-action.is-open { - --client-rail-state-duration: 480ms; color: var(--client-accent); } diff --git a/src/web/styles/themes.css b/src/web/styles/themes.css index f2da90b..e39da7d 100644 --- a/src/web/styles/themes.css +++ b/src/web/styles/themes.css @@ -117,10 +117,6 @@ stroke-dashoffset: 0; } - .client-rail-action:active:not(:disabled) svg { - transform: none; - } - .client-local-rule, .client-rule-handle, .client-rule-handle svg, @@ -141,8 +137,10 @@ .client-row-add, .client-row-add-slot > span, .client-local-rules-save, - .client-local-rules-help summary, - .client-local-rules-help > div, + .client-local-rules-help > button, + .client-local-rules-help-reveal, + .client-local-rules-help-content, + .client-local-rules-help-copy, .client-local-rules-runtime, .client-local-rules-actions button { transition: none; diff --git a/test/web/responsive-layout-contract.test.js b/test/web/responsive-layout-contract.test.js index 73c5914..6668d1d 100644 --- a/test/web/responsive-layout-contract.test.js +++ b/test/web/responsive-layout-contract.test.js @@ -139,36 +139,19 @@ test('secondary menus share one right rail and switch equal drawers as a vertica assert.match(disabledRulesLabel, /opacity:\s*0/); assert.match(disabledRulesLabel, /filter:\s*blur\(5px\)/); assert.match(styles, /\.client-local-rules-toggle:disabled:hover span\s*\{[\s\S]*opacity:\s*1/); - assert.match(subscription, /client-rail-subscription-back[\s\S]*client-rail-subscription-front[\s\S]*client-rail-subscription-lines[\s\S]*client-rail-subscription-scan/); + assert.match(subscription, /client-rail-subscription-back[\s\S]*client-rail-subscription-front[\s\S]*client-rail-subscription-lines/); assert.match(instructions, /client-rail-book-page-position is-left[\s\S]*client-rail-book-page is-left[\s\S]*client-rail-book-page-position is-right[\s\S]*client-rail-book-page is-right[\s\S]*client-rail-book-spine/); assert.match(component, / { assert.match(reducedMotion, /\.client-drawer/); assert.match(reducedMotion, /transition:\s*none/); assert.match(reducedMotion, /animation:\s*none/); - assert.match(reducedMotion, /\.client-rail-action:active:not\(:disabled\) svg[\s\S]*transform:\s*none/); + assert.doesNotMatch(styles, /\.client-rail-action:active:not\(:disabled\) svg/); }); test('tooltips stay opaque, above adjacent content, and do not stick after pointer clicks', () => { diff --git a/test/web/rule-editor-contract.test.js b/test/web/rule-editor-contract.test.js index e5037ec..82507e3 100644 --- a/test/web/rule-editor-contract.test.js +++ b/test/web/rule-editor-contract.test.js @@ -135,11 +135,16 @@ test('ordered rules use one accessible drag handle and a compact icon route cont assert.match(styles, /\.client-local-rule \+ \.client-local-rule::before[\s\S]*linear-gradient\(90deg/); }); -test('rule values normalize on paste and help stays collapsed above the list', () => { +test('rule values normalize on paste and help animates both directions above the list', () => { assert.match(routing, /normalizeRouteRules\(\[\{ \.\.\.rule, value \}\], \{ strict: true \}\)/); assert.match(routing, /onPaste=\{\(event\) => \{[\s\S]*event\.preventDefault\(\)[\s\S]*normalizeDraftRuleValue\(rule, event\.clipboardData\.getData\('text'\)\)/); assert.match(routing, /onBlur=\{\(\) => feature\.change\(index, 'value', normalizeDraftRuleValue\(rule, rule\.value\)\)\}/); - assert.match(routing, /
[\s\S]*[\s\S]*Как работают правила[\s\S]* !current\)[\s\S]*Как работают правила[\s\S]*aria-hidden=\{!helpOpen\}[\s\S]* div \{[\s\S]*background: color-mix/); + assert.match(styles, /\.client-local-rules-help-reveal \{[\s\S]*grid-template-rows:\s*0fr[\s\S]*opacity:\s*0[\s\S]*420ms cubic-bezier\(0\.16, 1, 0\.3, 1\)/); + assert.match(styles, /\.client-local-rules-help\.is-open \.client-local-rules-help-reveal \{[\s\S]*grid-template-rows:\s*1fr[\s\S]*opacity:\s*1/); + assert.match(styles, /\.client-local-rules-help-content \{[\s\S]*overflow:\s*hidden/); + assert.match(styles, /\.client-local-rules-help-copy \{[\s\S]*background: color-mix[\s\S]*translateY\(-6px\)[\s\S]*transition:\s*transform 420ms/); }); diff --git a/test/web/style-boundaries.test.js b/test/web/style-boundaries.test.js index d697315..648d304 100644 --- a/test/web/style-boundaries.test.js +++ b/test/web/style-boundaries.test.js @@ -37,26 +37,26 @@ const expectedImports = [ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex'); const acceptedLedger = { counts: { - cascadeEdges: 950, - customProperties: 108, - declarations: 3516, + cascadeEdges: 949, + customProperties: 106, + declarations: 3474, important: 0, - keyframes: 53, + keyframes: 48, media: 13, - rules: 1000, - variableReferences: 840, + rules: 966, + variableReferences: 837, }, hashes: { - cascadeEdges: '5d255c2f624bf3b45037c027f127d644f02dfc5a130313e766597da9bee4875f', - customProperties: '3b97add4c3d685a532afff5046ddbe2b6eebcc0866c62781348559897f2930fb', - declarations: '4cf0d3ed382d4757bf7313ea9e941167ccbb28293198e89852fda855b288f492', + cascadeEdges: 'a2289354130ffe83c907b977d21d773799c4435c8016e51d14f153184f834804', + customProperties: 'fd6f16069f9fe3526f09d4d574431ddae67150d8e7a8a40e04c9fd2d179ec7ac', + declarations: '6581f90631086ad0ba2ef1b3d1ec86f608e872572af5486d4c7199f06802bcfd', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848', - keyframes: '78b90f6f08db3eb7c998d6be48799a1fff9179c416b6e4364c0efdeb3591e4d1', - ruleDeclarationSequences: '8d146a746adac504897039e31408dc342e8e9f2940c2984d69b4b21b4d6a8e3f', - selectors: 'fb7382b9ad1e8a7593553ce7f2544647882a1089c967e30623bb48344da0b490', - variableReferences: '1106c0a87671667957261a7f80cdab657d7a5f0078a9feece09c1efcad4f6455', - witnesses: '88e71ed6ab4be07b2c7b45f90677c2f70dbc5d5b7dc8a42c8715e3f8084096dc', + keyframes: 'a0d69c5b3e5f235d3a76ed04bdd64f67a73fafc9c59fe2635ffda603709c0ad9', + ruleDeclarationSequences: 'dc793f9ea84540c87ae3650fa915efed643b941c393e8dd0fd75fcb01c543bb3', + selectors: 'a556fe1d9608d73332db5b3c14cd4cfde42f3346a542d3e2b2ce2af34f556c8c', + variableReferences: '843a5f71b0fb74691ed623dbc7ebb7ddddc34e5736e1f419f17cf54e22d43073', + witnesses: '10b015073632f8697ac16b5849f28029322d9164060896c366ba4d35dbb3cee5', }, }; @@ -209,7 +209,7 @@ test('client typography uses the shared semantic scale outside the token owner', test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => { const witnesses = readStyleWitnesses(root); - assert.equal(witnesses.length, 863); + assert.equal(witnesses.length, 860); assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0); const ledger = createStyleLedger(readStyleSource(root), { witnesses }); assert.deepEqual(ledger.counts, acceptedLedger.counts); @@ -405,8 +405,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-BlKS5cQQ.css']); + assert.deepEqual(assets, ['index-DUSJhcRO.css']); const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0])); - assert.equal(built.byteLength, 136693); - assert.equal(sha256(built), '3486f1e09a97e1293d7cdd4c7661b994bda1f388c1560518657da1b3a8318d6e'); + assert.equal(built.byteLength, 133278); + assert.equal(sha256(built), 'f3116a2f0f282a10030589057ec0c62800640b7b552180dfe12d4b737a5fc436'); });