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:
41
web/src/components/Companion/LottiePlayer.tsx
Normal file
41
web/src/components/Companion/LottiePlayer.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
// Light build: drops the eval-based expression engine (smaller bundle, no eval
|
||||
// warning). Plenty for a looping mascot; swap to 'lottie-web' only if an asset
|
||||
// genuinely needs After Effects expressions.
|
||||
import lottie, { type AnimationItem } from 'lottie-web/build/player/lottie_light'
|
||||
|
||||
interface Props {
|
||||
// Parsed Lottie JSON (imported, so it bundles offline). When undefined the
|
||||
// `fallback` is rendered instead — letting the companion ship before any
|
||||
// animation asset exists.
|
||||
animationData?: object
|
||||
loop?: boolean
|
||||
className?: string
|
||||
fallback: React.ReactNode
|
||||
}
|
||||
|
||||
// A thin wrapper over lottie-web (framework-agnostic, no React peer deps, fully
|
||||
// offline). Reloads the animation whenever the data changes — moods swap by
|
||||
// passing a different `animationData`.
|
||||
export function LottiePlayer({ animationData, loop = true, className, fallback }: Props) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const anim = useRef<AnimationItem | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !animationData) return
|
||||
anim.current = lottie.loadAnimation({
|
||||
container: ref.current,
|
||||
renderer: 'svg',
|
||||
loop,
|
||||
autoplay: true,
|
||||
animationData,
|
||||
})
|
||||
return () => {
|
||||
anim.current?.destroy()
|
||||
anim.current = null
|
||||
}
|
||||
}, [animationData, loop])
|
||||
|
||||
if (!animationData) return <>{fallback}</>
|
||||
return <div ref={ref} className={className} aria-hidden />
|
||||
}
|
||||
Reference in New Issue
Block a user