diff --git a/src/shared/versions.ts b/src/shared/versions.ts
index 1818129..b966e0f 100644
--- a/src/shared/versions.ts
+++ b/src/shared/versions.ts
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
- macClient: '0.21.7',
- gatewayClient: '0.22.2',
+ macClient: '0.21.8',
+ gatewayClient: '0.22.3',
gatewayBackend: '0.22.5',
});
diff --git a/src/web/features/devices/DevicesPanel.tsx b/src/web/features/devices/DevicesPanel.tsx
index e99e04e..16884ff 100644
--- a/src/web/features/devices/DevicesPanel.tsx
+++ b/src/web/features/devices/DevicesPanel.tsx
@@ -367,7 +367,8 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
{devices.map((device) => {
const collapsing = Boolean(pinCollapses[device.id]);
- const displayPinned = device.pinned || collapsing;
+ const expanded = device.pinned && !collapsing;
+ const showDetails = device.pinned || collapsing;
const hasName = Boolean(device.alias || device.hostname);
const title = device.alias || device.hostname || device.ip || 'Неизвестное устройство';
const editing = editingId === device.id;
@@ -409,7 +410,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
if (node) deviceNodes.current.set(device.id, node);
else deviceNodes.current.delete(device.id);
}}
- className={`client-device is-${device.status}${displayPinned ? ' is-pinned' : ''}${collapsing ? ' is-collapsing' : ''}`}
+ className={`client-device is-${device.status}${expanded ? ' is-pinned' : ''}${collapsing ? ' is-collapsing' : ''}`}
key={device.id}
>
@@ -528,7 +529,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
scale={trafficScale}
capacity={snapshot?.trafficHistoryCapacity || device.trafficHistory?.length || 1}
routeLabel={device.appliedPolicy === 'direct' ? 'Напрямую' : 'Gateway'}
- pinned={displayPinned}
+ pinned={showDetails}
collapsing={collapsing}
onCollapseEnd={() => finishPinCollapse(device.id)}
/>
diff --git a/src/web/features/devices/TrafficChart.tsx b/src/web/features/devices/TrafficChart.tsx
index ff47361..db160d6 100644
--- a/src/web/features/devices/TrafficChart.tsx
+++ b/src/web/features/devices/TrafficChart.tsx
@@ -200,7 +200,7 @@ export function TrafficChart({
'--traffic-chart-mid': `${(100 + TRAFFIC_CHART_HEADROOM) / 2}%`,
} as CSSProperties}
onAnimationEnd={(event: AnimationEvent) => {
- if (collapsing && event.animationName === 'client-device-chart-collapse') onCollapseEnd?.();
+ if (collapsing && event.animationName === 'client-device-traffic-plot-collapse') onCollapseEnd?.();
}}
>
{pinned && max > 0n &&
diff --git a/src/web/styles/features/devices.css b/src/web/styles/features/devices.css
index b82d457..e5d8c12 100644
--- a/src/web/styles/features/devices.css
+++ b/src/web/styles/features/devices.css
@@ -670,13 +670,40 @@
overflow: visible;
}
-.client-device.is-pinned .client-device-traffic-chart {
- transform-origin: bottom center;
- animation: client-device-chart-expand 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
+.client-device.is-pinned .client-device-traffic-plot {
+ transform-origin: top center;
+ animation: client-device-traffic-plot-expand 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
-.client-device.is-pinned .client-device-traffic-chart.is-collapsing {
- animation-name: client-device-chart-collapse;
+.client-device.is-collapsing .client-device-traffic-plot {
+ transform-origin: top center;
+ animation: client-device-traffic-plot-collapse 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
+}
+
+.client-device.is-pinned .client-device-traffic-axis,
+.client-device.is-pinned .client-device-traffic-grid {
+ animation: client-device-traffic-detail-in 360ms 80ms cubic-bezier(0.16, 1, 0.3, 1) both;
+}
+
+.client-device.is-collapsing .client-device-traffic-axis {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 34px;
+ height: 58px;
+}
+
+.client-device.is-collapsing .client-device-traffic-axis,
+.client-device.is-collapsing .client-device-traffic-grid {
+ animation: client-device-traffic-detail-out 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
+}
+
+.client-device.is-pinned .client-device-traffic-time {
+ animation: client-device-traffic-time-expand 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
+}
+
+.client-device.is-collapsing .client-device-traffic-time {
+ animation: client-device-traffic-time-collapse 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.client-device-traffic-axis {
@@ -861,30 +888,34 @@
from { opacity: 0; filter: blur(3px); }
}
-@keyframes client-device-chart-expand {
- from {
- opacity: 0.72;
- clip-path: inset(calc(100% - 34px) 0 0);
- transform: scaleY(0.48);
- }
- to {
- opacity: 1;
- clip-path: inset(0);
- transform: scaleY(1);
- }
+@keyframes client-device-traffic-plot-expand {
+ from { transform: scaleY(0.3448275862); }
+ to { transform: scaleY(1); }
}
-@keyframes client-device-chart-collapse {
- from {
- opacity: 1;
- clip-path: inset(0);
- transform: scaleY(1);
- }
- to {
- opacity: 0.72;
- clip-path: inset(calc(100% - 34px) 0 0);
- transform: scaleY(0.48);
- }
+@keyframes client-device-traffic-plot-collapse {
+ from { transform: scaleY(2.9); }
+ to { transform: scaleY(1); }
+}
+
+@keyframes client-device-traffic-time-expand {
+ from { transform: translateY(-38px); }
+ to { transform: translateY(0); }
+}
+
+@keyframes client-device-traffic-time-collapse {
+ from { transform: translateY(38px); }
+ to { transform: translateY(0); }
+}
+
+@keyframes client-device-traffic-detail-in {
+ from { opacity: 0; transform: translateY(5px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes client-device-traffic-detail-out {
+ from { opacity: 1; transform: translateY(0); }
+ to { opacity: 0; transform: translateY(5px); }
}
.client-device-policy-wrap {
diff --git a/src/web/styles/themes.css b/src/web/styles/themes.css
index a236c42..9a6f026 100644
--- a/src/web/styles/themes.css
+++ b/src/web/styles/themes.css
@@ -75,6 +75,10 @@
.client-device-traffic-value > span,
.client-device-traffic-breakdown,
.client-device-traffic-breakdown > span,
+ .client-device-traffic-plot,
+ .client-device-traffic-axis,
+ .client-device-traffic-grid,
+ .client-device-traffic-time,
.client-device-traffic-lines,
.client-device-traffic-point-tooltip,
.client-device-traffic-chart,
diff --git a/test/web/device-inventory-contract.test.js b/test/web/device-inventory-contract.test.js
index f29061b..7100b3a 100644
--- a/test/web/device-inventory-contract.test.js
+++ b/test/web/device-inventory-contract.test.js
@@ -116,13 +116,15 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.doesNotMatch(panel, /точная MAC|частная MAC|Источник<\/dt>/);
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
assert.match(panel, /const \[pinCollapses, setPinCollapses\][\s\S]*animationDone[\s\S]*result: saved \? 'saved' : 'failed'/);
- assert.match(panel, /const displayPinned = device\.pinned \|\| collapsing[\s\S]*collapsing=\{collapsing\}[\s\S]*onCollapseEnd=/);
+ assert.match(panel, /const expanded = device\.pinned && !collapsing[\s\S]*const showDetails = device\.pinned \|\| collapsing[\s\S]*pinned=\{showDetails\}[\s\S]*onCollapseEnd=/);
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
assert.match(panel, /maxLength=\{64\}[\s\S]*autoFocus/);
assert.match(styles, /\.client-devices \{\s*width: min\(580px, 100vw\)/);
assert.match(styles, /\.client-device \{[\s\S]*--client-device-chart-height: 34px;[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*grid-template-rows: 34px var\(--client-device-chart-height\);[\s\S]*padding: 10px 8px/);
assert.match(styles, /\.client-device\.is-pinned \{[\s\S]*--client-device-chart-height: 72px/);
- assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-chart \{[\s\S]*client-device-chart-expand 520ms/);
+ assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-expand 520ms/);
+ assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-collapse 520ms/);
+ assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-axis \{[\s\S]*top: 0;[\s\S]*height: 58px/);
assert.match(styles, /\.client-device-main \{[\s\S]*display: flex;[\s\S]*align-items: center/);
assert.match(styles, /\.client-device-traffic-total,[\s\S]*\.client-device-traffic-breakdown > span \{[\s\S]*grid-template-columns: 46px minmax\(0, 1fr\)/);
assert.match(styles, /\.client-device-traffic-breakdown > span \{[\s\S]*translateY\(-12px\)[\s\S]*\.client-device-traffic:hover \.client-device-traffic-breakdown > span,[\s\S]*opacity: 1[\s\S]*translateY\(0\)/);
@@ -139,8 +141,10 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(styles, /\.client-device-traffic-point-tooltip \{[\s\S]*position: fixed;[\s\S]*z-index: 1002/);
assert.doesNotMatch(styles, /client-device-traffic-shift|client-device-traffic-line-draw/);
assert.match(styles, /\.client-device-traffic-lines \.is-upload \{[\s\S]*stroke-dasharray: 5 4/);
- assert.match(styles, /@keyframes client-device-chart-expand[\s\S]*clip-path: inset\(calc\(100% - 34px\) 0 0\)[\s\S]*scaleY\(1\)/);
- assert.match(styles, /@keyframes client-device-chart-collapse[\s\S]*clip-path: inset\(0\)[\s\S]*clip-path: inset\(calc\(100% - 34px\) 0 0\)[\s\S]*scaleY\(0\.48\)/);
+ assert.match(styles, /@keyframes client-device-traffic-plot-expand[\s\S]*scaleY\(0\.3448275862\)[\s\S]*scaleY\(1\)/);
+ assert.match(styles, /@keyframes client-device-traffic-plot-collapse[\s\S]*scaleY\(2\.9\)[\s\S]*scaleY\(1\)/);
+ assert.match(styles, /@keyframes client-device-traffic-detail-in[\s\S]*opacity: 0[\s\S]*opacity: 1/);
+ assert.match(styles, /@keyframes client-device-traffic-detail-out[\s\S]*opacity: 1[\s\S]*opacity: 0/);
assert.match(styles, /\.client-device-policy \{[\s\S]*width: 34px;[\s\S]*border-radius: 50%/);
assert.match(styles, /\.client-device-alias-trigger \{[\s\S]*font: 700 14px\/1\.2/);
assert.match(styles, /\.client-device-alias-input \{[\s\S]*width: var\(--alias-width\)[\s\S]*caret-color: var\(--client-accent\)[\s\S]*client-device-alias-edit-in 360ms/);
diff --git a/test/web/style-boundaries.test.js b/test/web/style-boundaries.test.js
index 1b1356a..07b89b4 100644
--- a/test/web/style-boundaries.test.js
+++ b/test/web/style-boundaries.test.js
@@ -36,24 +36,24 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
- cascadeEdges: 798,
+ cascadeEdges: 804,
customProperties: 31,
- declarations: 2900,
+ declarations: 2914,
important: 0,
- keyframes: 47,
+ keyframes: 51,
media: 10,
- rules: 865,
+ rules: 878,
variableReferences: 336,
},
hashes: {
- cascadeEdges: 'f177c44a1d610ba7c907ca3dc892e30e33dde390513139b5607f6a3cfe2d7825',
+ cascadeEdges: '18e5bdda435293145dbb52319f1e842cc8b7cc3e8beb60ec381c26d0410cfe44',
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
- declarations: '9839eb3b27e0af3861942950de7fc8f01bd0ecf637a371803b616984ed3a61b7',
+ declarations: '01152af9f22aa7a7654356f81b8a6110488f9d70754b2517d1f6bafeff4b14fd',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
- keyframes: 'ee0752081f8987d4b3ef09c0cdb5a34ebcc73e2a452976c509360bdaa5ce7e5c',
- ruleDeclarationSequences: '56d2adf0cfd83dc14a798fc79bfed2a026ec658a0989926cb17cf0ad9794ffed',
- selectors: '09042be79ac63f890fc98c78be92ca62ceca37443e4f4b808423243b06379ed8',
+ keyframes: '0bed7ec3cd3a86ee091cf9b07c081ab2364ac5a17e55e95bc9c4a6430419b019',
+ ruleDeclarationSequences: '7ffd551563a8d801bcd5e5f9886b9551940dd9561240f0f8a771a85d44fc19db',
+ selectors: '09962ebb3bc769f74ae785a44f8e032c0a83985ce14ea652135732b5af69042a',
variableReferences: '5988cb6c9b658d380c5b51c04621c0579e1cc5e8abcd764d78a9d42b802252c9',
witnesses: '68d7c48f20f7a8ae2c0a6ddabe743dfb84fd02fba303e3cc9d3b902efd0f8758',
},
@@ -293,8 +293,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-UzWB2BIJ.css']);
+ assert.deepEqual(assets, ['index-BJnEzGgg.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
- assert.equal(built.byteLength, 107898);
- assert.equal(sha256(built), 'df4965f0acfa4cf95e8ffa29c4942ce795d4cb903ad6914d54e61077b6ffd6f9');
+ assert.equal(built.byteLength, 109140);
+ assert.equal(sha256(built), '9e1f7811080177f366b693a77e92c615aff1a74537b34d597100c9707a8aea95');
});