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.
This commit is contained in:
Anso
2026-07-12 22:45:50 -04:00
committed by GitHub
parent fb178871ad
commit c01f2479b9
21 changed files with 447 additions and 32 deletions
+5 -1
View File
@@ -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:
+17
View File
@@ -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.
+2 -1
View File
@@ -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
+319
View File
@@ -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<string, unknown>) {
await page.addInitScript((payload) => {
localStorage.setItem('sencho.appearance.theme', JSON.stringify(payload));
}, state);
}
async function computedBackdrop(locator: Locator): Promise<string> {
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<number> {
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);
});
});
@@ -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"
>
<span className={dotClass} />
<span className={cn(textClass, 'flex items-center gap-1 min-w-0 flex-1 overflow-hidden')}>
@@ -55,7 +55,7 @@ export function ReconnectingOverlay({ preUpdateStartedAt }: ReconnectingOverlayP
}, [timedOut, preUpdateStartedAt]);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-[10px] backdrop-saturate-[1.15]">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-[10px] backdrop-saturate-[1.15]" data-sn-glass="overlay">
<div className="text-center space-y-4">
{timedOut ? (
<>
@@ -53,7 +53,7 @@ export function UpdateStatusBadge({ status, error, onRetry, onDismiss }: UpdateS
<div className="h-2 w-2 rounded-full bg-destructive/60" />
</Cursor>
<CursorFollow side="bottom" sideOffset={8} align="end">
<div className="bg-popover/95 backdrop-blur-[10px] backdrop-saturate-[1.15] border border-card-border shadow-md rounded-lg px-3 py-2 max-w-xs">
<div className="bg-popover/95 backdrop-blur-[10px] backdrop-saturate-[1.15] border border-card-border shadow-md rounded-lg px-3 py-2 max-w-xs" data-sn-glass="panel">
<p className="font-mono tabular-nums text-xs text-stat-subtitle">{error}</p>
</div>
</CursorFollow>
@@ -559,7 +559,7 @@ export function GlobalObservabilityView({ headerActions }: GlobalObservabilityVi
</button>
)}
{fabOpen ? (
<div className="pointer-events-auto flex items-center gap-1 rounded-full border border-glass-border bg-popover/95 p-1 shadow-md backdrop-blur-[10px] backdrop-saturate-[1.15]">
<div className="pointer-events-auto flex items-center gap-1 rounded-full border border-glass-border bg-popover/95 p-1 shadow-md backdrop-blur-[10px] backdrop-saturate-[1.15]" data-sn-glass="panel">
<Button variant="ghost" size="icon" className="h-10 w-10" onClick={() => { setIsPaused(p => !p); setFabOpen(false); }} aria-label={isPaused ? 'Resume stream' : 'Pause stream'}>
{isPaused ? <Play className="h-4 w-4" strokeWidth={1.5} /> : <Pause className="h-4 w-4" strokeWidth={1.5} />}
</Button>
@@ -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 ? <Play className="h-5 w-5" strokeWidth={1.5} /> : <SlidersHorizontal className="h-5 w-5" strokeWidth={1.5} />}
</button>
@@ -595,7 +596,7 @@ export function GlobalObservabilityView({ headerActions }: GlobalObservabilityVi
{pendingCount} new · resume
</button>
)}
<div className="pointer-events-auto flex items-center gap-1 rounded-md border border-glass-border bg-popover/95 p-1 shadow-md backdrop-blur-[10px] backdrop-saturate-[1.15]">
<div className="pointer-events-auto flex items-center gap-1 rounded-md border border-glass-border bg-popover/95 p-1 shadow-md backdrop-blur-[10px] backdrop-saturate-[1.15]" data-sn-glass="panel">
<Button
variant="ghost"
size="sm"
+1 -1
View File
@@ -275,7 +275,7 @@ export default function HostConsole({ stackName, onClose }: HostConsoleProps) {
<div ref={terminalRef} style={{ width: '100%', height: '100%' }} />
<div className="pointer-events-none absolute bottom-4 right-6 z-10 flex items-center gap-2">
<div className="pointer-events-auto flex items-center gap-1 rounded-md border border-glass-border bg-popover/95 p-1 shadow-md backdrop-blur-[10px] backdrop-saturate-[1.15]">
<div className="pointer-events-auto flex items-center gap-1 rounded-md border border-glass-border bg-popover/95 p-1 shadow-md backdrop-blur-[10px] backdrop-saturate-[1.15]" data-sn-glass="panel">
<Button
variant="ghost"
size="sm"
+1
View File
@@ -77,6 +77,7 @@ export function MobileTabBar({
return (
<nav
aria-label="Primary mobile"
data-sn-glass="mobile-tabbar"
className={cn(
'md:hidden flex shrink-0 items-stretch',
'border-t border-hairline',
@@ -366,7 +366,7 @@ export function NotificationPanel({
<div className="max-h-[480px] overflow-y-auto border-t border-card-border/60">
{groups.map((group) => (
<div key={group.label}>
<div className="sticky top-0 z-10 border-b border-card-border/40 bg-popover/95 px-[var(--density-row-x)] py-[var(--density-cell-y)] font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle backdrop-blur-[10px] backdrop-saturate-[1.15]">
<div className="sticky top-0 z-10 border-b border-card-border/40 bg-popover/95 px-[var(--density-row-x)] py-[var(--density-cell-y)] font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle backdrop-blur-[10px] backdrop-saturate-[1.15]" data-sn-glass="panel">
{group.label}
</div>
{group.items.map((notif) => (
+3 -3
View File
@@ -269,12 +269,12 @@ export default function TerminalComponent({ stackName, nodeId, deploySessionId,
<Button variant="outline" size="sm" onClick={() => {
setIsSearchVisible(true);
setTimeout(() => searchInputRef.current?.focus(), 50);
}} className="h-8 bg-background/80 backdrop-blur-sm shadow-sm" title="Search (Ctrl+F)">
}} className="h-8 bg-background/80 backdrop-blur-sm shadow-sm" data-sn-glass="overlay" title="Search (Ctrl+F)">
<Search className="w-4 h-4 mr-2" />
Search
</Button>
) : (
<div className="flex flex-row items-center p-1 pr-1 bg-background/95 backdrop-blur-sm border border-border shadow-md rounded-md">
<div className="flex flex-row items-center p-1 pr-1 bg-background/95 backdrop-blur-sm border border-border shadow-md rounded-md" data-sn-glass="overlay">
<Input
ref={searchInputRef}
value={searchText}
@@ -296,7 +296,7 @@ export default function TerminalComponent({ stackName, nodeId, deploySessionId,
</div>
)}
<Button variant="outline" size="sm" onClick={handleDownload} className="h-8 bg-background/80 backdrop-blur-sm shadow-sm" title="Download Logs">
<Button variant="outline" size="sm" onClick={handleDownload} className="h-8 bg-background/80 backdrop-blur-sm shadow-sm" data-sn-glass="overlay" title="Download Logs">
<Download className="w-4 h-4 mr-2" />
Download
</Button>
+1
View File
@@ -47,6 +47,7 @@ export function TopBar({
const centered = !showLabels && navAlign === 'center';
return (
<div
data-sn-chrome="topbar"
className={cn(
'relative flex h-14 items-center gap-3 px-4',
'border-b border-glass-border bg-sidebar backdrop-blur-md',
@@ -20,6 +20,7 @@ import { UI_FONT_OPTIONS, MONO_FONT_OPTIONS } from '@/components/theme/typeOptio
import { SettingsSection } from './SettingsSection';
import { SettingsField } from './SettingsField';
import { SettingsActions, SettingsSecondaryButton } from './SettingsActions';
import { SettingsCallout } from './SettingsCallout';
const DENSITY_OPTIONS: { value: Density; label: string }[] = [
{ value: 'comfortable', label: 'Comfortable' },
@@ -240,9 +241,28 @@ export function AppearanceSection() {
</SettingsSection>
<SettingsSection title="Motion & effects" kicker="this browser">
{!reducedMotion ? (
<SettingsCallout
tone="warn"
icon={<Info className="h-4 w-4" strokeWidth={1.5} />}
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}
<SettingsField
label="Reduced motion"
helper="Minimizes interface animations and transitions (dialogs, menus, expand and collapse). First control to try for high idle GPU use on constrained graphics. Toasts are unaffected."
>
<TogglePill
checked={reducedMotion}
onChange={setReducedMotion}
aria-label="Reduced motion"
/>
</SettingsField>
<SettingsField
label="Reduced effects"
helper="Flattens card bevels, the accent glow, and chart gradients for a calmer surface."
helper="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 reported idle GPU cost."
>
<TogglePill
checked={effectiveReduced}
@@ -252,17 +272,6 @@ export function AppearanceSection() {
/>
</SettingsField>
<SettingsField
label="Reduced motion"
helper="Minimizes interface animations and transitions (dialogs, menus, expand and collapse). Toasts are unaffected."
>
<TogglePill
checked={reducedMotion}
onChange={setReducedMotion}
aria-label="Reduced motion"
/>
</SettingsField>
<SettingsField
label="Ambient glow"
helper="Intensity of the accent-tinted glow behind the page."
@@ -35,6 +35,25 @@ describe('AppearanceSection', () => {
expect(document.documentElement.dataset.chartStyle).toBe('muted');
});
it('shows the constrained-graphics callout when Reduced motion is off, and hides it when on', () => {
render(<AppearanceSection />);
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(<AppearanceSection />);
// Baseline: nothing reduced, so no slider is disabled.
@@ -61,7 +61,10 @@ export function StackSidebar(props: StackSidebarProps) {
}, []);
return (
<div className="w-64 max-md:w-full max-md:flex-1 max-md:min-h-0 max-md:border-r-0 border-r border-glass-border bg-sidebar backdrop-blur-md flex flex-col">
<div
data-sn-chrome="sidebar"
className="w-64 max-md:w-full max-md:flex-1 max-md:min-h-0 max-md:border-r-0 border-r border-glass-border bg-sidebar backdrop-blur-md flex flex-col"
>
{/* 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. */}
+1
View File
@@ -26,6 +26,7 @@ const DialogContent = React.forwardRef<
<AnimateDialogOverlay className="fixed inset-0 z-50 bg-[var(--scrim)] backdrop-blur-sm" />
<AnimateDialogContent
ref={ref}
data-sn-chrome="dialog"
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-glass-border bg-popover p-6 shadow-lg backdrop-blur-[10px] backdrop-saturate-[1.15] sm:rounded-lg',
panelGlow && 'panel-glow',
+1
View File
@@ -22,6 +22,7 @@ const PopoverContent = React.forwardRef<
"z-50 w-72 rounded-md border border-glass-border bg-popover p-4 text-popover-foreground shadow-md backdrop-blur-[10px] backdrop-saturate-[1.15] outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
className
)}
data-sn-chrome="popover"
{...props}
/>
</PopoverPrimitive.Portal>
+4 -4
View File
@@ -153,7 +153,7 @@ interface SheetHeaderBandProps {
function SheetHeaderBand({ crumb, name, meta, onDismiss }: SheetHeaderBandProps) {
const lastIdx = crumb.length - 1;
return (
<div className="relative bg-popover/95 backdrop-blur-md border-b border-card-border/60 px-6 pt-5 pb-4 pr-14">
<div className="relative bg-popover/95 backdrop-blur-md border-b border-card-border/60 px-6 pt-5 pb-4 pr-14" data-sn-glass="panel">
<span aria-hidden className="absolute inset-y-0 left-0 w-[2px] bg-brand" />
<nav aria-label="Sheet location" className={cn(CRUMB_CLASS, 'flex items-center gap-1.5 leading-none')}>
@@ -221,7 +221,7 @@ interface ToolbarBandProps {
function ToolbarBand({ primary, secondaries, destructive }: ToolbarBandProps) {
return (
<div className="bg-popover/95 backdrop-blur-md flex items-center gap-2 border-b border-card-border/60 px-6 py-3">
<div className="bg-popover/95 backdrop-blur-md flex items-center gap-2 border-b border-card-border/60 px-6 py-3" data-sn-glass="panel">
{primary && (
<Button size="sm" onClick={primary.onClick} disabled={primary.disabled} className="gap-1.5">
{primary.icon && <primary.icon className="h-3.5 w-3.5" strokeWidth={1.5} />}
@@ -266,7 +266,7 @@ interface TabsBandProps {
function TabsBand({ tabs, activeTab, onTabChange }: TabsBandProps) {
return (
<div role="tablist" className="bg-popover/95 backdrop-blur-md flex items-stretch gap-0 border-b border-card-border/60 px-4">
<div role="tablist" className="bg-popover/95 backdrop-blur-md flex items-stretch gap-0 border-b border-card-border/60 px-4" data-sn-glass="panel">
{tabs.map((tab) => {
const isActive = tab.id === activeTab;
return (
@@ -305,7 +305,7 @@ function TabsBand({ tabs, activeTab, onTabChange }: TabsBandProps) {
function FooterBand({ context }: { context: React.ReactNode }) {
return (
<div className="bg-popover/95 backdrop-blur-md border-t border-card-border/60 px-6 py-3">
<div className="bg-popover/95 backdrop-blur-md border-t border-card-border/60 px-6 py-3" data-sn-glass="panel">
<div className={cn(KICKER_CLASS, 'text-stat-subtitle leading-none')}>{context}</div>
</div>
);
+1
View File
@@ -126,6 +126,7 @@ const ToastItem = memo(function ToastItem({
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.3 }}
className="pointer-events-auto relative w-full max-w-sm overflow-hidden rounded-md border border-glass-border bg-popover/95 ring-1 ring-glass-border drop-shadow-xl backdrop-blur-[10px] backdrop-saturate-[1.15]"
data-sn-glass="panel"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
role={type === 'error' ? 'alert' : 'status'}
+40 -4
View File
@@ -436,12 +436,48 @@
--sev-vuln: oklch(0.660 0.100 45);
}
/* Reduced effects: flatten the material (Calm only). Charts read the reduced
flag from the store; here we flatten the real card top-bevel token. */
[data-effects="reduced"] {
/* Reduced effects: lower-overhead rendering path (Calm / Reduced effects /
Readability). Flatten material, drop glass blur, and solidify translucent
chrome so constrained GPUs are not forced to sample backdrop-filter.
data-theme and data-effects both live on <html>, so token overrides use
combined selectors. Exceptions to the blur kill need a more-specific
!important rule, not a normal opt-out class.
Charts also read the reduced flag from the store. */
html[data-effects="reduced"] {
--card-bevel: none;
--pop-a: 1;
/* Dim (default theme when data-theme is dim or unset on first paint) */
--sidebar: oklch(var(--bg) 0 0);
}
[data-effects="reduced"] .animate-pulse {
html[data-theme="oled"][data-effects="reduced"] {
--sidebar: oklch(var(--bg) 0 0);
}
html[data-theme="light"][data-effects="reduced"] {
--sidebar: oklch(0.98 0 0);
}
/* Both properties: Chromium uses backdrop-filter; the WebKit-prefixed form
still appears in some engines. */
html[data-effects="reduced"] *,
html[data-effects="reduced"] *::before,
html[data-effects="reduced"] *::after {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
/* Direct-alpha glass surfaces (bg-popover/95, color-mix tab bar, etc.) stay
translucent after --pop-a: 1. Mark them with data-sn-glass and solidify here. */
html[data-effects="reduced"] [data-sn-glass="panel"] {
background-color: var(--popover);
}
html[data-effects="reduced"] [data-sn-glass="overlay"] {
background-color: var(--background);
}
html[data-effects="reduced"] [data-sn-glass="mobile-tabbar"] {
background-color: var(--card);
}
html[data-effects="reduced"] .animate-pulse {
animation: none;
}