Grow the companion, fix idle fallback, add update banner

Companion: bump the corner mascot ~12% (128→144) and make animation
resolution robust — fall back to the idle clip when a mood has no clip of
its own, and pin the always-asleep cat to one pose so its Lottie never
reloads. This stops a bare emoji from replacing the cute companion on idle.

Update notifications: serve a build id at /api/version (a hash of the
embedded dist/index.html, which changes on every frontend deploy). The
client records its load-time id, polls every 90s (and on tab focus), and
floats a Mandarin-first "new version available" banner offering a refresh
when the id changes.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-25 22:45:23 -07:00
parent a634994d25
commit 95123e8c49
7 changed files with 180 additions and 7 deletions

View File

@@ -44,10 +44,20 @@ export function PetalCompanion({ wordCount, saveStatus, editTick, acceptTick }:
// dozes them, keep their normal idle pose instead of a sleepy face. Only a
// mascot with a real sleep animation (the always-asleep cat) shows the zzz.
const hasSleepClip = Boolean(companion.animations.sleeping)
const renderMood: Mood =
mood === 'sleeping' && !companion.alwaysAsleep && !hasSleepClip ? 'idle' : mood
// An always-asleep mascot (the cat) stays pinned to one pose so its Lottie
// never reloads as the engine flips moods underneath it.
const renderMood: Mood = companion.alwaysAsleep
? 'idle'
: mood === 'sleeping' && !hasSleepClip
? 'idle'
: mood
const napping = companion.alwaysAsleep || (mood === 'sleeping' && hasSleepClip)
// Never swap the cute companion for a bare emoji on an idle nap: fall back to
// the idle animation when a mood has no clip of its own, and only reach for
// the emoji if the companion ships no animations at all.
const animationData = companion.animations[renderMood] ?? companion.animations.idle
function choose(id: string) {
setCompanionId(id)
try {
@@ -153,8 +163,8 @@ export function PetalCompanion({ wordCount, saveStatus, editTick, acceptTick }:
aria-label="Choose a companion"
className={`petal-companion pointer-events-auto select-none ${napping ? 'petal-companion-sleep' : ''}`}
style={{
width: 128,
height: 128,
width: 144,
height: 144,
padding: 0,
borderRadius: 'var(--radius-pill)',
background: 'var(--color-surface-alt)',
@@ -167,9 +177,9 @@ export function PetalCompanion({ wordCount, saveStatus, editTick, acceptTick }:
>
<LottiePlayer
key={companion.id}
animationData={companion.animations[renderMood]}
className="h-28 w-28"
fallback={<span style={{ fontSize: 64, lineHeight: 1 }}>{MOOD_EMOJI[renderMood]}</span>}
animationData={animationData}
className="h-32 w-32"
fallback={<span style={{ fontSize: 72, lineHeight: 1 }}>{MOOD_EMOJI[renderMood]}</span>}
/>
{napping && (
<span className="petal-zzz absolute" aria-hidden style={{ color: 'var(--color-muted)' }}>