Simplify rail motion and add routing help toggle
Build and Deploy Gateway / build-and-push (push) Successful in 35s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-17 17:26:57 +03:00
parent 0a0a932057
commit dc1fd76c44
11 changed files with 91 additions and 221 deletions
+2 -2
View File
@@ -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',
});
@@ -249,8 +249,6 @@ export function DevicesToggle({
</g>
<rect className="client-rail-device-phone" x="16.5" y="7" width="5" height="9" rx="1.3" />
<path className="client-rail-device-link" d="M19 16v3h-5.5" />
<path className="client-rail-device-bridge" d="M10.5 19h3" />
<path className="client-rail-device-packet" pathLength="1" d="M7 19h12" />
</svg>
</RailAction>;
}
@@ -61,7 +61,6 @@ export function DiagnosticsToggle({
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path className="client-rail-diagnostics-trace" d="M4.5 10h2l1.2-2.4 2.2 4.8 1.2-2.4h3.4" />
<path className="client-rail-diagnostics-scan" pathLength="1" d="M4.5 10h2l1.2-2.4 2.2 4.8 1.2-2.4h3.4" />
<g className="client-rail-diagnostics-position">
<g className="client-rail-diagnostics-glass">
<circle cx="9.5" cy="9.5" r="6.5" />
+19 -8
View File
@@ -863,6 +863,7 @@ export function RoutingPendingStatus({
}
export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature; statusSlot?: ReactNode }) {
const [helpOpen, setHelpOpen] = useState(false);
const draftRules = feature.rules.filter((rule) => !rule.removing);
const editorDisabled = feature.blocked || !feature.editable;
const canAdd = canAppendRouteRule(draftRules) && !editorDisabled;
@@ -891,7 +892,6 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
Сохранить
</button>
<h2 id="local-rules-title">Правила маршрутизации</h2>
<p>Проверяются сверху вниз. Первое совпадение выбирает маршрут.</p>
{!feature.editable && (
<p className="client-local-rules-runtime" role="status">
Обновите Harbor, чтобы редактировать правила.
@@ -912,14 +912,25 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
Правила сохранены и начнут работать после запуска Harbor.
</p>
)}
<details className="client-local-rules-help">
<summary>Как работают правила</summary>
<div>
<p>Правила проверяются сверху вниз. Первое совпадение выбирает маршрут: щит означает VPN, стрелка прямое подключение.</p>
<p>Правила применяются только к трафику внутри маршрутизации Harbor. Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список.</p>
<p>Можно вставить полный URL: Harbor сразу оставит только домен. Путь и параметры HTTPS для маршрутизации недоступны.</p>
<div className={`client-local-rules-help${helpOpen ? ' is-open' : ''}`}>
<button
type="button"
aria-expanded={helpOpen}
aria-controls="client-local-rules-help-content"
onClick={() => setHelpOpen((current) => !current)}
>
Как работают правила
</button>
<div className="client-local-rules-help-reveal" aria-hidden={!helpOpen}>
<div id="client-local-rules-help-content" className="client-local-rules-help-content">
<div className="client-local-rules-help-copy">
<p>Правила проверяются сверху вниз. Первое совпадение выбирает маршрут: щит означает VPN, стрелка прямое подключение.</p>
<p>Правила применяются только к трафику внутри маршрутизации Harbor. Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список.</p>
<p>Можно вставить полный URL: Harbor сразу оставит только домен. Путь и параметры HTTPS для маршрутизации недоступны.</p>
</div>
</div>
</div>
</details>
</div>
</header>
<form id="client-local-rules-form" className="client-local-rules-form" onSubmit={feature.save}>
@@ -322,7 +322,6 @@ export function SubscriptionToggle({
<g className="client-rail-subscription-front">
<rect className="client-rail-subscription-card" x="4" y="6" width="14" height="14" rx="2" />
<path className="client-rail-subscription-lines" d="M7.5 10.5h6.5M7.5 14.5H12" />
<path className="client-rail-subscription-scan" pathLength="1" d="M7.5 10.5H14" />
</g>
</svg>
</RailAction>;
+31 -6
View File
@@ -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);
-148
View File
@@ -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);
}
+4 -6
View File
@@ -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;