fix(ui): restore pixel-grid spinners across the app

This commit is contained in:
usrnk1
2026-10-21 10:22:33 +02:00
Unverified
9 changed files with 27 additions and 135 deletions
-25
View File
@@ -207,31 +207,6 @@
);
}
[data-slot="titlebar-update-loader"] {
display: block;
flex-shrink: 0;
margin: 1px;
width: 12px;
height: 12px;
border-radius: 9999px;
border: 2px solid color-mix(in srgb, var(--v2-icon-icon-accent) 30%, transparent);
border-top-color: var(--v2-icon-icon-accent);
animation: titlebar-update-loader-spin 0.67s linear infinite;
transform-origin: 50% 50%;
}
:root[data-motion="reduce"] {
[data-slot="titlebar-update-loader"] {
animation: none;
}
}
@keyframes titlebar-update-loader-spin {
to {
transform: rotate(360deg);
}
}
@keyframes home-projects-fade-top {
from {
visibility: hidden;
@@ -1,6 +1,6 @@
import { createMemo, For, Show } from "solid-js"
import { syncSpinPhase } from "@opencode/ui/hooks"
import { Icon } from "@opencode/ui/icon"
import { Spinner } from "@opencode/ui/spinner"
import { TitlebarItem } from "@opencode/gui-extensions/sdk"
import { useExtensionHost } from "./host"
@@ -73,7 +73,7 @@ function TitlebarItemButton(props: { item: TitlebarItem; vertical?: boolean }) {
>
<Show
when={!props.item.busy}
fallback={<span ref={syncSpinPhase} data-slot="titlebar-update-loader" aria-hidden="true" />}
fallback={<Spinner class="m-px size-3" aria-hidden="true" />}
>
<Show
when={props.item.icon}
+4
View File
@@ -4,3 +4,7 @@
width: 18px;
aspect-ratio: 1;
}
:root[data-motion="reduce"] [data-slot="spinner-grid"] rect {
animation: none !important;
}
+3 -6
View File
@@ -15,17 +15,14 @@ const squares = Array.from({ length: 16 }, (_, i) => ({
corner: cornerIndices.has(i),
}))
export function Spinner(props: {
class?: string
classList?: ComponentProps<"div">["classList"]
style?: ComponentProps<"div">["style"]
}) {
export function Spinner(props: ComponentProps<"svg"> & { "data-component"?: string }) {
return (
<svg
{...props}
ref={syncSpinPhase}
viewBox="0 0 15 15"
data-component="spinner"
data-component={props["data-component"] ?? "spinner"}
data-slot="spinner-grid"
classList={{
...props.classList,
[props.class ?? ""]: !!props.class,
@@ -2,31 +2,4 @@
display: block;
flex-shrink: 0;
color: var(--v2-icon-icon-muted);
/* The tail fades in to a solid head; the head's round cap still gets full colour just past the arc. */
mask-image: conic-gradient(from 90deg, transparent 0turn, black 0.72turn, black 0.78turn, transparent 0.9turn);
animation: activity-ring-spin 800ms linear infinite;
}
[data-component="activity-ring"] circle {
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
}
@keyframes activity-ring-spin {
to {
transform: rotate(1turn);
}
}
@media (prefers-contrast: more) {
[data-component="activity-ring"] {
mask-image: none;
}
}
:root[data-motion="reduce"] {
[data-component="activity-ring"] {
animation: none;
}
}
@@ -1,26 +1,15 @@
import { splitProps, type ComponentProps } from "solid-js"
import { syncSpinPhase } from "../../hooks/spin-phase"
import { type ComponentProps } from "solid-js"
import { Spinner } from "../../components/spinner"
import "./activity-ring.css"
// An open arc in constant rotation, matching the iOS drawer's busy ring.
export function ActivityRing(props: ComponentProps<"svg">) {
const [local, rest] = splitProps(props, ["class", "classList", "width", "height"])
return (
<svg
{...rest}
ref={syncSpinPhase}
class={local.class}
classList={local.classList}
width={local.width ?? 14}
height={local.height ?? 14}
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
<Spinner
{...props}
width={props.width ?? 14}
height={props.height ?? 14}
data-component="activity-ring"
aria-hidden={rest["aria-hidden"] ?? "true"}
>
<circle cx="7" cy="7" r="6.1" pathLength="100" stroke-dasharray="72 28" />
</svg>
aria-hidden={props["aria-hidden"] ?? "true"}
/>
)
}
@@ -1,32 +1,5 @@
[data-component="loader-v2"] {
--_duration: 900ms;
display: block;
flex-shrink: 0;
color: var(--v2-icon-icon-muted, #808080);
animation: loader-v2-spin var(--_duration) linear infinite;
transform-origin: center;
}
[data-component="loader-v2"] [data-slot="loader-v2-background"] {
stroke: currentColor;
opacity: 0.2;
}
[data-component="loader-v2"] [data-slot="loader-v2-progress"] {
stroke: currentColor;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: center;
}
@keyframes loader-v2-spin {
to {
transform: rotate(360deg);
}
}
:root[data-motion="reduce"] {
[data-component="loader-v2"] {
animation: none;
}
}
@@ -1,13 +1,13 @@
import { Loader } from "./loader"
const docs = `### Overview
Circular v2 loader for compact loading states.
Pixel-grid loader for compact loading states.
### API
- Accepts standard SVG props.
### Behavior
- The foreground ring covers 33% of the circumference and rotates continuously.
- Grid pixels pulse in opacity using the shared spinner animation.
### Accessibility
- Sets \`aria-hidden="true"\` by default.
+8 -27
View File
@@ -1,34 +1,15 @@
import { splitProps, type ComponentProps } from "solid-js"
import { syncSpinPhase } from "../../hooks/spin-phase"
import { type ComponentProps } from "solid-js"
import { Spinner } from "../../components/spinner"
import "./loader.css"
export function Loader(props: ComponentProps<"svg">) {
const [local, rest] = splitProps(props, ["class", "classList", "width", "height"])
return (
<svg
{...rest}
ref={syncSpinPhase}
class={local.class}
classList={local.classList}
width={local.width ?? 16}
height={local.height ?? 16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
<Spinner
{...props}
width={props.width ?? 16}
height={props.height ?? 16}
data-component="loader-v2"
aria-hidden={rest["aria-hidden"] ?? "true"}
>
<circle cx="8" cy="8" r="6" data-slot="loader-v2-background" stroke-width="2" />
<circle
cx="8"
cy="8"
r="6"
data-slot="loader-v2-progress"
pathLength="100"
stroke-width="2"
stroke-dasharray="33 67"
/>
</svg>
aria-hidden={props["aria-hidden"] ?? "true"}
/>
)
}