Improve startup animation and traffic chart headroom
Build and Deploy Gateway / build-and-push (push) Successful in 13s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-07 22:03:18 +03:00
parent fbf22e0b88
commit b2a2ed7104
6 changed files with 54 additions and 19 deletions
+2 -2
View File
@@ -627,7 +627,7 @@ export function ClientOverviewPage({
}
});
const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription);
const [showIntro, setShowIntro] = useState(!hasSubscription);
const [showIntro, setShowIntro] = useState(true);
const [subscriptionContentReady, setSubscriptionContentReady] = useState(hasSubscription);
const [copyFeedback, setCopyFeedback] = useState(null);
const [subscriptionValidation, setSubscriptionValidation] = useState({
@@ -1113,7 +1113,7 @@ export function ClientOverviewPage({
return (
<div
className={`client-shell${hasSubscription ? '' : ' is-first-run'}${showIntro && !hasSubscription ? ' is-intro' : ''}`}
className={`client-shell${hasSubscription ? '' : ' is-first-run'}${showIntro ? ' is-intro' : ''}`}
>
<VersionDisplay isGateway={isGateway} versionInfo={versionInfo} />
<div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
+16 -6
View File
@@ -16,6 +16,8 @@ const AUTO_REFRESH_MS = 15_000;
const DEVICE_MOVE_MS = 520;
const COPY_FEEDBACK_MS = 800;
const TRAFFIC_DELTA_MS = 2_200;
const TRAFFIC_CHART_HEADROOM = 10;
const trafficChartY = (ratio) => 100 - ratio * (100 - TRAFFIC_CHART_HEADROOM);
function Tooltip({ children }) {
return <span className="client-tooltip" role="tooltip">{children}</span>;
@@ -62,8 +64,8 @@ function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
x: (firstSlot + index) * 100 / Math.max(1, capacity - 1),
gateway,
proxy,
gatewayY: 100 - trafficScaleRatio(gateway, max, scale) * 100,
proxyY: 100 - trafficScaleRatio(proxy, max, scale) * 100,
gatewayY: trafficChartY(trafficScaleRatio(gateway, max, scale)),
proxyY: trafficChartY(trafficScaleRatio(proxy, max, scale)),
};
});
const previous = points.slice(0, -1);
@@ -108,7 +110,15 @@ function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
document.body,
);
return <span className="client-device-traffic-chart" role="img" aria-label={`История трафика, шкала ${scale === 'log' ? 'логарифмическая' : 'линейная'}, максимум ${formatByteString(max)}`}>
return <span
className="client-device-traffic-chart"
role="img"
aria-label={`История трафика, шкала ${scale === 'log' ? 'логарифмическая' : 'линейная'}, максимум ${formatByteString(max)}`}
style={{
'--traffic-chart-top': `${TRAFFIC_CHART_HEADROOM}%`,
'--traffic-chart-mid': `${(100 + TRAFFIC_CHART_HEADROOM) / 2}%`,
}}
>
{pinned && max > 0n && <span className="client-device-traffic-axis" aria-hidden="true">
<span className="is-max">{formatByteString(max)}</span>
<span className="is-mid">{formatByteString(mid)}</span>
@@ -117,8 +127,8 @@ function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
<span className="client-device-traffic-plot" onPointerMove={trackPointer} onPointerLeave={() => setHovered(null)}>
<svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
{pinned && <g className="client-device-traffic-grid">
<line x1="0" x2="100" y1="0" y2="0" />
<line x1="0" x2="100" y1="50" y2="50" />
<line x1="0" x2="100" y1={TRAFFIC_CHART_HEADROOM} y2={TRAFFIC_CHART_HEADROOM} />
<line x1="0" x2="100" y1={(100 + TRAFFIC_CHART_HEADROOM) / 2} y2={(100 + TRAFFIC_CHART_HEADROOM) / 2} />
<line x1="0" x2="100" y1="100" y2="100" />
</g>}
<g className="client-device-traffic-lines" style={{ '--sample-count': Math.max(1, capacity) }}>
@@ -165,7 +175,7 @@ function TrafficChart({ samples, scale, capacity, routeLabel, pinned }) {
</circle>}
</g>
{hovered && <g className="client-device-traffic-cursor">
<line x1={hovered.x} x2={hovered.x} y1="0" y2="100" />
<line x1={hovered.x} x2={hovered.x} y1={TRAFFIC_CHART_HEADROOM} y2="100" />
<circle className="is-gateway" cx={hovered.x} cy={hovered.gatewayY} r="2" />
{hovered.proxy > 0n && <circle className="is-proxy" cx={hovered.x} cy={hovered.proxyY} r="2" />}
</g>}
+17 -9
View File
@@ -365,19 +365,21 @@ p {
}
.client-shell.is-intro .harbor-brand-content {
animation: harbor-first-run 1200ms cubic-bezier(0.16, 1, 0.3, 1);
animation: harbor-startup-brand 1200ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-shell.is-intro .client-form {
animation: harbor-first-run-form 650ms 720ms cubic-bezier(0.16, 1, 0.3, 1) both;
.client-shell.is-intro .client-panel,
.client-shell.is-intro .client-secondary-menu,
.client-shell.is-intro .harbor-versions {
animation: harbor-startup-content 680ms 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes harbor-first-run {
from { transform: translateY(53px) scale(1.59); }
@keyframes harbor-startup-brand {
from { opacity: 0.72; filter: blur(5px); transform: translateY(53px); }
}
@keyframes harbor-first-run-form {
from { opacity: 0; filter: blur(8px); }
@keyframes harbor-startup-content {
from { opacity: 0; filter: blur(10px); }
}
.harbor-brand svg {
@@ -1406,11 +1408,11 @@ p {
}
.client-device-traffic-axis > .is-max {
top: 0;
top: var(--traffic-chart-top);
}
.client-device-traffic-axis > .is-mid {
top: 50%;
top: var(--traffic-chart-mid);
transform: translateY(-50%);
}
@@ -4985,6 +4987,12 @@ p {
animation: none;
}
.client-shell.is-intro .client-panel,
.client-shell.is-intro .client-secondary-menu,
.client-shell.is-intro .harbor-versions {
animation: none;
}
.harbor-mode-stack em,
.harbor-mode-gateway > span,
.harbor-mode-swap g,