Skip to content

Commit ce00a5f

Browse files
authored
Merge pull request #59 from asfires/t3code/sky-blue-rounded-spinner
fix(web): refine sidebar spinner and backdrop compositing
2 parents be0b921 + 13c4ea9 commit ce00a5f

2 files changed

Lines changed: 27 additions & 51 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -236,22 +236,21 @@ function JumpHintBadge(props: { label: string }) {
236236
// lucide's CircleCheck (r=10, edge 11) sat visibly inside the spinner.
237237
//
238238
// The spinner is Material's transform-only construction (see .working-spinner
239-
// in index.css): a static track ring, then two half-ring SVGs whose rotation
240-
// inside clipped halves makes the arc grow and reel in without animating any
241-
// paint property. Half ring = full circumference dash, offset by half.
242-
const SPINNER_RING_CIRCUMFERENCE = 2 * Math.PI * 10;
243-
244-
function SpinnerHalfRing() {
239+
// in index.css): a static track ring, then two filled half-ring paths whose
240+
// rotation inside clipped halves makes the arc grow and reel in without
241+
// animating any paint property. Each path is one continuous silhouette with a
242+
// square clipped-seam end and a rounded exposed end, avoiding both hidden-cap
243+
// leakage and self-overdraw between a stroked path and a separate cap shape.
244+
function SpinnerHalfRing({ roundedEnd }: { roundedEnd: "start" | "end" }) {
245245
return (
246246
<svg aria-hidden xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
247-
<circle
248-
cx="12"
249-
cy="12"
250-
r="10"
251-
stroke="currentColor"
252-
strokeWidth="4"
253-
strokeDasharray={SPINNER_RING_CIRCUMFERENCE}
254-
strokeDashoffset={SPINNER_RING_CIRCUMFERENCE / 2}
247+
<path
248+
d={
249+
roundedEnd === "start"
250+
? "M24 12A12 12 0 0 1 0 12L4 12A8 8 0 0 0 20 12A2 2 0 0 1 24 12Z"
251+
: "M24 12A12 12 0 0 1 0 12A2 2 0 0 1 4 12A8 8 0 0 0 20 12Z"
252+
}
253+
fill="currentColor"
255254
/>
256255
</svg>
257256
);
@@ -271,13 +270,10 @@ function WorkingSpinnerIcon(props: { className?: string }) {
271270
<span className="working-spinner__container">
272271
<span className="working-spinner__layer">
273272
<span className="working-spinner__clipper working-spinner__clipper--left">
274-
<SpinnerHalfRing />
275-
</span>
276-
<span className="working-spinner__patch">
277-
<SpinnerHalfRing />
273+
<SpinnerHalfRing roundedEnd="start" />
278274
</span>
279275
<span className="working-spinner__clipper working-spinner__clipper--right">
280-
<SpinnerHalfRing />
276+
<SpinnerHalfRing roundedEnd="end" />
281277
</span>
282278
</span>
283279
</span>

apps/web/src/index.css

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -406,12 +406,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
406406
var(--app-scrollbar-width) 100%;
407407
}
408408

409-
/* Stage-channel art needs a mask and pseudo-element gradient, so keep the
410-
behavior composable without tying it to the global components layer. */
409+
/* Fade stage-channel art with an overlay rather than a CSS mask. Chromium can
410+
mis-composite the masked blueprint SVG while another sidebar layer is
411+
animating, leaking fragments of the artwork into unrelated tiles. The
412+
overlay reaches the sidebar surface color by 93%, so the mask was visually
413+
redundant as well as an extra composited layer. */
411414
@utility sidebar-stage-backdrop {
412415
--stage-fade: var(--sidebar-stage-fade, var(--app-chrome-background));
413-
mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%);
414-
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%);
415416

416417
&::after {
417418
content: "";
@@ -1904,13 +1905,13 @@ code {
19041905
}
19051906

19061907
/* Sidebar "Working" spinner: Material's indeterminate ring, transform-only.
1907-
The arc's grow-and-reel-in is faked by two half-ring SVGs, one per clipped
1908-
half, counter-rotating with ease-in-out inside a stepped-rotation layer
1909-
inside a linearly rotating container (MDC's construction and timings). Every
1910-
animated property is a transform on an HTML element or a root <svg>, so the
1911-
compositor drives all of it and the main thread never repaints a frame —
1912-
the same bar the stepped status indicators hold themselves to. A stroke-dash
1913-
animation would look identical but repaint on the main thread every vsync. */
1908+
The arc's grow-and-reel-in is faked by two filled half-ring SVGs, one per
1909+
clipped half, counter-rotating with ease-in-out inside a stepped-rotation
1910+
layer inside a linearly rotating container (MDC's construction and timings).
1911+
Every animated property is a transform on an HTML element or a root <svg>,
1912+
so the compositor drives all of it and the main thread never repaints a
1913+
frame. A stroke-dash animation would look identical but repaint on the main
1914+
thread every vsync. */
19141915
.working-spinner {
19151916
position: relative;
19161917
display: block;
@@ -1974,27 +1975,6 @@ code {
19741975
animation: working-spinner-right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;
19751976
}
19761977

1977-
/* A hairline sliver of ring parked over the 12 o'clock seam where the two
1978-
halves meet (the arc always spans it), so antialiasing never shows a gap. */
1979-
.working-spinner__patch {
1980-
position: absolute;
1981-
top: 0;
1982-
left: 46%;
1983-
display: block;
1984-
width: 8%;
1985-
height: 100%;
1986-
overflow: hidden;
1987-
}
1988-
1989-
.working-spinner__patch > svg {
1990-
position: absolute;
1991-
top: 0;
1992-
left: -575%;
1993-
width: 1250%;
1994-
height: 100%;
1995-
transform: rotate(180deg);
1996-
}
1997-
19981978
@keyframes working-spinner-container-rotate {
19991979
to {
20001980
transform: rotate(360deg);

0 commit comments

Comments
 (0)