From c01f2479b94b2214a949ae736a3c5141cc52b5be Mon Sep 17 00:00:00 2001 From: Anso Date: Sun, 12 Jul 2026 22:45:50 -0400 Subject: [PATCH] fix(ui): lower-overhead Reduced effects for constrained GPUs (#1616) * fix(ui): lower-overhead Reduced effects path for constrained GPUs Disable backdrop-filter glass and solidify chrome fills when data-effects is reduced (Calm default), with Appearance warning and docs for constrained rendering devices. Related to #1614. * test(ui): harden Reduced effects glass e2e and docs wording Parse Color Level 4 slash alpha so opacity asserts are not vacuously true, cover dialog/toast/overlay both ways with stable data-sn hooks, and soften troubleshooting copy that overstated the unmeasured diagnosis. * docs(ui): lead constrained-GPU guidance with Reduced Motion Rename the troubleshooting section to neutral GPU framing, point Appearance and docs at Reduced Motion first, and show the callout whenever Motion is off so Calm or Reduced effects alone do not hide the validated workaround. --- docs/features/appearance.mdx | 6 +- docs/operations/troubleshooting.mdx | 17 + docs/reference/settings.mdx | 3 +- e2e/reduced-effects-glass.spec.ts | 319 ++++++++++++++++++ .../src/components/DeployFeedbackPill.tsx | 1 + .../FleetView/ReconnectingOverlay.tsx | 2 +- .../FleetView/UpdateStatusBadge.tsx | 2 +- .../components/GlobalObservabilityView.tsx | 5 +- frontend/src/components/HostConsole.tsx | 2 +- frontend/src/components/MobileTabBar.tsx | 1 + frontend/src/components/NotificationPanel.tsx | 2 +- frontend/src/components/Terminal.tsx | 6 +- frontend/src/components/TopBar.tsx | 1 + .../components/settings/AppearanceSection.tsx | 33 +- .../__tests__/AppearanceSection.test.tsx | 19 ++ .../src/components/sidebar/StackSidebar.tsx | 5 +- frontend/src/components/ui/dialog.tsx | 1 + frontend/src/components/ui/popover.tsx | 1 + frontend/src/components/ui/system-sheet.tsx | 8 +- frontend/src/components/ui/toast.tsx | 1 + frontend/src/index.css | 44 ++- 21 files changed, 447 insertions(+), 32 deletions(-) create mode 100644 e2e/reduced-effects-glass.spec.ts diff --git a/docs/features/appearance.mdx b/docs/features/appearance.mdx index 0ced667c..06354425 100644 --- a/docs/features/appearance.mdx +++ b/docs/features/appearance.mdx @@ -45,9 +45,13 @@ The **Chart palette** sets the colors used on the Security page charts: ## Motion & effects -- **Reduced effects** flattens card bevels, the accent glow, and chart gradients for a calmer, flatter surface. +- **Reduced motion** minimizes interface animations and transitions (dialogs, menus, expand and collapse). Toasts are unaffected. On constrained graphics, this is the first control to try when the active Sencho tab feels heavy on idle. +- When Reduced motion is off, Appearance shows a short notice about constrained graphics and points you at Reduced motion. +- **Reduced effects** flattens card bevels, the accent glow, and chart gradients, and uses a simpler rendering path: glass blur is off and chrome fills are solid. Fresh installs get this through the default **Calm** visual style. Treat it as an optional secondary experiment for material cost; it is not a substitute for Reduced motion on the reported idle-GPU symptom. The impact varies by device, browser, and graphics configuration. - **Ambient glow** sets the intensity of the soft, accent-tinted glow behind the page. Slide it to zero for a completely flat background. (Reduced effects already dims it.) +If the active Sencho tab feels heavy on constrained graphics, see [High GPU usage on constrained graphics](/operations/troubleshooting#high-gpu-usage-on-constrained-graphics). + ## Theme The **Mode** control sets the overall surface palette: diff --git a/docs/operations/troubleshooting.mdx b/docs/operations/troubleshooting.mdx index d6211e13..bf3cfaa6 100644 --- a/docs/operations/troubleshooting.mdx +++ b/docs/operations/troubleshooting.mdx @@ -9,6 +9,23 @@ description: Solutions to the most common Sencho setup and runtime problems. If something has broken and you want a quick "what do I do now?" overview rather than a specific symptom, start with the [Recovery guide](/operations/recovery). It walks through getting back to a working state when Sencho, a deploy, sign-in, Docker, or a remote node fails, and links back here for the details. +## High GPU usage on constrained graphics + +**Symptom:** With Sencho as the active browser tab, GPU use stays high on idle, and can climb further when an update or deploy dialog is open. Other sites in the same browser stay quieter. On some dual-GPU PCs the issue shows up only when the browser is forced onto integrated graphics. + +**What helped on the reported Windows configuration:** Turning on **Reduced motion** dropped idle iGPU use substantially (about 50% to about 7% in that report). **Calm** and **Reduced effects** alone did not. On that machine the issue did not appear when Windows selected the GPU or when Chrome used the discrete GPU. + +**What to try:** + +1. Open **Settings → Appearance → Motion & effects**. +2. Turn on **Reduced motion**. That is the first step to try for high idle GPU use on constrained graphics. +3. Optionally try **Reduced effects** or **Calm** as a secondary experiment (simpler glass and solid chrome). That path is unverified for this symptom. +4. On dual-GPU Windows PCs, you can also set the browser's GPU preference to let the OS decide, or prefer a discrete GPU, if your OS exposes that control. + +**How to help validate a fix on your hardware:** Identify Chrome's GPU process (Shift+Esc), use Task Manager Details at High update speed on that PID's GPU column, leave the Sencho tab in the foreground for about 15 seconds, then record about ten one-second samples and take the median. Repeat for at least three independent runs (close and reopen the tab). Compare idle Home or Settings with Reduced motion off versus on. Separately compare an update or deploy dialog with Raw output hidden and visible. On Linux single-board devices, use one documented GPU or responsiveness counter for the whole matrix, and label CPU-only readings as secondary. + +--- + ## Containers won't start after deploy **Symptom:** You click Deploy and containers immediately exit or never appear. diff --git a/docs/reference/settings.mdx b/docs/reference/settings.mdx index f5bf885a..cd31c88d 100644 --- a/docs/reference/settings.mdx +++ b/docs/reference/settings.mdx @@ -118,7 +118,8 @@ Control how Sencho looks and how dense the workspace feels. Each browser remembe | Control | What it does | |---------|--------------| | **Chart palette** | Colors the Security page charts: **Muted**, **Heat** (one warm ramp), or **Signature** (saturated). | -| **Reduced effects** | Flattens card bevels, the accent glow, and chart gradients. | +| **Reduced motion** | Minimizes interface animations and transitions. First control to try for high idle GPU use on constrained graphics. Toasts are unaffected. | +| **Reduced effects** | Flattens card bevels, the accent glow, and chart gradients, and turns off glass blur with solid chrome fills. Optional secondary material simplification; not a substitute for Reduced motion on the reported idle-GPU symptom. | | **Ambient glow** | Intensity of the accent-tinted page glow. | ### Theme and typography diff --git a/e2e/reduced-effects-glass.spec.ts b/e2e/reduced-effects-glass.spec.ts new file mode 100644 index 00000000..6bf9c5a8 --- /dev/null +++ b/e2e/reduced-effects-glass.spec.ts @@ -0,0 +1,319 @@ +/** + * Reduced-effects glass correctness (not GPU %). + * + * Proves that data-effects="reduced" clears backdrop-filter and solidifies + * chrome/floating fills, and that full-effects still keeps glass. Fresh Calm + * installs already set reduced via theme-init; Signature keeps blur until the + * operator opts into Reduced effects. + */ +import { test, expect, type Page, type Locator } from '@playwright/test'; +import { loginAs, waitForStacksLoaded } from './helpers'; + +const CALM_STATE = { + theme: 'dim', + accent: 'cyan', + borderBoost: 0, + glow: 0.16, + contrast: 0, + uiFont: 'Geist', + monoFont: 'Geist Mono', + typeScale: 1, + visualStyle: 'calm', + headingStyle: 'clean', + chartStyle: 'muted', + reducedEffects: true, + readability: false, + reducedMotion: false, +} as const; + +const SIGNATURE_STATE = { + ...CALM_STATE, + visualStyle: 'signature', + headingStyle: 'signature', + chartStyle: 'signature', + reducedEffects: false, +} as const; + +async function seedAppearance(page: Page, state: Record) { + await page.addInitScript((payload) => { + localStorage.setItem('sencho.appearance.theme', JSON.stringify(payload)); + }, state); +} + +async function computedBackdrop(locator: Locator): Promise { + return locator.evaluate((el) => { + const cs = getComputedStyle(el); + const v = + cs.backdropFilter || + (cs as CSSStyleDeclaration & { webkitBackdropFilter?: string }).webkitBackdropFilter || + 'none'; + return v.trim() || 'none'; + }); +} + +/** + * Parse alpha from getComputedStyle backgroundColor. Handles rgb/rgba, + * space-separated rgb, and Color Level 4 forms that keep a slash alpha + * (oklch / oklab / lab / lch / color / hsl / hwb). Returns 1 only when the + * color is genuinely opaque or unparseable without an alpha channel. + */ +async function backgroundAlpha(locator: Locator): Promise { + return locator.evaluate((el) => { + const bg = getComputedStyle(el).backgroundColor.trim(); + if (!bg || bg === 'transparent') return 0; + + const slash = bg.match(/\/\s*([0-9]*\.?[0-9]+)\s*\)\s*$/); + if (slash) { + const n = Number(slash[1]); + return Number.isFinite(n) ? n : 1; + } + + const rgbaComma = bg.match(/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i); + if (rgbaComma) { + return rgbaComma[4] !== undefined ? Number(rgbaComma[4]) : 1; + } + + const rgbSpace = bg.match(/^rgba?\(\s*([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)(?:\s*\/\s*([0-9.]+))?\s*\)$/i); + if (rgbSpace) { + return rgbSpace[4] !== undefined ? Number(rgbSpace[4]) : 1; + } + + // Modern color functions without a slash alpha are opaque. + if (/^(oklch|oklab|lab|lch|color|hsl|hwb)\(/i.test(bg)) return 1; + return 1; + }); +} + +async function expectNoBlur(locator: Locator) { + const filter = await computedBackdrop(locator); + expect(filter === 'none' || filter === 'none none').toBeTruthy(); +} + +async function expectHasBlur(locator: Locator) { + const filter = await computedBackdrop(locator); + expect(filter === 'none' || filter === 'none none').toBeFalsy(); +} + +async function expectOpaque(locator: Locator) { + expect(await backgroundAlpha(locator)).toBeGreaterThan(0.99); +} + +async function expectTranslucent(locator: Locator) { + expect(await backgroundAlpha(locator)).toBeLessThan(0.99); +} + +async function openAppearance(page: Page) { + await page.getByRole('button', { name: /profile/i }).click(); + await page.getByRole('button', { name: 'Settings', exact: true }).click(); + await page.getByRole('button', { name: 'Appearance', exact: true }).click(); + await expect(page.getByText('Motion & effects')).toBeVisible(); +} + +async function setThemeMode(page: Page, label: RegExp) { + await openAppearance(page); + await page.getByRole('radio', { name: label }).click(); +} + +async function showInfoToast(page: Page) { + await page.evaluate(async () => { + const mod = await import('/src/components/ui/toast-store.ts'); + mod.toast.info('Glass opacity probe', { duration: 60_000 }); + }); + await expect(page.locator('[data-sn-glass="panel"]').filter({ hasText: 'Glass opacity probe' })).toBeVisible(); +} + +async function openCreateStackDialog(page: Page) { + await page.getByRole('button', { name: 'Create Stack' }).click(); + await expect(page.locator('[data-sn-chrome="dialog"]')).toBeVisible(); +} + +test.describe('Reduced-effects glass', () => { + test('cold load with Calm applies data-effects by DOMContentLoaded and clears chrome blur', async ({ page }) => { + await seedAppearance(page, CALM_STATE); + + await page.addInitScript(() => { + document.addEventListener( + 'DOMContentLoaded', + () => { + (window as unknown as { __snEffectsAtDCL?: string | null }).__snEffectsAtDCL = + document.documentElement.getAttribute('data-effects'); + }, + { once: true }, + ); + }); + + await page.goto('/'); + const effectsAtDomContent = await page.evaluate( + () => (window as unknown as { __snEffectsAtDCL?: string | null }).__snEffectsAtDCL ?? null, + ); + expect(effectsAtDomContent).toBe('reduced'); + + await loginAs(page); + await waitForStacksLoaded(page); + + await expect(page.locator('html')).toHaveAttribute('data-effects', 'reduced'); + await expectNoBlur(page.locator('[data-sn-chrome="topbar"]')); + await expectNoBlur(page.locator('[data-sn-chrome="sidebar"]')); + await expectOpaque(page.locator('[data-sn-chrome="topbar"]')); + await expectOpaque(page.locator('[data-sn-chrome="sidebar"]')); + }); + + test('Signature chrome is translucent with blur; Reduced effects reverses both without reload', async ({ page }) => { + await seedAppearance(page, SIGNATURE_STATE); + await loginAs(page); + await waitForStacksLoaded(page); + + const topbar = page.locator('[data-sn-chrome="topbar"]'); + const sidebar = page.locator('[data-sn-chrome="sidebar"]'); + + await expect(page.locator('html')).not.toHaveAttribute('data-effects', 'reduced'); + await expectHasBlur(topbar); + await expectHasBlur(sidebar); + await expectTranslucent(sidebar); + + await openAppearance(page); + await expect(page.getByText('Constrained graphics', { exact: true })).toBeVisible(); + + await page.getByRole('switch', { name: 'Reduced effects' }).click(); + await expect(page.locator('html')).toHaveAttribute('data-effects', 'reduced'); + await expect(page.getByText('Constrained graphics', { exact: true })).toBeVisible(); + + await page.getByRole('switch', { name: 'Reduced motion' }).click(); + await expect(page.locator('html')).toHaveAttribute('data-effects', 'reduced'); + await expect(page.locator('html')).toHaveAttribute('data-motion', 'reduced'); + await expect(page.getByText('Constrained graphics', { exact: true })).toHaveCount(0); + await expectNoBlur(topbar); + await expectNoBlur(sidebar); + await expectOpaque(sidebar); + + await page.getByRole('button', { name: /profile/i }).click(); + const popover = page.locator('[data-sn-chrome="popover"]'); + await expect(popover).toBeVisible(); + await expectNoBlur(popover); + await expectOpaque(popover); + await page.keyboard.press('Escape'); + + await page.getByRole('switch', { name: 'Reduced motion' }).click(); + await expect(page.locator('html')).not.toHaveAttribute('data-motion', 'reduced'); + await expect(page.getByText('Constrained graphics', { exact: true })).toBeVisible(); + + await page.getByRole('switch', { name: 'Reduced effects' }).click(); + await expect(page.locator('html')).not.toHaveAttribute('data-effects', 'reduced'); + await expectHasBlur(topbar); + await expectTranslucent(sidebar); + }); + + test('dialog toast and overlay glass flip with Reduced effects both ways', async ({ page }) => { + await seedAppearance(page, SIGNATURE_STATE); + await loginAs(page); + await waitForStacksLoaded(page); + + // Dialog (Create Stack uses Modal -> DialogContent) + await openCreateStackDialog(page); + const dialog = page.locator('[data-sn-chrome="dialog"]'); + await expectHasBlur(dialog); + // Dialog uses bg-popover; under Signature --pop-a is translucent. + await expectTranslucent(dialog); + await page.keyboard.press('Escape'); + await expect(dialog).toHaveCount(0); + + // Panel toast (data-sn-glass="panel") + await showInfoToast(page); + const toastPanel = page.locator('[data-sn-glass="panel"]').filter({ hasText: 'Glass opacity probe' }); + await expectHasBlur(toastPanel); + await expectTranslucent(toastPanel); + + // Overlay probe: mount the same contract as Terminal overlay chrome so we + // exercise data-sn-glass="overlay" without depending on a live PTY. + await page.evaluate(() => { + const el = document.createElement('div'); + el.setAttribute('data-sn-glass', 'overlay'); + el.setAttribute('data-testid', 'glass-overlay-probe'); + el.className = 'fixed bottom-4 left-4 z-[100] h-10 w-40 bg-background/80 backdrop-blur-sm'; + document.body.appendChild(el); + }); + const overlay = page.getByTestId('glass-overlay-probe'); + await expect(overlay).toBeVisible(); + await expectHasBlur(overlay); + await expectTranslucent(overlay); + + await openAppearance(page); + await page.getByRole('switch', { name: 'Reduced effects' }).click(); + await expect(page.locator('html')).toHaveAttribute('data-effects', 'reduced'); + + await expectNoBlur(toastPanel); + await expectOpaque(toastPanel); + await expectNoBlur(overlay); + await expectOpaque(overlay); + + await openCreateStackDialog(page); + await expectNoBlur(dialog); + await expectOpaque(dialog); + await page.keyboard.press('Escape'); + + // Back to full effects without reload: translucency and blur return. + await page.getByRole('switch', { name: 'Reduced effects' }).click(); + await expect(page.locator('html')).not.toHaveAttribute('data-effects', 'reduced'); + await expectHasBlur(toastPanel); + await expectTranslucent(toastPanel); + await expectHasBlur(overlay); + await expectTranslucent(overlay); + + await openCreateStackDialog(page); + await expectHasBlur(dialog); + await expectTranslucent(dialog); + }); + + test('Dim OLED Light and Auto media prefs keep reduced chrome opaque', async ({ page }) => { + await seedAppearance(page, CALM_STATE); + await loginAs(page); + await waitForStacksLoaded(page); + + for (const mode of [ + { radio: /^Dim$/i, theme: 'dim' }, + { radio: /^OLED$/i, theme: 'oled' }, + { radio: /^Light$/i, theme: 'light' }, + ] as const) { + await setThemeMode(page, mode.radio); + await expect(page.locator('html')).toHaveAttribute('data-theme', mode.theme); + await expect(page.locator('html')).toHaveAttribute('data-effects', 'reduced'); + await expectNoBlur(page.locator('[data-sn-chrome="topbar"]')); + await expectOpaque(page.locator('[data-sn-chrome="sidebar"]')); + } + + await setThemeMode(page, /^Auto$/i); + await page.emulateMedia({ colorScheme: 'dark' }); + await page.waitForTimeout(200); + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dim'); + await expectNoBlur(page.locator('[data-sn-chrome="topbar"]')); + + await page.emulateMedia({ colorScheme: 'light' }); + await page.waitForTimeout(200); + await expect(page.locator('html')).toHaveAttribute('data-theme', 'light'); + await expectNoBlur(page.locator('[data-sn-chrome="topbar"]')); + }); + + test('mobile tab bar clears blur and solidifies under reduced effects', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await seedAppearance(page, CALM_STATE); + await loginAs(page); + await waitForStacksLoaded(page); + + const tabBar = page.locator('[data-sn-glass="mobile-tabbar"]'); + await expect(tabBar).toBeVisible(); + await expectNoBlur(tabBar); + await expectOpaque(tabBar); + }); + + test('Reduced motion hides the constrained-graphics callout', async ({ page }) => { + await seedAppearance(page, SIGNATURE_STATE); + await loginAs(page); + await waitForStacksLoaded(page); + await openAppearance(page); + + await expect(page.getByText('Constrained graphics', { exact: true })).toBeVisible(); + await page.getByRole('switch', { name: 'Reduced motion' }).click(); + await expect(page.locator('html')).toHaveAttribute('data-motion', 'reduced'); + await expect(page.getByText('Constrained graphics', { exact: true })).toHaveCount(0); + }); +}); diff --git a/frontend/src/components/DeployFeedbackPill.tsx b/frontend/src/components/DeployFeedbackPill.tsx index a057dbf8..93bd1161 100644 --- a/frontend/src/components/DeployFeedbackPill.tsx +++ b/frontend/src/components/DeployFeedbackPill.tsx @@ -41,6 +41,7 @@ function DeployFeedbackPillBase({ isVisible, onExpand }: DeployFeedbackPillProps onClick={onExpand} onKeyDown={handleKeyDown} className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 w-[280px] bg-popover/95 backdrop-blur-[10px] backdrop-saturate-[1.15] border border-glass-border rounded-full px-3 py-1.5 shadow-lg cursor-pointer flex items-center gap-2 max-md:bottom-[calc(var(--sn-mobile-tabbar-h)_+_env(safe-area-inset-bottom)_+_0.75rem)]" + data-sn-glass="panel" > diff --git a/frontend/src/components/FleetView/ReconnectingOverlay.tsx b/frontend/src/components/FleetView/ReconnectingOverlay.tsx index 56ca741b..0d07b645 100644 --- a/frontend/src/components/FleetView/ReconnectingOverlay.tsx +++ b/frontend/src/components/FleetView/ReconnectingOverlay.tsx @@ -55,7 +55,7 @@ export function ReconnectingOverlay({ preUpdateStartedAt }: ReconnectingOverlayP }, [timedOut, preUpdateStartedAt]); return ( -
+
{timedOut ? ( <> diff --git a/frontend/src/components/FleetView/UpdateStatusBadge.tsx b/frontend/src/components/FleetView/UpdateStatusBadge.tsx index cc8317ca..27bb1f7d 100644 --- a/frontend/src/components/FleetView/UpdateStatusBadge.tsx +++ b/frontend/src/components/FleetView/UpdateStatusBadge.tsx @@ -53,7 +53,7 @@ export function UpdateStatusBadge({ status, error, onRetry, onDismiss }: UpdateS
-
+

{error}

diff --git a/frontend/src/components/GlobalObservabilityView.tsx b/frontend/src/components/GlobalObservabilityView.tsx index eae65d7d..3317e6ac 100644 --- a/frontend/src/components/GlobalObservabilityView.tsx +++ b/frontend/src/components/GlobalObservabilityView.tsx @@ -559,7 +559,7 @@ export function GlobalObservabilityView({ headerActions }: GlobalObservabilityVi )} {fabOpen ? ( -
+
@@ -579,6 +579,7 @@ export function GlobalObservabilityView({ headerActions }: GlobalObservabilityVi onClick={() => setFabOpen(true)} aria-label="Log actions" className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-glass-border bg-popover/95 text-stat-value shadow-md backdrop-blur-[10px] backdrop-saturate-[1.15]" + data-sn-glass="panel" > {isPaused ? : } @@ -595,7 +596,7 @@ export function GlobalObservabilityView({ headerActions }: GlobalObservabilityVi {pendingCount} new · resume )} -
+
) : ( -
+
)} - diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx index aeba5256..4404012b 100644 --- a/frontend/src/components/TopBar.tsx +++ b/frontend/src/components/TopBar.tsx @@ -47,6 +47,7 @@ export function TopBar({ const centered = !showLabels && navAlign === 'center'; return (
+ {!reducedMotion ? ( + } + title="Constrained graphics" + subtitle="If the active Sencho tab feels heavy on integrated graphics or similar devices, turn on Reduced motion. Reduced effects and Calm alone may not be enough for that idle cost." + /> + ) : null} + + + + - - - - { expect(document.documentElement.dataset.chartStyle).toBe('muted'); }); + it('shows the constrained-graphics callout when Reduced motion is off, and hides it when on', () => { + render(); + expect(screen.getByText('Constrained graphics')).toBeTruthy(); + + // Reduced effects alone must not hide the Motion guidance. + fireEvent.click(screen.getByRole('switch', { name: 'Reduced effects' })); + expect(screen.getByText('Constrained graphics')).toBeTruthy(); + + fireEvent.click(screen.getByRole('switch', { name: 'Reduced motion' })); + expect(screen.queryByText('Constrained graphics', { exact: true })).toBeNull(); + + fireEvent.click(screen.getByRole('switch', { name: 'Reduced motion' })); + expect(screen.getByText('Constrained graphics')).toBeTruthy(); + + // Readability does not enable Motion, so the callout stays. + fireEvent.click(screen.getByRole('switch', { name: 'Readability mode' })); + expect(screen.getByText('Constrained graphics')).toBeTruthy(); + }); + it('readability locks the header + chart controls and disables the glow slider', () => { const { container } = render(); // Baseline: nothing reduced, so no slider is disabled. diff --git a/frontend/src/components/sidebar/StackSidebar.tsx b/frontend/src/components/sidebar/StackSidebar.tsx index aa16180e..8ff88a8d 100644 --- a/frontend/src/components/sidebar/StackSidebar.tsx +++ b/frontend/src/components/sidebar/StackSidebar.tsx @@ -61,7 +61,10 @@ export function StackSidebar(props: StackSidebarProps) { }, []); return ( -
+
{/* On mobile the status masthead leads (it carries the node switcher as its kicker chip), so the in-sidebar brand and node rows are redundant there and hidden to save vertical space. */} diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index 2cb68319..152324b5 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -26,6 +26,7 @@ const DialogContent = React.forwardRef< diff --git a/frontend/src/components/ui/system-sheet.tsx b/frontend/src/components/ui/system-sheet.tsx index b6a9dd91..ab6c5b28 100644 --- a/frontend/src/components/ui/system-sheet.tsx +++ b/frontend/src/components/ui/system-sheet.tsx @@ -153,7 +153,7 @@ interface SheetHeaderBandProps { function SheetHeaderBand({ crumb, name, meta, onDismiss }: SheetHeaderBandProps) { const lastIdx = crumb.length - 1; return ( -
+