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
2 changes: 1 addition & 1 deletion apps/web/src/components/ConnectionStatusDot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function ConnectionStatusDot({
{pingClassName ? (
<span
className={cn(
"absolute inline-flex h-full w-full animate-ping rounded-full",
"absolute inline-flex h-full w-full animate-status-ping rounded-full",
pingClassName,
)}
/>
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,9 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
/>
}
>
<TerminalIcon className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} />
<TerminalIcon
className={`size-3 ${terminalStatus.pulse ? "animate-status-pulse" : ""}`}
/>
</TooltipTrigger>
<TooltipPopup side="top">{terminalStatus.label}</TooltipPopup>
</Tooltip>
Expand Down Expand Up @@ -2226,7 +2228,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
<span className="absolute inset-0 flex items-center justify-center transition-opacity duration-150 group-hover/project-header:opacity-0">
<span
className={`size-[9px] rounded-full ${projectStatus.dotClass} ${
projectStatus.pulse ? "animate-pulse" : ""
projectStatus.pulse ? "animate-status-pulse" : ""
}`}
/>
</span>
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/components/ThreadStatusIndicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export function ThreadStatusLabel({
>
<span
className={`size-[9px] rounded-full ${status.dotClass} ${
status.pulse ? "animate-pulse" : ""
status.pulse ? "animate-status-pulse" : ""
}`}
/>
</TooltipTrigger>
Expand All @@ -170,7 +170,7 @@ export function ThreadStatusLabel({
>
<span
className={`h-1.5 w-1.5 rounded-full ${status.dotClass} ${
status.pulse ? "animate-pulse" : ""
status.pulse ? "animate-status-pulse" : ""
}`}
/>
<span className="hidden md:inline">{status.label}</span>
Expand Down Expand Up @@ -276,7 +276,9 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma
/>
}
>
<TerminalIcon className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`} />
<TerminalIcon
className={`size-3 ${terminalStatus.pulse ? "animate-status-pulse" : ""}`}
/>
</TooltipTrigger>
<TooltipPopup side="top">{terminalStatus.label}</TooltipPopup>
</Tooltip>
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1055,9 +1055,9 @@ function WorkingTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "workin
<div className="py-0.5 pl-1.5">
<div className="flex items-center gap-2 pt-1 text-[11px] text-muted-foreground/70 tabular-nums">
<span className="inline-flex items-center gap-[3px]">
<span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse" />
<span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:200ms]" />
<span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:400ms]" />
<span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-status-pulse" />
<span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-status-pulse [animation-delay:200ms]" />
<span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-status-pulse [animation-delay:400ms]" />
</span>
<span>
{row.createdAt ? (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/preview/AgentBrowserCursor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function AgentBrowserCursorEvent(props: {
{event.phase === "click" ? (
<span
key={event.sequence}
className="absolute left-0.5 top-0.5 size-4 animate-ping rounded-full bg-primary/25 motion-reduce:animate-none"
className="absolute left-0.5 top-0.5 size-4 animate-status-ping rounded-full bg-primary/25 motion-reduce:animate-none"
/>
) : null}
<MousePointer2
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/preview/PreviewChromeRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ export function PreviewChromeRow({
>
<Camera className={cn(recording && "text-destructive")} />
{recording ? (
<span className="absolute right-0.5 top-0.5 size-1.5 animate-pulse rounded-full bg-destructive" />
<span className="absolute right-0.5 top-0.5 size-1.5 animate-status-pulse rounded-full bg-destructive" />
) : null}
</TooltipTrigger>
<TooltipPopup>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/preview/PreviewLocalServerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function describeServer(server: PreviewableServer): string {
function PulsingDot() {
return (
<span aria-label="Listening" className="relative inline-flex size-2 shrink-0">
<span className="absolute inset-0 animate-ping rounded-full bg-success opacity-60" />
<span className="absolute inset-0 animate-status-ping rounded-full bg-success opacity-60" />
<span className="relative inline-flex size-2 rounded-full bg-success" />
</span>
);
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ function Sidebar({
<SidebarInstanceContext value={instanceContextValue}>
<div
className={cn(
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
"flex h-full w-(--sidebar-width) flex-col bg-sidebar surface-grain text-sidebar-foreground",
className,
)}
data-slot="sidebar"
Expand All @@ -234,7 +234,7 @@ function Sidebar({
<Sheet onOpenChange={setOpenMobile} open={openMobile} {...props}>
<SheetPopup
className={cn(
"w-(--sidebar-width) max-w-none bg-sidebar p-0 text-sidebar-foreground",
"w-(--sidebar-width) max-w-none bg-sidebar surface-grain p-0 text-sidebar-foreground",
className,
)}
data-mobile="true"
Expand Down Expand Up @@ -304,7 +304,7 @@ function Sidebar({
{...props}
>
<div
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm/5"
className="flex h-full w-full flex-col bg-sidebar surface-grain group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm/5"
data-sidebar="sidebar"
data-slot="sidebar-inner"
>
Expand Down Expand Up @@ -622,7 +622,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
className={cn(
"relative flex min-w-0 w-full flex-1 flex-col bg-background",
"relative flex min-w-0 w-full flex-1 flex-col bg-background surface-grain",
"md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ms-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm/5",
className,
)}
Expand Down
89 changes: 76 additions & 13 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@

@theme inline {
--animate-skeleton: skeleton 2s -1s infinite linear;
/* Duty-cycled indicator animations: long opacity holds with short stepped
ramps, so the compositor only produces frames while the value changes
(~20% of the cycle) instead of every vsync. Keep flat holds dominant. */
--animate-status-pulse: status-pulse 2s infinite;
--animate-status-ping: status-ping 2s infinite;
--font-sans:
"DM Sans Variable", "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
sans-serif;
Expand Down Expand Up @@ -78,6 +83,36 @@
background-position: -200% 0;
}
}
@keyframes status-pulse {
0%,
40% {
opacity: 1;
animation-timing-function: steps(6);
}
50%,
90% {
opacity: 0.5;
animation-timing-function: steps(6);
}
100% {
opacity: 1;
}
}
@keyframes status-ping {
/* Burst first (immediate feedback for click ripples), then hold
invisible for the rest of the cycle. Mirrors animate-ping's
75%-scale start. */
0% {
opacity: 0.9;
scale: 0.75;
animation-timing-function: steps(8);
}
40%,
100% {
opacity: 0;
scale: 2;
}
}
}

@layer base {
Expand Down Expand Up @@ -151,11 +186,18 @@
padding-inline-end: calc(env(safe-area-inset-right) + 0.75rem);
}

.chat-composer-glass,
.chat-composer-lower-chrome {
.chat-composer-glass {
background: color-mix(in srgb, var(--card) 20%, transparent);
}

/* Mix from --background (not --card): the lower chrome is a full-width
strip over the main view, and a card-tinted wash reads as a lighter
seam against it when no content is scrolled underneath. The backdrop
blur, not the tint, carries the frosted effect. */
.chat-composer-lower-chrome {
background: color-mix(in srgb, var(--background) 20%, transparent);
}

.chat-composer-glass {
box-shadow:
0 18px 48px -20px rgb(0 0 0 / 28%),
Expand All @@ -167,11 +209,14 @@
backdrop-filter: blur(16px);
}

.dark .chat-composer-glass,
.dark .chat-composer-lower-chrome {
.dark .chat-composer-glass {
background: color-mix(in srgb, var(--card) 45%, transparent);
}

.dark .chat-composer-lower-chrome {
background: color-mix(in srgb, var(--background) 45%, transparent);
}

.dark .chat-composer-glass {
box-shadow:
0 18px 48px -20px rgb(0 0 0 / 60%),
Expand All @@ -192,10 +237,13 @@
}

@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
.chat-composer-glass,
.chat-composer-lower-chrome {
.chat-composer-glass {
background: var(--card);
}

.chat-composer-lower-chrome {
background: var(--background);
}
}
}

Expand All @@ -215,6 +263,15 @@
padding-right: max(env(safe-area-inset-right), 0px);
}

/* Grain texture for chrome surfaces that float above the body (see the
--surface-grain note). Layered over the element's background-color, so it
composes with bg-* utilities. */
@utility surface-grain {
background-image: var(--surface-grain);
background-repeat: repeat;
background-size: 256px 256px;
}

/* Suppress all transitions during theme changes */
.no-transitions,
.no-transitions *,
Expand Down Expand Up @@ -320,13 +377,19 @@ body {
padding-top: max(env(safe-area-inset-top), 0px);
}

body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.035;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
/* App-chrome grain. Baked into each surface's own background (behind
content) rather than a fixed overlay on top: a full-viewport overlay
forces the compositor to re-blend every frame any animation produces,
which multiplied idle GPU cost. The overlay's 0.035 opacity lives in the
SVG rect instead. Surfaces that float over the body (e.g. the inset main
card) must opt in via the surface-grain utility, or they lose the
grain's subtle brightening and stand out against the body. */
:root {
--surface-grain: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E");
}

body {
background-image: var(--surface-grain);
background-repeat: repeat;
background-size: 256px 256px;
}
Expand Down
Loading