Add animated relative last-seen device timestamps
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-08-07 13:48:51 +03:00
parent 44367e0ef3
commit 76b75624b8
5 changed files with 84 additions and 13 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.9.1',
gatewayClient: '0.9.1',
macClient: '0.9.2',
gatewayClient: '0.9.2',
gatewayBackend: '0.9.0',
});
+24 -3
View File
@@ -12,6 +12,15 @@ function Tooltip({ children }) {
return <span className="client-tooltip" role="tooltip">{children}</span>;
}
function TextMorph({ from, to }) {
const anchor = from.length >= to.length ? from : to;
return <span className="client-text-morph" aria-hidden="true">
<span className="client-text-morph-anchor">{anchor}</span>
<span className="client-text-morph-value is-date">{from}</span>
<span className="client-text-morph-value is-relative">{to}</span>
</span>;
}
export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
const [snapshot, setSnapshot] = useState(null);
const [status, setStatus] = useState('idle');
@@ -73,6 +82,17 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
inert={!open ? true : undefined}
>
<div className="client-instructions-sheet client-devices-sheet">
<svg className="client-text-morph-filter" aria-hidden="true">
<defs>
<filter id="client-text-morph-goo">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -8"
/>
</filter>
</defs>
</svg>
<button
ref={closeRef}
className="client-drawer-close"
@@ -178,9 +198,10 @@ export function DevicesPanel({ open, panelRef, closeRef, onClose }) {
</span>}
{device.interface && <span>{device.interface}</span>}
</div>
<span className="client-device-last-seen client-tooltip-anchor" tabIndex="0" aria-label={seen.tooltip}>
<time dateTime={device.lastSeenAt}>{seen.label}</time>
<Tooltip>{seen.tooltip}</Tooltip>
<span className="client-device-last-seen" tabIndex="0">
<time dateTime={device.lastSeenAt} aria-label={seen.tooltip}>
<TextMorph from={seen.label} to={seen.relative} />
</time>
</span>
</div>
{device.manufacturer && <p className="client-device-manufacturer">{device.manufacturer}</p>}
+50 -4
View File
@@ -918,24 +918,69 @@ p {
flex: 0 0 auto;
margin-left: auto;
color: var(--client-text);
cursor: help;
cursor: default;
white-space: nowrap;
}
.client-device-last-seen > .client-tooltip,
.client-device-pin-wrap > .client-tooltip {
right: 0;
left: auto;
transform: translate(0, 2px);
}
.client-device-last-seen:hover > .client-tooltip,
.client-device-last-seen:focus-visible > .client-tooltip,
.client-device-pin-wrap:hover > .client-tooltip,
.client-device-pin-wrap:has(> :focus-visible) > .client-tooltip {
transform: translate(0, 0);
}
.client-text-morph-filter {
position: absolute;
width: 0;
height: 0;
pointer-events: none;
}
.client-text-morph {
display: inline-grid;
filter: url('#client-text-morph-goo');
}
.client-text-morph-anchor,
.client-text-morph-value {
grid-area: 1 / 1;
justify-self: end;
white-space: nowrap;
}
.client-text-morph-anchor {
visibility: hidden;
}
.client-text-morph-value {
will-change: opacity, filter, transform;
transition: opacity 240ms ease, filter 280ms cubic-bezier(0.16, 1, 0.3, 1), transform 280ms cubic-bezier(0.16, 1, 0.3, 1);
}
.client-text-morph-value.is-relative {
opacity: 0;
filter: blur(4px);
transform: scale(0.9);
}
.client-device-last-seen:hover .client-text-morph-value.is-date,
.client-device-last-seen:focus-visible .client-text-morph-value.is-date {
opacity: 0;
filter: blur(4px);
transform: scale(1.08);
}
.client-device-last-seen:hover .client-text-morph-value.is-relative,
.client-device-last-seen:focus-visible .client-text-morph-value.is-relative {
opacity: 1;
filter: blur(0);
transform: scale(1);
}
.client-device-last-seen:focus-visible,
.client-device-identity:focus-visible {
border-radius: 3px;
@@ -3947,6 +3992,7 @@ p {
.client-device-edit,
.client-device-edit svg,
.client-device-edit-wrap,
.client-text-morph-value,
.client-subscription-edit,
.client-subscription-edit::after,
.client-subscription-submit,
+2 -2
View File
@@ -32,7 +32,7 @@ export function formatTime(iso) {
export function formatLastSeen(iso, now = new Date()) {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return { label: "Нет данных", tooltip: "Нет данных" };
if (Number.isNaN(date.getTime())) return { label: "Нет данных", relative: "Нет данных", tooltip: "Нет данных" };
const current = new Date(now);
const day = (value) => Date.UTC(value.getFullYear(), value.getMonth(), value.getDate());
const daysAgo = Math.round((day(current) - day(date)) / 86_400_000);
@@ -62,5 +62,5 @@ export function formatLastSeen(iso, now = new Date()) {
hour: "2-digit",
minute: "2-digit",
});
return { label: `${dateLabel}, ${time}`, tooltip: `${full} (${relative})` };
return { label: `${dateLabel}, ${time}`, relative, tooltip: `${full} (${relative})` };
}