Skip to content

Commit ea75ba2

Browse files
authored
Merge pull request #141 from httpsphl/feat/hide-idle-state
feat(bots): hide the idle state, fade the badge in
2 parents f1121fa + f80595e commit ea75ba2

5 files changed

Lines changed: 37 additions & 59 deletions

File tree

‎app/src/features/bots/BotStateBadge.tsx‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ import {
88
Power,
99
RotateCw,
1010
} from "lucide-react";
11+
import { useState } from "react";
1112
import { type Messages, t, useT } from "../../i18n";
1213
import type { Bot, BotState } from "../../lib/protocol.gen";
1314
import { TONES, type Tone as ToneName } from "../../ui/tone";
14-
import { ReadyIcon, type StateIcon, WorkingIcon } from "./StateIcons";
15+
import { type StateIcon, WorkingIcon } from "./StateIcons";
1516

1617
type Tone = Exclude<ToneName, "accent">;
1718

1819
export interface StateView {
1920
label: string;
2021
tone: Tone;
21-
icon: StateIcon;
22+
/** None for idle: a bot doing nothing is free, which needs no badge. */
23+
icon: StateIcon | null;
2224
spin?: boolean;
2325
/** One sentence on what the state means for the owner. */
2426
hint: string;
@@ -30,7 +32,7 @@ type Look = Omit<StateView, "label" | "hint">;
3032
const looks: Record<BotState, Look> = {
3133
offline: { tone: "quiet", icon: Power },
3234
launching: { tone: "work", icon: LoaderCircle, spin: true },
33-
idle: { tone: "ok", icon: ReadyIcon },
35+
idle: { tone: "ok", icon: null },
3436
busy: { tone: "work", icon: WorkingIcon },
3537
needs_approval: { tone: "warn", icon: Hand },
3638
rate_limited: { tone: "warn", icon: Hourglass },
@@ -70,10 +72,19 @@ export function BotStateBadge({
7072
const messages = useT();
7173
const view = stateView(bot, crewPaused, messages);
7274
const Icon = view.icon;
75+
// The badge fades in when a bot leaves idle in front of the owner; one
76+
// that was already there when the list opened shows still.
77+
const [wasShown, setWasShown] = useState(Icon !== null);
78+
const [entering, setEntering] = useState(false);
79+
if ((Icon !== null) !== wasShown) {
80+
setWasShown(Icon !== null);
81+
setEntering(Icon !== null);
82+
}
83+
if (!Icon) return null;
7384
return (
7485
<span
7586
title={view.hint}
76-
className={`inline-flex shrink-0 items-center gap-1 font-medium transition-colors duration-300 ${compact ? "text-xs" : "text-sm"} ${TONES[view.tone].text}`}
87+
className={`inline-flex shrink-0 items-center gap-1 font-medium transition-colors duration-300 ${compact ? "text-xs" : "text-sm"} ${TONES[view.tone].text} ${entering ? "animate-fade" : ""}`}
7788
>
7889
<Icon
7990
aria-hidden

‎app/src/features/bots/StateIcons.tsx‎

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
// Two state icons that move (motion.css), where a still lucide icon would
2-
// say less: a ready bot pulses, a working one runs a line. Both keep the
3-
// lucide look (24 grid, 2px stroke) so they sit with the other states.
1+
// A state icon that moves (motion.css), where a still lucide icon would
2+
// say less: a working bot runs a line. It keeps the lucide look (24 grid,
3+
// 2px stroke) so it sits with the other states.
44

55
import type { ComponentType } from "react";
66

@@ -13,26 +13,6 @@ export interface StateIconProps {
1313
/** A state's icon: one of these, or any lucide icon. */
1414
export type StateIcon = ComponentType<StateIconProps>;
1515

16-
/** "Ready": a dot with two rings spreading from it, one after the other. */
17-
export function ReadyIcon({ size = 14, className = "" }: StateIconProps) {
18-
return (
19-
<svg
20-
aria-hidden
21-
width={size}
22-
height={size}
23-
viewBox="0 0 24 24"
24-
fill="none"
25-
stroke="currentColor"
26-
strokeWidth={2}
27-
className={className}
28-
>
29-
<circle className="state-ring" cx="12" cy="12" r="5" />
30-
<circle className="state-ring state-ring-late" cx="12" cy="12" r="5" />
31-
<circle cx="12" cy="12" r="5" fill="currentColor" stroke="none" />
32-
</svg>
33-
);
34-
}
35-
3616
// The line of lucide's Activity, the icon this state had before it moved.
3717
const PULSE =
3818
"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2";

‎app/src/features/bots/avatar.test.tsx‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,26 @@ describe("mascot", () => {
106106
});
107107

108108
describe("state badge", () => {
109-
test("ready pulses and working runs its line", () => {
109+
test("idle shows nothing and working runs its line", () => {
110110
const { container, rerender } = render(
111111
<BotStateBadge bot={{ state: "idle", paused: false }} />,
112112
);
113-
expect(container.querySelectorAll(".state-ring")).toHaveLength(2);
113+
expect(container.textContent).toBe("");
114114

115115
rerender(<BotStateBadge bot={{ state: "busy", paused: false }} />);
116116
expect(container.querySelector(".state-trace")?.getAttribute("pathLength")).toBe("100");
117117
});
118+
119+
test("fades in when the bot starts working, not when it was already working", () => {
120+
const { container, rerender } = render(
121+
<BotStateBadge bot={{ state: "busy", paused: false }} />,
122+
);
123+
expect(container.querySelector(".animate-fade")).toBeNull();
124+
125+
rerender(<BotStateBadge bot={{ state: "idle", paused: false }} />);
126+
rerender(<BotStateBadge bot={{ state: "busy", paused: false }} />);
127+
expect(container.querySelector(".animate-fade")).not.toBeNull();
128+
});
118129
});
119130

120131
describe("ListAvatar", () => {

‎app/src/motion.css‎

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -111,25 +111,12 @@
111111
}
112112

113113
@layer components {
114-
/* The state icons of "Ready" and "Working" (features/bots/StateIcons.tsx):
115-
two rings spread from the dot, one after the other; a bright stretch
116-
runs along the pulse line. The mascot has its own file (mascot.css). */
117-
.state-ring,
114+
/* The state icon of "Working" (features/bots/StateIcons.tsx): a bright
115+
stretch runs along the pulse line. The mascot has its own file
116+
(mascot.css). */
118117
.state-trace {
119118
transform-box: fill-box;
120119
transform-origin: 50%;
121-
}
122-
123-
.state-ring {
124-
opacity: 0;
125-
animation: ring 2.6s var(--ease-out) infinite;
126-
}
127-
128-
.state-ring-late {
129-
animation-delay: 1.3s;
130-
}
131-
132-
.state-trace {
133120
stroke-dasharray: 26 74;
134121
animation: trace 1.5s linear infinite;
135122
}
@@ -162,17 +149,6 @@
162149
}
163150
}
164151

165-
@keyframes ring {
166-
from {
167-
opacity: 0.6;
168-
transform: scale(1);
169-
}
170-
to {
171-
opacity: 0;
172-
transform: scale(2);
173-
}
174-
}
175-
176152
@keyframes trace {
177153
to {
178154
stroke-dashoffset: -100px;

0 commit comments

Comments
 (0)