diff --git a/apps/staged/src/app.css b/apps/staged/src/app.css index d57f79c90..f446775db 100644 --- a/apps/staged/src/app.css +++ b/apps/staged/src/app.css @@ -216,7 +216,18 @@ body { background: var(--scrollbar-thumb-hover); } -/* Keep compositor hints limited to tiny glyph and badge surfaces. */ +/* Pin tiny glyph rasters on their own compositor layer so repaints + can't re-snap them (WKWebView subpixel wobble). Every lucide icon + is covered by default; .stable-raster(-glyphs) remain for the few + non-lucide raster surfaces. + + Spinning icons are excluded: Tailwind's spin keyframes only define + `to { rotate(360deg) }`, so the implicit `from` frame would inherit + this translateZ(0) — mismatched transform lists then interpolate via + matrices, where rotate(360deg) is the identity, freezing the spinner. + A continuously rotating icon can't wobble and is layer-promoted while + animating anyway. */ +svg.lucide:not(.animate-spin), .stable-raster, .stable-raster-glyphs svg { transform: translateZ(0); diff --git a/apps/staged/src/lib/components/ui/badge/badge.svelte b/apps/staged/src/lib/components/ui/badge/badge.svelte index 405eda89c..9634f85e5 100644 --- a/apps/staged/src/lib/components/ui/badge/badge.svelte +++ b/apps/staged/src/lib/components/ui/badge/badge.svelte @@ -2,7 +2,7 @@ import { type VariantProps, tv } from 'tailwind-variants'; export const badgeVariants = tv({ - base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-[color,background-color,border-color,box-shadow,opacity] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge stable-raster stable-raster-glyphs inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:ring-[3px] [&>svg]:pointer-events-none', + base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-[color,background-color,border-color,box-shadow,opacity] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge stable-raster inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:ring-[3px] [&>svg]:pointer-events-none', variants: { variant: { default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', diff --git a/apps/staged/src/lib/components/ui/button/button.svelte b/apps/staged/src/lib/components/ui/button/button.svelte index 51098b4ae..27440616e 100644 --- a/apps/staged/src/lib/components/ui/button/button.svelte +++ b/apps/staged/src/lib/components/ui/button/button.svelte @@ -4,7 +4,7 @@ import { type VariantProps, tv } from 'tailwind-variants'; export const buttonVariants = tv({ - base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button stable-raster-glyphs inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-[color,background-color,border-color,box-shadow,opacity] outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", + base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-[color,background-color,border-color,box-shadow,opacity] outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/80', diff --git a/apps/staged/src/lib/features/agents/AgentIcon.svelte b/apps/staged/src/lib/features/agents/AgentIcon.svelte index eb3a683ed..0ae18b3f5 100644 --- a/apps/staged/src/lib/features/agents/AgentIcon.svelte +++ b/apps/staged/src/lib/features/agents/AgentIcon.svelte @@ -16,6 +16,10 @@ monochrome and inherit currentColor. Goose is a PNG tinted via a CSS mask (ported from goose-internal's GooseIcon). + The raw brand marks aren't lucide icons, so the global svg.lucide + compositor pin doesn't reach them — they opt into stable-raster to + avoid WKWebView subpixel wobble. + Props: id - provider id (the join key shared by the picker and Doctor) size - icon dimensions in px (default 16) @@ -40,11 +44,14 @@ {#if id === 'goose'} - {:else if id === 'claude'} {:else if id === 'codex'} {:else if id === 'amp'} {:else if id === 'copilot'} {:else if id === 'cursor'} + {#if badge.icon === 'warning'} {:else}