Skip to content

Commit 21f2512

Browse files
authored
feat(ui): redesign gateway connection-lost banner as floating pill (#101812)
1 parent 3d53ed7 commit 21f2512

3 files changed

Lines changed: 100 additions & 36 deletions

File tree

docs/web/control-ui.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,9 @@ The terminal is also available as a full-screen, terminal-only document at `/?vi
283283
## Connection loss and reconnect
284284

285285
Once a session is established, a dropped Gateway connection does not log you out. The dashboard
286-
stays visible with an amber "Gateway connection lost — reconnecting…" banner while the client
287-
retries automatically with backoff (800 ms up to 15 s). Live updates and actions pause until the
288-
connection returns; **Retry now** in the banner forces an immediate attempt.
286+
stays visible with a floating amber "Gateway connection lost — Reconnecting…" pill under the top
287+
bar while the client retries automatically with backoff (800 ms up to 15 s). Live updates and
288+
actions pause until the connection returns; **Retry now** in the pill forces an immediate attempt.
289289

290290
The login gate only appears when there is no established session yet (first open, page reload
291291
before connect) or when the Gateway actively rejects the credentials (bad token/password, revoked

ui/src/components/connection-banner.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,18 @@ export type ConnectionBannerProps = {
1313

1414
function renderConnectionBanner(props: ConnectionBannerProps) {
1515
const detail = props.lastError ? redactLoginFailureError(props.lastError) : null;
16+
const hint = t("connection.offlineHint");
1617
return html`
17-
<div class="connection-banner callout warn" role="status" aria-live="polite">
18-
<span class="connection-banner__spinner" aria-hidden="true">${icons.loader}</span>
19-
<span class="connection-banner__text">
20-
<strong>${t("connection.lostTitle")}</strong>
21-
${t("connection.reconnecting")}
22-
<span class="connection-banner__hint" title=${detail ?? ""}
23-
>${t("connection.offlineHint")}</span
24-
>
25-
</span>
26-
<button class="btn btn--sm connection-banner__retry" type="button" @click=${props.onRetry}>
27-
${t("connection.retryNow")}
28-
</button>
18+
<div class="connection-banner" role="status" aria-live="polite">
19+
<div class="connection-banner__pill" title=${detail ? `${hint}\n${detail}` : hint}>
20+
<span class="connection-banner__spinner" aria-hidden="true">${icons.loader}</span>
21+
<strong class="connection-banner__title">${t("connection.lostTitle")}</strong>
22+
<span class="connection-banner__state">${t("connection.reconnecting")}</span>
23+
<span class="connection-banner__sr-hint">${hint}</span>
24+
<button class="btn btn--sm connection-banner__retry" type="button" @click=${props.onRetry}>
25+
${t("connection.retryNow")}
26+
</button>
27+
</div>
2928
</div>
3029
`;
3130
}

ui/src/styles/components.css

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -312,31 +312,69 @@
312312
Connection Banner (gateway offline / reconnecting)
313313
=========================================== */
314314

315+
/* Fixed overlay lane under the topbar: connection flaps must not reflow the
316+
page the way the old in-flow banner did, so the pill floats over content. */
315317
.connection-banner {
316-
position: sticky;
317-
top: 0;
318-
z-index: 11;
318+
position: fixed;
319+
top: calc(var(--shell-topbar-height, 44px) + 10px);
320+
left: var(--shell-nav-width, 0px);
321+
right: 0;
322+
/* Above content, below the mobile nav drawer/backdrop (65+). */
323+
z-index: 50;
319324
display: flex;
320-
align-items: center;
321325
justify-content: center;
326+
padding: 0 16px;
327+
pointer-events: none;
328+
}
329+
330+
.connection-banner__pill {
331+
pointer-events: auto;
332+
display: inline-flex;
333+
align-items: center;
322334
gap: 10px;
323-
flex-wrap: wrap;
324-
margin: 0 calc(-1 * var(--shell-pad)) 0;
325-
border-radius: 0;
326-
border-left: none;
327-
border-right: none;
328-
font-weight: 500;
329-
padding: 10px 16px;
335+
min-width: 0;
336+
max-width: 100%;
337+
padding: 6px 6px 6px 14px;
338+
border-radius: var(--radius-full);
339+
border: 1px solid color-mix(in srgb, var(--warn) 36%, var(--border));
340+
background: color-mix(
341+
in srgb,
342+
var(--warn) 10%,
343+
color-mix(in srgb, var(--bg-elevated) 86%, transparent)
344+
);
345+
backdrop-filter: blur(12px) saturate(1.4);
346+
-webkit-backdrop-filter: blur(12px) saturate(1.4);
347+
box-shadow: var(--shadow-md);
348+
font-size: 13px;
349+
animation: connection-banner-enter 0.25s var(--ease-out);
350+
}
351+
352+
@keyframes connection-banner-enter {
353+
from {
354+
opacity: 0;
355+
transform: translateY(-6px);
356+
}
357+
to {
358+
opacity: 1;
359+
transform: translateY(0);
360+
}
361+
}
362+
363+
@media (prefers-reduced-motion: reduce) {
364+
.connection-banner__pill {
365+
animation: none;
366+
}
330367
}
331368

332369
.connection-banner__spinner {
333370
display: inline-flex;
334371
align-items: center;
372+
color: var(--warn);
335373
}
336374

337375
.connection-banner__spinner svg {
338-
width: 16px;
339-
height: 16px;
376+
width: 14px;
377+
height: 14px;
340378
fill: none;
341379
stroke: currentColor;
342380
stroke-width: 2;
@@ -350,22 +388,49 @@
350388
}
351389
}
352390

353-
.connection-banner__hint {
354-
margin-left: 6px;
355-
font-weight: 400;
356-
opacity: 0.75;
391+
.connection-banner__title {
392+
color: var(--text-strong);
393+
font-weight: 600;
394+
white-space: nowrap;
395+
}
396+
397+
.connection-banner__state {
398+
color: var(--muted);
399+
white-space: nowrap;
400+
}
401+
402+
/* Announced by the role="status" region and mirrored in the pill tooltip;
403+
too long to keep visible in the compact pill. */
404+
.connection-banner__sr-hint {
405+
position: absolute;
406+
width: 1px;
407+
height: 1px;
408+
margin: -1px;
409+
padding: 0;
410+
overflow: hidden;
411+
clip: rect(0 0 0 0);
412+
white-space: nowrap;
357413
}
358414

359415
.connection-banner .connection-banner__retry {
360-
border-color: currentColor;
361-
color: currentColor;
416+
border-radius: var(--radius-full);
417+
border-color: color-mix(in srgb, var(--warn) 42%, var(--border));
418+
background: transparent;
419+
color: var(--text-strong);
362420
font-size: 12px;
363421
padding: 4px 12px;
422+
white-space: nowrap;
364423
}
365424

366425
.connection-banner .connection-banner__retry:hover:not(:disabled) {
367-
border-color: currentColor;
368-
background: color-mix(in srgb, currentColor 14%, transparent);
426+
border-color: color-mix(in srgb, var(--warn) 60%, var(--border));
427+
background: color-mix(in srgb, var(--warn) 14%, transparent);
428+
}
429+
430+
@media (max-width: 640px) {
431+
.connection-banner__state {
432+
display: none;
433+
}
369434
}
370435

371436
/* ===========================================

0 commit comments

Comments
 (0)