All pages

Components

Loading

Two marks say the app is working right now — as opposed to the Skeleton, which stands in for content that hasn't arrived yet. Both stop under prefers-reduced-motion and still read at rest.

Dots

Three dots pulsing on a staggered 1.4s loop — the app's one “working on it” signal: the chat thinking state, the per-item fetch placeholder, and the composer's upload slot all use it. Each dot is shrink-0 so a narrow container cannot squash the circles into ellipses; the container is what has to make room for the 1.13rem row.

import { LoadingDots } from "@/components/base/loading-dots";

<LoadingDots />

Character

The agent's face — the chat's “the agent is still working” mark, at the foot of its reply. thinking, it ponders: the eyes drift up and away with a brow cocked, hold, blink, and look somewhere else. Turned off, it eases back to the drawn pose and stays there, which is how the latest reply keeps it once the turn settles. Frames are tweened only while something moves, and each rewrites the paths rather than transforming them, so a blink keeps the stroke's weight. Colour comes from currentColor, so the caller tones it with a text colour — it wants to sit a shade brighter than the label beside it.

Thinking

import { AiCharacter } from "@/components/ai/character";

<span className="flex items-center gap-2 text-ink-secondary">
<AiCharacter thinking />
Thinking
</span>