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á.