feat(sagi): flesh out mood animations and add avatar interaction motion
Gives watching/thinking real animations (previously static), adds character to happy/worried/stuck moods, and adds mount-in/press/error-pulse motion to the avatar button - all gated by prefers-reduced-motion. fix(test): pin NODE_ENV=production in client-dist-override.test.js The suite exercises server/index.js's production-only static-serving gate but never set NODE_ENV itself, so it silently depended on the ambient shell's NODE_ENV - failing whenever that wasn't "production".
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── watching ── (alert, tail/ear motion handled below via whole-mascot transform) */
|
||||
/* ── watching ── (attentive sway handled below via whole-mascot transform) */
|
||||
|
||||
/* ── worried ── (whole-mascot shake handled below) */
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── thinking ── head tilt handled below */
|
||||
/* ── thinking ── head-tilt rock handled below */
|
||||
|
||||
/* ── sleeping / disconnected ── (eyes shut via static scaleY, no cursor tracking) */
|
||||
.sagi-avatar[data-mood="sleeping"] .sagi-eye-blink,
|
||||
@@ -95,22 +95,34 @@
|
||||
@keyframes sagi-shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-2px);
|
||||
20% {
|
||||
transform: translateX(-3px) scale(1.01);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(2px);
|
||||
40% {
|
||||
transform: translateX(3px) scale(0.99);
|
||||
}
|
||||
60% {
|
||||
transform: translateX(-2px) scale(1.01);
|
||||
}
|
||||
80% {
|
||||
transform: translateX(2px) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes sagi-bob {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-4px) rotate(-4deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-3px);
|
||||
transform: translateY(-6px) rotate(0deg);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-4px) rotate(4deg);
|
||||
}
|
||||
}
|
||||
@keyframes sagi-sparkle-twinkle {
|
||||
@@ -124,6 +136,55 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes sagi-sway {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(-2.5deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(2.5deg);
|
||||
}
|
||||
}
|
||||
@keyframes sagi-rock {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-6deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(-11deg);
|
||||
}
|
||||
}
|
||||
@keyframes sagi-droop {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(0.985) translateY(2px);
|
||||
}
|
||||
}
|
||||
@keyframes sagi-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.6) translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes sagi-error-pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* idle: gentle breathing + occasional blink */
|
||||
.sagi-avatar[data-mood="idle"] {
|
||||
@@ -145,19 +206,24 @@
|
||||
animation: sagi-sparkle-twinkle 0.9s ease-in-out infinite;
|
||||
transform-origin: 1073px 424px;
|
||||
}
|
||||
/* worried: shake */
|
||||
/* worried: alarmed shake */
|
||||
.sagi-avatar[data-mood="worried"] {
|
||||
animation: sagi-shake 0.35s ease-in-out 0s 3;
|
||||
transform-origin: 640px 720px;
|
||||
}
|
||||
/* stuck: slow breathe */
|
||||
/* stuck: slow, drooping breathe (fatigued) */
|
||||
.sagi-avatar[data-mood="stuck"] {
|
||||
animation: sagi-breathe 2.4s ease-in-out infinite;
|
||||
animation: sagi-droop 2.4s ease-in-out infinite;
|
||||
transform-origin: 640px 720px;
|
||||
}
|
||||
/* thinking: subtle head tilt */
|
||||
/* thinking: rocking head tilt */
|
||||
.sagi-avatar[data-mood="thinking"] {
|
||||
transform: rotate(-6deg);
|
||||
animation: sagi-rock 1.8s ease-in-out infinite;
|
||||
transform-origin: 640px 720px;
|
||||
}
|
||||
/* watching: attentive sway while a session is live */
|
||||
.sagi-avatar[data-mood="watching"] {
|
||||
animation: sagi-sway 3.2s ease-in-out infinite;
|
||||
transform-origin: 640px 720px;
|
||||
}
|
||||
/* sleeping: slow breathe, droop */
|
||||
@@ -176,6 +242,10 @@
|
||||
.sagi-avatar * {
|
||||
animation: none !important;
|
||||
}
|
||||
.sagi-avatar-btn,
|
||||
.sagi-error-dot {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════ shell (avatar button, bubble, panel) ════════ */
|
||||
@@ -197,10 +267,14 @@
|
||||
left 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
top 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.15s ease;
|
||||
animation: sagi-enter 0.4s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.sagi-avatar-btn:hover {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
.sagi-avatar-btn:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
.sagi-avatar-btn:focus-visible {
|
||||
outline: 2px solid #818cf8;
|
||||
outline-offset: 3px;
|
||||
@@ -241,6 +315,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid #0c0c14;
|
||||
animation: sagi-error-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
.sagi-bubble {
|
||||
max-width: 16rem;
|
||||
|
||||
@@ -21,7 +21,12 @@ fs.mkdirSync(DIST);
|
||||
fs.writeFileSync(path.join(DIST, "index.html"), "<!doctype html><title>from-runtime</title>");
|
||||
|
||||
// Must be set BEFORE requiring the server: the data dir, discovery file and
|
||||
// the static mount are all resolved at startup.
|
||||
// the static mount are all resolved at startup. NODE_ENV=production is the
|
||||
// gate server/index.js uses to decide whether to serve the client bundle at
|
||||
// all - this suite exercises exactly that code path, so it cannot rely on
|
||||
// whatever NODE_ENV the ambient shell happens to have.
|
||||
const PREV_NODE_ENV = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = "production";
|
||||
process.env.CLAUDE_HOME = TMP;
|
||||
process.env.DASHBOARD_DB_PATH = path.join(TMP, "test.db");
|
||||
process.env.DASHBOARD_LIVENESS_PROBE = "0";
|
||||
@@ -81,6 +86,7 @@ describe("DASHBOARD_CLIENT_DIST pointing at a missing directory", () => {
|
||||
|
||||
after(() => {
|
||||
if (srv) srv.close();
|
||||
process.env.NODE_ENV = PREV_NODE_ENV;
|
||||
});
|
||||
|
||||
it("answers the API and only 404s the UI route", async () => {
|
||||
|
||||
Reference in New Issue
Block a user