mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 10:46:51 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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. */}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user