Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 15 additions & 19 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<svg aria-hidden xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
strokeDasharray={SPINNER_RING_CIRCUMFERENCE}
strokeDashoffset={SPINNER_RING_CIRCUMFERENCE / 2}
<path
d={
roundedEnd === "start"
? "M24 12A12 12 0 0 1 0 12L4 12A8 8 0 0 0 20 12A2 2 0 0 1 24 12Z"
: "M24 12A12 12 0 0 1 0 12A2 2 0 0 1 4 12A8 8 0 0 0 20 12Z"
}
fill="currentColor"
/>
</svg>
);
Expand All @@ -271,13 +270,10 @@ function WorkingSpinnerIcon(props: { className?: string }) {
<span className="working-spinner__container">
<span className="working-spinner__layer">
<span className="working-spinner__clipper working-spinner__clipper--left">
<SpinnerHalfRing />
</span>
<span className="working-spinner__patch">
<SpinnerHalfRing />
<SpinnerHalfRing roundedEnd="start" />
</span>
<span className="working-spinner__clipper working-spinner__clipper--right">
<SpinnerHalfRing />
<SpinnerHalfRing roundedEnd="end" />
</span>
</span>
</span>
Expand Down
44 changes: 12 additions & 32 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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: "";
Expand Down Expand Up @@ -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 <svg>, 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 <svg>,
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;
Expand Down Expand Up @@ -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);
Expand Down
Loading