From 13c4ea951a0d57ca62860856f3a6b72b093f5c04 Mon Sep 17 00:00:00 2001 From: Adam Firestone Date: Tue, 25 Aug 2026 15:01:07 -0500 Subject: [PATCH] fix(web): refine sidebar spinner and backdrop compositing - Use continuous filled half-ring silhouettes for cleaner rounded spinner ends - Remove the sidebar backdrop mask to prevent Chromium compositing leaks --- apps/web/src/components/Sidebar.tsx | 34 ++++++++++------------ apps/web/src/index.css | 44 ++++++++--------------------- 2 files changed, 27 insertions(+), 51 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 9390267f4e03..90782829c87e 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -236,22 +236,21 @@ function JumpHintBadge(props: { label: string }) { // lucide's CircleCheck (r=10, edge 11) sat visibly inside the spinner. // // The spinner is Material's transform-only construction (see .working-spinner -// in index.css): a static track ring, then two half-ring SVGs whose rotation -// inside clipped halves makes the arc grow and reel in without animating any -// paint property. Half ring = full circumference dash, offset by half. -const SPINNER_RING_CIRCUMFERENCE = 2 * Math.PI * 10; - -function SpinnerHalfRing() { +// in index.css): a static track ring, then two filled half-ring paths whose +// rotation inside clipped halves makes the arc grow and reel in without +// animating any paint property. Each path is one continuous silhouette with a +// square clipped-seam end and a rounded exposed end, avoiding both hidden-cap +// leakage and self-overdraw between a stroked path and a separate cap shape. +function SpinnerHalfRing({ roundedEnd }: { roundedEnd: "start" | "end" }) { return ( - ); @@ -271,13 +270,10 @@ function WorkingSpinnerIcon(props: { className?: string }) { - - - - + - + diff --git a/apps/web/src/index.css b/apps/web/src/index.css index b7a0c4d8ea5b..0c8b21791f78 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -406,12 +406,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil var(--app-scrollbar-width) 100%; } -/* Stage-channel art needs a mask and pseudo-element gradient, so keep the - behavior composable without tying it to the global components layer. */ +/* Fade stage-channel art with an overlay rather than a CSS mask. Chromium can + mis-composite the masked blueprint SVG while another sidebar layer is + animating, leaking fragments of the artwork into unrelated tiles. The + overlay reaches the sidebar surface color by 93%, so the mask was visually + redundant as well as an extra composited layer. */ @utility sidebar-stage-backdrop { --stage-fade: var(--sidebar-stage-fade, var(--app-chrome-background)); - mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); - -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); &::after { content: ""; @@ -1904,13 +1905,13 @@ code { } /* Sidebar "Working" spinner: Material's indeterminate ring, transform-only. - The arc's grow-and-reel-in is faked by two half-ring SVGs, one per clipped - half, counter-rotating with ease-in-out inside a stepped-rotation layer - inside a linearly rotating container (MDC's construction and timings). Every - animated property is a transform on an HTML element or a root , so the - compositor drives all of it and the main thread never repaints a frame — - the same bar the stepped status indicators hold themselves to. A stroke-dash - animation would look identical but repaint on the main thread every vsync. */ + The arc's grow-and-reel-in is faked by two filled half-ring SVGs, one per + clipped half, counter-rotating with ease-in-out inside a stepped-rotation + layer inside a linearly rotating container (MDC's construction and timings). + Every animated property is a transform on an HTML element or a root , + so the compositor drives all of it and the main thread never repaints a + frame. A stroke-dash animation would look identical but repaint on the main + thread every vsync. */ .working-spinner { position: relative; display: block; @@ -1974,27 +1975,6 @@ code { animation: working-spinner-right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; } -/* A hairline sliver of ring parked over the 12 o'clock seam where the two - halves meet (the arc always spans it), so antialiasing never shows a gap. */ -.working-spinner__patch { - position: absolute; - top: 0; - left: 46%; - display: block; - width: 8%; - height: 100%; - overflow: hidden; -} - -.working-spinner__patch > svg { - position: absolute; - top: 0; - left: -575%; - width: 1250%; - height: 100%; - transform: rotate(180deg); -} - @keyframes working-spinner-container-rotate { to { transform: rotate(360deg);