From 620bfe7b16cfacb0c7efb7f3df01c0516d42d04c Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:17:20 +0530 Subject: [PATCH] feat(assistant): show the user's mascot on the "Talk to Tiny" chip (#3582) --- app/src/features/human/Mascot/Defs.tsx | 74 ++++++++++++++----- app/src/features/human/Mascot/Ghosty.tsx | 32 +++++++- .../human/Mascot/MascotChipAvatar.test.tsx | 66 +++++++++++++++++ .../human/Mascot/MascotChipAvatar.tsx | 69 +++++++++++++++++ app/src/features/human/Mascot/index.ts | 2 + .../human/Mascot/mascotPalette.test.ts | 27 ++++++- .../features/human/Mascot/mascotPalette.ts | 19 +++++ app/src/pages/Accounts.tsx | 19 ++++- .../__tests__/Accounts.faceToggle.test.tsx | 1 + app/tailwind.config.js | 6 ++ 10 files changed, 290 insertions(+), 25 deletions(-) create mode 100644 app/src/features/human/Mascot/MascotChipAvatar.test.tsx create mode 100644 app/src/features/human/Mascot/MascotChipAvatar.tsx diff --git a/app/src/features/human/Mascot/Defs.tsx b/app/src/features/human/Mascot/Defs.tsx index b1b234868..94a449350 100644 --- a/app/src/features/human/Mascot/Defs.tsx +++ b/app/src/features/human/Mascot/Defs.tsx @@ -1,31 +1,69 @@ import React from 'react'; +import { shadeHex } from './mascotPalette'; import { BODY_PATH } from './paths'; -export const GhostyDefs: React.FC<{ idPrefix: string; bodyColor: string }> = ({ +/** + * `shaded` (default) is the original moody, dark-stopped body gradient used by + * the full-size mascot stage. `flat` is a bright, body-colour-dominant gradient + * that mirrors the Rive mascot's look — used for compact avatars (e.g. the + * "Talk to Tiny" chip) so the small mascot matches the big one in the panel + * rather than reading as a dark blob. + */ +export type GhostyVariant = 'shaded' | 'flat'; + +export interface GhostyDefsProps { + idPrefix: string; + bodyColor: string; + variant?: GhostyVariant; +} + +export const GhostyDefs: React.FC = ({ idPrefix, bodyColor, + variant = 'shaded', }) => { const id = (k: string) => `${idPrefix}-${k}`; + const flat = variant === 'flat'; + // Derive a soft highlight + edge shadow from the body colour so any palette + // (including custom hexes) gets a clean, bright 3D body. + const highlight = shadeHex(bodyColor, 0.32); + const edge = shadeHex(bodyColor, -0.24); return ( - - - - - - - - - - - - - - - - - + {flat ? ( + + + + + + ) : ( + + + + + + + + + + + )} + {flat ? ( + + + + + + ) : ( + + + + + + + + )} = ({ viseme, size = '100%', idPrefix = 'mascot', + animated = true, + variant = 'shaded', }) => { - const t = useMascotClock(); + const t = useMascotClock(animated); const preset = presetFor(face); // Gentle bob for the whole character. @@ -297,7 +311,7 @@ export const Ghosty: React.FC = ({ viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`} style={{ overflow: 'visible', display: 'block' }} data-face={face}> - + = ({ - + {/* Inner edge shadow. Kept subtle in the flat (bright) variant so the + compact mascot stays vivid like the Rive stage; full strength in + the moody full-size variant. */} + diff --git a/app/src/features/human/Mascot/MascotChipAvatar.test.tsx b/app/src/features/human/Mascot/MascotChipAvatar.test.tsx new file mode 100644 index 000000000..6fa79bb03 --- /dev/null +++ b/app/src/features/human/Mascot/MascotChipAvatar.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { MascotChipAvatar } from './MascotChipAvatar'; +import { getMascotPalette, shadeHex } from './mascotPalette'; + +describe('MascotChipAvatar', () => { + it('renders the custom GIF (decorative) when a gifUrl is provided', () => { + render(); + + const wrapper = screen.getByTestId('mascot-chip-avatar'); + expect(wrapper).toHaveAttribute('data-variant', 'gif'); + + const img = wrapper.querySelector('img') as HTMLImageElement; + expect(img).toBeTruthy(); + expect(img).toHaveAttribute('src', 'https://example.com/avatar.gif'); + // Decorative: the chip's )} diff --git a/app/src/pages/__tests__/Accounts.faceToggle.test.tsx b/app/src/pages/__tests__/Accounts.faceToggle.test.tsx index 8aa1b1549..c9b80ff5d 100644 --- a/app/src/pages/__tests__/Accounts.faceToggle.test.tsx +++ b/app/src/pages/__tests__/Accounts.faceToggle.test.tsx @@ -62,6 +62,7 @@ vi.mock('../../features/human/Mascot', () => ({ CustomGifMascot: ({ src }: { src: string }) => ( ), + MascotChipAvatar: () => , getMascotPalette: vi.fn(() => ({ bodyFill: '#4A83DD', neckShadowColor: '#2A63BD' })), hexToArgbInt: vi.fn((_hex: string) => 0xff4a83dd), })); diff --git a/app/tailwind.config.js b/app/tailwind.config.js index bd54ca66c..6fa648c5b 100644 --- a/app/tailwind.config.js +++ b/app/tailwind.config.js @@ -238,6 +238,7 @@ module.exports = { 'glow-pulse': 'glowPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', 'float': 'float 3s ease-in-out infinite', 'ticker': 'ticker 30s linear infinite', + 'wiggle': 'wiggle 0.5s ease-in-out', }, keyframes: { @@ -277,6 +278,11 @@ module.exports = { '0%': { transform: 'translateX(0)' }, '100%': { transform: 'translateX(-50%)' }, }, + wiggle: { + '0%, 100%': { transform: 'rotate(0deg)' }, + '25%': { transform: 'rotate(-9deg)' }, + '75%': { transform: 'rotate(9deg)' }, + }, }, // Backdrop blur for glass morphism