Raise Harbor versions and move secondary menus to the right rail
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 15s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-07-14 21:49:48 +03:00
parent f629309f32
commit a37c211c42
4 changed files with 87 additions and 152 deletions

View File

@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.8.1',
gatewayClient: '0.8.1',
macClient: '0.8.2',
gatewayClient: '0.8.2',
gatewayBackend: '0.8.0',
});

View File

@@ -1068,46 +1068,47 @@ export function ClientOverviewPage({
blocked={gatewayAutoBlocked}
onSetGatewayAuto={onSetGatewayAuto}
/>
{hasSubscription && subscriptionContentReady && <button
ref={instructionsToggleRef}
className={`client-instructions-toggle${instructionsOpen ? ' is-open' : ''}`}
type="button"
aria-expanded={instructionsOpen}
aria-controls="client-instructions"
aria-label={instructionsOpen ? 'Закрыть инструкции' : 'Как использовать'}
onClick={() => {
if (localRulesOpen && !requestCloseLocalRules()) return;
setInstructionsOpen((open) => !open);
}}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m3 5 7 7-7 7" />
<path d="m7 5 7 7-7 7" />
<path d="m11 5 7 7-7 7" />
</svg>
<span>Как использовать</span>
</button>}
{hasSubscription && subscriptionContentReady && <button
ref={localRulesToggleRef}
className={`client-local-rules-toggle${localRulesOpen ? ' is-open' : ''}${localRulesPendingRestart ? ' has-pending' : ''}`}
type="button"
disabled={gatewayDirect}
aria-expanded={localRulesOpen}
aria-controls="client-local-rules"
aria-label={gatewayDirect
? 'Сейчас работают правила Harbor Gateway'
: localRulesOpen ? 'Закрыть локальные правила' : 'Настроить локальные правила'}
onClick={() => localRulesOpen ? requestCloseLocalRules() : openLocalRules()}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 7h9M17 7h3M4 17h3M11 17h9" />
<circle cx="15" cy="7" r="2" />
<circle cx="9" cy="17" r="2" />
</svg>
<span>{gatewayDirect
? 'Сейчас работают правила Gateway'
: localRulesPendingRestart ? 'Правила ждут перезапуска' : 'Локальные правила'}</span>
</button>}
{hasSubscription && subscriptionContentReady && <nav className="client-secondary-menu" aria-label="Дополнительные меню">
<button
ref={instructionsToggleRef}
className={`client-instructions-toggle${instructionsOpen ? ' is-open' : ''}`}
type="button"
aria-expanded={instructionsOpen}
aria-controls="client-instructions"
aria-label={instructionsOpen ? 'Закрыть инструкции' : 'Как использовать'}
onClick={() => {
if (localRulesOpen && !requestCloseLocalRules()) return;
setInstructionsOpen((open) => !open);
}}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="8.5" />
<path d="M12 11v5M12 8h.01" />
</svg>
<span>Как использовать</span>
</button>
<button
ref={localRulesToggleRef}
className={`client-local-rules-toggle${localRulesOpen ? ' is-open' : ''}${localRulesPendingRestart ? ' has-pending' : ''}`}
type="button"
disabled={gatewayDirect}
aria-expanded={localRulesOpen}
aria-controls="client-local-rules"
aria-label={gatewayDirect
? 'Сейчас работают правила Harbor Gateway'
: localRulesOpen ? 'Закрыть локальные правила' : 'Настроить локальные правила'}
onClick={() => localRulesOpen ? requestCloseLocalRules() : openLocalRules()}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 7h9M17 7h3M4 17h3M11 17h9" />
<circle cx="15" cy="7" r="2" />
<circle cx="9" cy="17" r="2" />
</svg>
<span>{gatewayDirect
? 'Сейчас работают правила Gateway'
: localRulesPendingRestart ? 'Правила ждут перезапуска' : 'Локальные правила'}</span>
</button>
</nav>}
<main className={`client-panel${showPower ? '' : ' is-setup'}${hasSubscription ? ' has-subscription' : ''}`}>
{showPower && (
<section className="client-power-section" aria-labelledby="connection-title">

View File

@@ -662,107 +662,21 @@ p {
92% { transform: translateX(-0.75px); }
}
.client-instructions-toggle {
.client-secondary-menu {
position: fixed;
top: 50%;
left: 14px;
right: max(14px, env(safe-area-inset-right));
z-index: 30;
width: 42px;
height: 54px;
display: grid;
place-items: center;
padding: 0;
border: 0;
background: transparent;
color: var(--client-muted);
cursor: pointer;
gap: 6px;
transform: translateY(-50%);
transition: transform 440ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-instructions-toggle svg {
position: relative;
z-index: 1;
width: 23px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
overflow: visible;
transition: transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-instructions-toggle path {
opacity: 0;
transform: translateX(-5px);
transition: opacity 260ms ease, transform 480ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-instructions-toggle path:last-child {
opacity: 1;
transform: translateX(-4px);
}
.client-instructions-toggle span {
position: absolute;
left: 48px;
width: max-content;
color: var(--client-text);
font: 700 10px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
letter-spacing: 0.04em;
opacity: 0;
filter: blur(5px);
pointer-events: none;
transform: translateX(-8px);
transition: opacity 350ms ease, filter 350ms ease, transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-instructions-toggle:hover,
.client-instructions-toggle:focus-visible {
outline: none;
transform: translateY(-50%) translateX(3px);
}
.client-instructions-toggle:hover path,
.client-instructions-toggle:focus-visible path {
opacity: 1;
transform: translateX(0);
}
.client-instructions-toggle:hover span,
.client-instructions-toggle:focus-visible span {
opacity: 1;
filter: blur(0);
transform: translateX(0);
}
.client-instructions-toggle.is-open svg {
transform: rotate(180deg);
}
.client-instructions-toggle.is-open path {
opacity: 0;
}
.client-instructions-toggle.is-open path:first-child {
opacity: 1;
transform: translateX(4px);
}
.client-instructions-toggle.is-open span {
opacity: 0;
visibility: hidden;
}
.client-instructions-toggle,
.client-local-rules-toggle {
position: fixed;
top: 50%;
right: 14px;
z-index: 30;
position: relative;
width: 42px;
height: 54px;
height: 48px;
display: grid;
place-items: center;
padding: 0;
@@ -770,10 +684,10 @@ p {
background: transparent;
color: var(--client-muted);
cursor: pointer;
transform: translateY(-50%);
transition: color 240ms ease, transform 440ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-instructions-toggle svg,
.client-local-rules-toggle svg {
width: 23px;
height: 23px;
@@ -782,9 +696,11 @@ p {
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
overflow: visible;
transition: color 240ms ease, filter 360ms ease, transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-instructions-toggle span,
.client-local-rules-toggle span {
position: absolute;
right: 48px;
@@ -799,18 +715,17 @@ p {
transition: opacity 350ms ease, filter 350ms ease, transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-instructions-toggle:hover,
.client-instructions-toggle:focus-visible,
.client-local-rules-toggle:hover,
.client-local-rules-toggle:focus-visible {
outline: none;
outline: 0;
color: var(--client-accent);
transform: translateY(-50%) translateX(-3px);
}
.client-local-rules-toggle:hover svg,
.client-local-rules-toggle:focus-visible svg {
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-accent) 52%, transparent));
transform: translateX(-3px);
}
.client-instructions-toggle:hover span,
.client-instructions-toggle:focus-visible span,
.client-local-rules-toggle:hover span,
.client-local-rules-toggle:focus-visible span {
opacity: 1;
@@ -818,9 +733,19 @@ p {
transform: translateX(0);
}
.client-instructions-toggle:hover svg,
.client-instructions-toggle:focus-visible svg,
.client-local-rules-toggle:hover svg,
.client-local-rules-toggle:focus-visible svg,
.client-instructions-toggle.is-open svg,
.client-local-rules-toggle.is-open svg {
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-accent) 52%, transparent));
}
.client-local-rules-toggle:disabled {
cursor: default;
color: var(--client-muted);
transform: none;
}
.client-local-rules-toggle:disabled span {
@@ -829,6 +754,7 @@ p {
transform: translateX(0);
}
.client-instructions-toggle.is-open,
.client-local-rules-toggle.is-open {
color: var(--client-accent);
}
@@ -845,6 +771,7 @@ p {
transform: rotate(90deg);
}
.client-instructions-toggle.is-open span,
.client-local-rules-toggle.is-open span {
opacity: 0;
visibility: hidden;
@@ -1380,16 +1307,16 @@ p {
.client-instructions {
position: fixed;
inset: 0 auto 0 0;
inset: 0 0 0 auto;
z-index: 20;
width: min(470px, 100vw);
overflow-y: auto;
background: color-mix(in oklch, var(--client-bg) 99%, var(--client-panel));
color: var(--client-text);
box-shadow: 26px 0 72px oklch(0.09 0.015 145 / 0.12);
box-shadow: -26px 0 72px oklch(0.09 0.015 145 / 0.12);
opacity: 0;
visibility: hidden;
transform: translateX(-104%);
transform: translateX(104%);
transition: transform 760ms cubic-bezier(0.16, 1, 0.3, 1), opacity 500ms ease, visibility 0s 760ms;
}
@@ -1403,7 +1330,7 @@ p {
.client-instructions-sheet {
position: relative;
min-height: 100%;
padding: 54px 34px 72px 72px;
padding: 54px 72px 72px 34px;
font-family: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
}
@@ -3520,11 +3447,7 @@ p {
transform: translateX(-50%) scale(1.35);
}
.client-instructions-toggle {
left: 8px;
}
.client-local-rules-toggle {
.client-secondary-menu {
right: 8px;
}
@@ -3572,7 +3495,7 @@ p {
}
.client-instructions-sheet {
padding: 40px 18px 60px 58px;
padding: 40px 58px 60px 18px;
}
.client-local-rules-sheet {

View File

@@ -73,6 +73,17 @@ test('tablet and mobile regions use normal flow with viewport-safe widths', () =
assert.ok(Math.min(360, desktopOuterColumn) <= desktopOuterColumn, 'details fit the 1440px desktop grid');
});
test('secondary menus share one right rail and both drawers open from the right', () => {
assert.match(component, /<nav className="client-secondary-menu" aria-label="Дополнительные меню">/);
assert.match(component, /client-instructions-toggle[\s\S]*client-local-rules-toggle/);
assert.match(rule('.client-secondary-menu'), /right:\s*max\(14px, env\(safe-area-inset-right\)\)/);
assert.match(rule('.client-secondary-menu'), /display:\s*grid/);
assert.match(rule('.client-instructions'), /inset:\s*0 0 0 auto/);
assert.match(rule('.client-instructions'), /transform:\s*translateX\(104%\)/);
assert.match(rule('.client-local-rules'), /inset:\s*0 0 0 auto/);
assert.match(rule('.client-local-rules'), /transform:\s*translateX\(104%\)/);
});
test('responsive motion has a reduced-motion fallback', () => {
const reducedMotion = /@media \(prefers-reduced-motion: reduce\) \{([\s\S]*)\n\}/.exec(styles)?.[1] || '';