Add companion kitten — reactive corner mascot

A cozy bottom-right mascot that reacts to the writing session: cheers on
accepted suggestions and word-count milestones, drops Mandarin-first writing
tips, suggests screen breaks after long stretches, and naps when idle.

- useCompanion: library-agnostic behavior engine (priority + cooldown paced
  so it never nags); tips.ts holds all copy, bilingual zh-first.
- LottiePlayer wraps lottie-web's light build (offline, no eval/CDN) so it
  bundles into the Go binary. Ships with an emoji-kitten placeholder per mood;
  dropping a Lottie cat JSON into animations/index.ts is the only swap needed.
- Speech bubble uses the CJK-first font stack (spec Note #17).

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-25 21:43:58 -07:00
parent 534f7262ab
commit 8a32dde587
10 changed files with 465 additions and 8 deletions

View File

@@ -175,6 +175,50 @@ button, a, input {
pointer-events: none;
}
/* --- Companion kitten -------------------------------------------------------
The cozy corner mascot. Gently bobs while awake, settles and sways slowly
while napping; its speech bubble pops in; little zzz drift up when asleep. */
.petal-companion {
animation: petal-bob 3.2s ease-in-out infinite;
transition: transform 200ms ease;
}
.petal-companion:hover {
transform: translateY(-2px) scale(1.04);
}
.petal-companion-sleep {
animation: petal-bob-slow 5s ease-in-out infinite;
}
@keyframes petal-bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
@keyframes petal-bob-slow {
0%, 100% { transform: translateY(0) rotate(-1deg); }
50% { transform: translateY(-2px) rotate(1deg); }
}
.petal-bubble {
animation: petal-bubble-in 240ms cubic-bezier(0.2, 0.8, 0.3, 1.2) both;
transform-origin: bottom right;
}
@keyframes petal-bubble-in {
from { opacity: 0; transform: translateY(6px) scale(0.92); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.petal-zzz {
top: 2px;
right: 8px;
font-weight: 800;
font-size: 0.8rem;
animation: petal-zzz 2.4s ease-in-out infinite;
}
@keyframes petal-zzz {
0% { opacity: 0; transform: translateY(0) scale(0.8); }
30% { opacity: 0.9; }
100% { opacity: 0; transform: translateY(-14px) scale(1.1); }
}
/* Blinking caret in the Ask Petal bubble while awaiting the first token. */
.petal-chat-caret {
animation: petal-breathe 1s ease-in-out infinite;