From dffc5469d28c869afc04f102e7f26762949307a0 Mon Sep 17 00:00:00 2001
From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com>
Date: Fri, 2 Oct 2026 10:37:49 -0300
Subject: [PATCH] feat(app): idle mascots move again in the sidebar and crew
cards
Since #77 an idle bot's mascot stood still in the lists to save the
repaint of its morphing flame. The owner misses it: the slow flame and
the glances are how an idle bot reads at a glance. Only a sleeping bot
(stopped or paused) stays still now; a hidden window still pauses
everything (paused.css).
---
app/src/features/bots/BotAvatar.tsx | 10 ++++++----
app/src/features/bots/avatar.test.tsx | 4 ++--
app/src/mascot.css | 2 +-
docs/spec.md | 2 +-
4 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/app/src/features/bots/BotAvatar.tsx b/app/src/features/bots/BotAvatar.tsx
index 31bfb639..665ad485 100644
--- a/app/src/features/bots/BotAvatar.tsx
+++ b/app/src/features/bots/BotAvatar.tsx
@@ -209,9 +209,11 @@ function Eye({ eye, side, fill }: { eye: Ellipse; side: "l" | "r"; fill: string
}
/**
- * A bot's mascot in a list (the sidebar, a crew's cards): it moves only
- * while the bot does something. One idle mascot per bot, all moving, cost
- * a repaint every frame even with nothing going on.
+ * A bot's mascot in a list (the sidebar, a crew's cards): it moves while
+ * the bot is awake, idle too, since the owner reads an idle bot by its
+ * slow flame and glances. A sleeping one keeps its look but does not
+ * move: a morphing flame repaints every frame, and a stopped bot has
+ * nothing to show.
*/
export function ListAvatar({
bot,
@@ -223,6 +225,6 @@ export function ListAvatar({
size: number;
}) {
const mood = moodOf(bot, crewPaused);
- const still = mood === "idle" || mood === "sleeping";
+ const still = mood === "sleeping";
return ;
}
diff --git a/app/src/features/bots/avatar.test.tsx b/app/src/features/bots/avatar.test.tsx
index f3e393e7..88d281fc 100644
--- a/app/src/features/bots/avatar.test.tsx
+++ b/app/src/features/bots/avatar.test.tsx
@@ -124,13 +124,13 @@ describe("ListAvatar", () => {
paused: false,
});
- test("a bot at rest keeps its look but does not move; a working one moves", () => {
+ test("a sleeping bot keeps its look but does not move; an idle or working one moves", () => {
const { container, rerender } = render(
,
);
const svg = () => container.querySelector("svg") as SVGElement;
expect(svg().dataset.mood).toBe("idle");
- expect(svg().hasAttribute("data-still")).toBe(true);
+ expect(svg().hasAttribute("data-still")).toBe(false);
rerender();
expect(svg().dataset.mood).toBe("sleeping");
diff --git a/app/src/mascot.css b/app/src/mascot.css
index 2c112b14..0bfc6a46 100644
--- a/app/src/mascot.css
+++ b/app/src/mascot.css
@@ -98,7 +98,7 @@
animation-delay: var(--spark-delay, 0s);
}
- /* In a list, a bot at rest keeps its look (shut eyes, low flame) but
+ /* In a list, a sleeping bot keeps its look (shut eyes, low flame) but
does not move: a morphing clip path repaints every frame, one per
mascot on screen. */
[data-mood][data-still] * {
diff --git a/docs/spec.md b/docs/spec.md
index 1cdab4b2..caf9b380 100644
--- a/docs/spec.md
+++ b/docs/spec.md
@@ -827,7 +827,7 @@ Tamanho: a janela inteira é desenhada numa escala (zoom do webview, `setZoom`),
Movimento: o app se mexe para parecer vivo, sem atrapalhar o trabalho. Animações curtas (150 a 400 ms, saída suave) em CSS, sem biblioteca (`motion.css`), e todas param quando o Windows desliga os efeitos de animação (`prefers-reduced-motion`) ou quando o dono liga "Menos animações" nas Configurações (`motion-less.css` repete as mesmas regras para `:root[data-motion="less"]`, e `reducedMotion()` olha os dois).
-- **Mascote:** cada bot tem um humor que segue o estado, e a chama queima de verdade: o contorno muda de forma quadro a quadro e o sombreado acompanha (`mascot.css`, `mascot-flame.css`). Parado, a chama balança devagar e os olhos dão uma olhada para baixo e para o lado de vez em quando, como nas poses do desenho; trabalhando (`busy`, `launching`), queima forte e solta faíscas pelo alto; esperando o dono (`needs_approval`), inclina rápido e dá pulinhos; no limite de uso, queima baixo, de pálpebras pesadas que caem de vez em quando; pausado ou parado, vira uma brasa pequena e dorme, de olhos fechados em dois arcos. Os olhos piscam fechando de cima e de baixo, cada bot num ritmo, para uma equipe não piscar junta. Mexem-se os mascotes do cabeçalho e da resposta em andamento, e os da barra lateral e dos cartões da crew enquanto o bot faz alguma coisa (trabalha, espera o dono, está no limite de uso); nessas listas, um bot parado ou dormindo mostra o humor sem se mexer (`ListAvatar`), porque cada chama que muda de forma redesenha a tela a cada quadro. O histórico do chat fica parado. O humor soma ao estado, que continua com cor, ícone e texto.
+- **Mascote:** cada bot tem um humor que segue o estado, e a chama queima de verdade: o contorno muda de forma quadro a quadro e o sombreado acompanha (`mascot.css`, `mascot-flame.css`). Parado, a chama balança devagar e os olhos dão uma olhada para baixo e para o lado de vez em quando, como nas poses do desenho; trabalhando (`busy`, `launching`), queima forte e solta faíscas pelo alto; esperando o dono (`needs_approval`), inclina rápido e dá pulinhos; no limite de uso, queima baixo, de pálpebras pesadas que caem de vez em quando; pausado ou parado, vira uma brasa pequena e dorme, de olhos fechados em dois arcos. Os olhos piscam fechando de cima e de baixo, cada bot num ritmo, para uma equipe não piscar junta. Mexem-se os mascotes do cabeçalho e da resposta em andamento, e os da barra lateral e dos cartões da crew enquanto o bot está acordado, parado também: é pela chama devagar e pelas olhadas que o dono vê de relance que ele está livre. Nessas listas, só um bot dormindo mostra o humor sem se mexer (`ListAvatar`), porque cada chama que muda de forma redesenha a tela a cada quadro e um bot desligado não tem o que mostrar. O histórico do chat fica parado. O humor soma ao estado, que continua com cor, ícone e texto.
- **Chegadas:** o que chega enquanto o dono olha entra subindo e aparecendo: mensagens, ferramentas, pedidos, avisos e bots novos na barra lateral. O que já estava lá quando ele abriu o chat (ou o app) aparece parado, e páginas antigas carregadas em cima não se mexem. A resposta já aparece enquanto é escrita, então não anima de novo ao terminar.
- **Trabalhando:** os três pontos pulam em sequência; o texto sendo escrito tem um cursor piscando no fim; a mão de "Precisa de aprovação" acena; um pedido pendente pulsa uma vez ao aparecer. No estado do bot, o ponto de "Disponível" solta dois anéis, um depois do outro, e na linha de "Trabalhando" corre um trecho aceso (`StateIcons.tsx`); com menos movimento, a linha fica inteira acesa.
- **Trocar de bot ou de crew:** o painel principal some rápido e o novo sobe (View Transitions do webview); a barra lateral fica onde está.