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
@@ -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'}