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
10 changes: 6 additions & 4 deletions app/src/features/bots/BotAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 <BotAvatar color={bot.color} size={size} mood={mood} still={still} />;
}
4 changes: 2 additions & 2 deletions app/src/features/bots/avatar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ListAvatar bot={bot("idle")} crewPaused={false} size={32} />,
);
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(<ListAvatar bot={bot("offline")} crewPaused={false} size={32} />);
expect(svg().dataset.mood).toBe("sleeping");
Expand Down
2 changes: 1 addition & 1 deletion app/src/mascot.css
Original file line number Diff line number Diff line change
Expand Up @@ -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] * {
Expand Down
2 changes: 1 addition & 1 deletion docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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á.
Expand Down
Loading