Rename client app to Harbor Connect and refresh state animations
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 1s

This commit is contained in:
2026-07-11 16:24:28 +03:00
parent edad26d978
commit b53cd08dcc
4 changed files with 70 additions and 23 deletions

View File

@@ -78,6 +78,12 @@ function DurationPart({ name, children }) {
);
}
function AnimatedSeconds({ value, padded = true }) {
return String(value).padStart(padded ? 2 : 1, '0').split('').map((digit, index) => (
<span className="client-duration-second-digit" key={`${index}-${digit}`}>{digit}</span>
));
}
function HarborBrand({ isGateway, gatewayAvailable, gatewayDirect, busy, onSetGatewayAuto }) {
const [modeAnimating, setModeAnimating] = useState(false);
const stopModeAnimationRef = useRef(false);
@@ -213,6 +219,9 @@ export function ClientOverviewPage({
const proxyUrls = localProxyUrls(state?.proxyPort, gatewayAddress);
const usage = subscriptionUsage(state?.userInfo);
const duration = connectionDurationParts(state?.singboxStartedAt, now);
const connectionTitle = connected
? gatewayDirect ? 'Gateway подключён' : 'VPN включён'
: 'Подключение выключено';
const [displayedUsed, setDisplayedUsed] = useState(usage.used);
const hasUsage = Boolean(
state?.userInfo && ['upload', 'download', 'total', 'expire'].some((key) => key in state.userInfo),
@@ -513,10 +522,10 @@ export function ClientOverviewPage({
</svg>
</button>
<div className="client-state-copy" aria-live="polite">
<h2 key={connected ? 'connected' : 'disconnected'} id="connection-title">
{connected
? gatewayDirect ? 'Gateway подключён' : 'VPN включён'
: 'Подключение выключено'}
<h2 id="connection-title" className="client-connection-title" aria-label={connectionTitle}>
<span className={!connected ? 'is-active' : ''} aria-hidden="true">Подключение выключено</span>
<span className={connected && !gatewayDirect ? 'is-active' : ''} aria-hidden="true">VPN включён</span>
<span className={connected && gatewayDirect ? 'is-active' : ''} aria-hidden="true">Gateway подключён</span>
</h2>
<div className="client-state-detail">
{connected ? (
@@ -534,7 +543,7 @@ export function ClientOverviewPage({
>
<DurationPart name="hours-value">{String(duration.totalHours).padStart(2, '0')}</DurationPart>
:<DurationPart name="minutes-value">{String(duration.minutes.value).padStart(2, '0')}</DurationPart>
:<DurationPart name="seconds-value" key={`digital-${duration.seconds.value}`}>{String(duration.seconds.value).padStart(2, '0')}</DurationPart>
:<DurationPart name="seconds-value"><AnimatedSeconds value={duration.seconds.value} /></DurationPart>
</time>
<time
className={`client-duration${durationMode === 'words' ? ' is-active' : ''}`}
@@ -549,7 +558,9 @@ export function ClientOverviewPage({
.filter(([name, part]) => part.value || name === 'seconds')
.map(([name, part]) => (
<span className="client-duration-unit" data-unit={name} key={name}>
<DurationPart name={`${name}-value`} key={`${name}-${name === 'seconds' ? part.value : 'value'}`}>{part.value}</DurationPart>{' '}
<DurationPart name={`${name}-value`}>{name === 'seconds'
? <AnimatedSeconds value={part.value} padded={false} />
: part.value}</DurationPart>{' '}
<DurationPart name={`${name}-label`}>{part.label}</DurationPart>
</span>
))}

View File

@@ -921,6 +921,26 @@ p {
animation: client-state-reveal 700ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-connection-title {
display: grid;
min-height: 24px;
place-items: center;
}
.client-connection-title > span {
grid-area: 1 / 1;
opacity: 0;
filter: blur(5px);
transform: scale(0.97);
transition: opacity 480ms ease, filter 620ms cubic-bezier(0.16, 1, 0.3, 1), transform 620ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-connection-title > .is-active {
opacity: 1;
filter: blur(0);
transform: scale(1);
}
.client-state-detail {
min-height: 20px;
display: grid;
@@ -978,12 +998,24 @@ p {
.client-duration-seconds-value {
display: inline-block;
animation: client-second-tick 380ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-duration-second-digit {
display: inline-block;
animation: client-second-tick 520ms cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes client-second-tick {
from { opacity: 0.28; filter: blur(3px); transform: scale(0.9); }
to { opacity: 1; filter: blur(0); transform: scale(1); }
from {
opacity: 0.82;
filter: blur(1px);
text-shadow: 0 0 8px color-mix(in oklch, var(--client-accent) 52%, transparent);
}
to {
opacity: 1;
filter: blur(0);
text-shadow: 0 0 0 transparent;
}
}
.client-duration-toggle {
@@ -1912,6 +1944,7 @@ p {
.client-copy-feedback,
.client-duration,
.client-duration-seconds-value,
.client-duration-second-digit,
.client-instructions,
.client-instructions-toggle,
.client-instructions-toggle svg,
@@ -1961,7 +1994,9 @@ p {
}
.client-state-copy h2,
.client-connection-title > span,
.client-state-detail > * {
transition: none;
animation: none;
}
}