@dotcraft/avatar 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +46 -43
  3. package/dist/accessories.css +14 -14
  4. package/dist/avatar.css +62 -62
  5. package/dist/composer/ComposerMascot.d.ts +1 -1
  6. package/dist/composer/ComposerMascot.d.ts.map +1 -1
  7. package/dist/composer/ComposerMascot.js +33 -107
  8. package/dist/composer/ComposerMascot.js.map +1 -1
  9. package/dist/composer/MascotIdleStage.d.ts +11 -0
  10. package/dist/composer/MascotIdleStage.d.ts.map +1 -0
  11. package/dist/composer/MascotIdleStage.js +9 -0
  12. package/dist/composer/MascotIdleStage.js.map +1 -0
  13. package/dist/composer/constants.d.ts +12 -1
  14. package/dist/composer/constants.d.ts.map +1 -1
  15. package/dist/composer/constants.js +9 -4
  16. package/dist/composer/constants.js.map +1 -1
  17. package/dist/composer/styles/energy.css +99 -99
  18. package/dist/composer/styles/expressions.css +20 -20
  19. package/dist/composer/styles/feedback.css +188 -188
  20. package/dist/composer/styles/focus.css +72 -72
  21. package/dist/composer/styles/foundation.css +301 -287
  22. package/dist/composer/styles/idle.css +449 -438
  23. package/dist/composer/styles/overdrive.css +264 -264
  24. package/dist/composer/styles/props.css +21 -21
  25. package/dist/composer/types.d.ts +1 -0
  26. package/dist/composer/types.d.ts.map +1 -1
  27. package/dist/composer/useComposerAvatarBehavior.d.ts +1 -0
  28. package/dist/composer/useComposerAvatarBehavior.d.ts.map +1 -1
  29. package/dist/composer/useComposerAvatarBehavior.js +2 -2
  30. package/dist/composer/useComposerAvatarBehavior.js.map +1 -1
  31. package/dist/composer/useMascotActiveIdle.d.ts +33 -0
  32. package/dist/composer/useMascotActiveIdle.d.ts.map +1 -0
  33. package/dist/composer/useMascotActiveIdle.js +128 -0
  34. package/dist/composer/useMascotActiveIdle.js.map +1 -0
  35. package/dist/react.d.ts +5 -0
  36. package/dist/react.d.ts.map +1 -1
  37. package/dist/react.js +4 -0
  38. package/dist/react.js.map +1 -1
  39. package/dist/rig.css +265 -265
  40. package/dist/styles.css +13 -13
  41. package/package.json +56 -55
  42. package/src/AnimatedDecoration.tsx +43 -0
  43. package/src/AppearanceRig.tsx +15 -0
  44. package/src/Avatar.tsx +85 -0
  45. package/src/DecorationShapes.tsx +8 -0
  46. package/src/Decorations.tsx +31 -0
  47. package/src/Faces.tsx +37 -0
  48. package/src/HatDecorations.tsx +75 -0
  49. package/src/HeldDecorations.tsx +48 -0
  50. package/src/MascotRig.tsx +191 -0
  51. package/src/ObjectDecorations.tsx +55 -0
  52. package/src/accessories.css +14 -0
  53. package/src/appearanceModel.ts +65 -0
  54. package/src/avatar.css +62 -0
  55. package/src/characters.ts +5 -0
  56. package/src/composer/ComposerMascot.tsx +445 -0
  57. package/src/composer/ComposerMascotShadow.tsx +5 -0
  58. package/src/composer/MascotIdleStage.tsx +23 -0
  59. package/src/composer/constants.ts +89 -0
  60. package/src/composer/mascotHandoff.ts +19 -0
  61. package/src/composer/styles/energy.css +99 -0
  62. package/src/composer/styles/expressions.css +20 -0
  63. package/src/composer/styles/feedback.css +188 -0
  64. package/src/composer/styles/focus.css +72 -0
  65. package/src/composer/styles/foundation.css +301 -0
  66. package/src/composer/styles/idle.css +449 -0
  67. package/src/composer/styles/overdrive.css +264 -0
  68. package/src/composer/styles/props.css +21 -0
  69. package/src/composer/types.ts +32 -0
  70. package/src/composer/useComposerAvatarBehavior.ts +98 -0
  71. package/src/composer/useComposerMotion.ts +15 -0
  72. package/src/composer/useComposerProfile.ts +50 -0
  73. package/src/composer/useMascotActiveIdle.ts +170 -0
  74. package/src/decorationCatalog.ts +45 -0
  75. package/src/decorationMotion.ts +68 -0
  76. package/src/environment.ts +46 -0
  77. package/src/index.ts +5 -0
  78. package/src/palette.ts +52 -0
  79. package/src/react.ts +11 -0
  80. package/src/rig.css +265 -0
  81. package/src/styles.css +13 -0
  82. package/src/useEventReplay.ts +13 -0
  83. package/src/useGesture.ts +51 -0
@@ -0,0 +1,65 @@
1
+ // Frozen v1 draw order. IDs are serialized; positions are only sampling weights.
2
+ export const primaryIds = ['none', 'baseball-cap', 'bucket-hat', 'beret', 'beanie', 'top-hat', 'wizard-hat', 'chef-hat', 'party-hat', 'crown', 'hard-hat', 'nightcap', 'straw-hat', 'poop', 'banana', 'fried-egg', 'rubber-duck', 'paper-boat', 'traffic-cone', 'sprout', 'donut'] as const
3
+ export const heldIds = ['task-board', 'wrench', 'shield', 'magnifier', 'control-panel'] as const
4
+ export type HeldId = typeof heldIds[number]
5
+ export const secondaryIds = ['none', 'forehead-goggles', ...heldIds] as const
6
+ export type PrimaryId = typeof primaryIds[number]
7
+ export type SecondaryId = typeof secondaryIds[number]
8
+ export type DecorationId = Exclude<PrimaryId | SecondaryId, 'none'>
9
+ export interface Appearance {
10
+ version: 2
11
+ palette: number
12
+ baseFace: number
13
+ primary: PrimaryId
14
+ secondary: SecondaryId
15
+ }
16
+
17
+ // Explicit allow-list, including the undecorated head. No cartesian-product fallback.
18
+ const faceAccessories = ['none', ...heldIds] as const
19
+ export const compatibility: Record<PrimaryId, readonly SecondaryId[]> = {
20
+ none: [...faceAccessories, 'forehead-goggles'],
21
+ 'baseball-cap': faceAccessories, 'bucket-hat': faceAccessories, beret: faceAccessories,
22
+ beanie: faceAccessories, 'top-hat': faceAccessories, 'wizard-hat': faceAccessories,
23
+ 'chef-hat': faceAccessories, 'party-hat': faceAccessories, crown: faceAccessories,
24
+ 'hard-hat': faceAccessories, nightcap: faceAccessories, 'straw-hat': faceAccessories,
25
+ poop: [...faceAccessories, 'forehead-goggles'], banana: [...faceAccessories, 'forehead-goggles'],
26
+ 'fried-egg': [...faceAccessories, 'forehead-goggles'], 'rubber-duck': [...faceAccessories, 'forehead-goggles'],
27
+ 'paper-boat': [...faceAccessories, 'forehead-goggles'], 'traffic-cone': [...faceAccessories, 'forehead-goggles'],
28
+ sprout: [...faceAccessories, 'forehead-goggles'], donut: [...faceAccessories, 'forehead-goggles'],
29
+ }
30
+ export function isCompatible(primary: PrimaryId, secondary: SecondaryId) {
31
+ return compatibility[primary].includes(secondary)
32
+ }
33
+ export function withPrimary(appearance: Appearance, primary: PrimaryId): Appearance {
34
+ return { ...appearance, primary, secondary: isCompatible(primary, appearance.secondary) ? appearance.secondary : 'none' }
35
+ }
36
+ export function hashSeed(seed: string): number {
37
+ let value = 0x811c9dc5
38
+ for (let index = 0; index < seed.length; index++) value = Math.imul(value ^ seed.charCodeAt(index), 0x01000193)
39
+ // Avalanche avoids correlations between adjacent sample IDs and independent dimensions.
40
+ value ^= value >>> 16; value = Math.imul(value, 0x7feb352d)
41
+ value ^= value >>> 15; value = Math.imul(value, 0x846ca68b)
42
+ return (value ^ (value >>> 16)) >>> 0
43
+ }
44
+ export function isHeld(id: SecondaryId): id is HeldId { return (heldIds as readonly string[]).includes(id) }
45
+ export const originalAppearance: Appearance = { version: 2, palette: -1, baseFace: 0, primary: 'none', secondary: 'none' }
46
+ export function normalizeName(name: string): string { return name.trim().normalize('NFC') }
47
+ export function deriveAppearance(name: string): Appearance {
48
+ const seed = normalizeName(name)
49
+ if (!seed) return { ...originalAppearance }
50
+ const draw = (dimension: string) => hashSeed(JSON.stringify(['dotcraft-avatar', 1, seed, dimension])) / 0x100000000
51
+ const primary = primaryIds[Math.floor(draw('primary') * primaryIds.length)]
52
+ const choices = compatibility[primary].filter(id => id !== 'none')
53
+ const secondaryDraw = (dimension: string) => hashSeed(JSON.stringify(['dotcraft-avatar', 2, seed, dimension])) / 0x100000000
54
+ const secondary = secondaryDraw('secondary-presence') < .5 ? 'none' : choices[Math.floor(secondaryDraw('secondary-item') * choices.length)]
55
+ return { version: 2, palette: Math.floor(draw('palette') * 12), baseFace: Math.floor(draw('face') * 5), primary, secondary }
56
+ }
57
+ export function sampleSeed(seed: string, round: number, index: number) {
58
+ return JSON.stringify([seed, round, index])
59
+ }
60
+ export function appearanceWall(seed: string, round = 0) {
61
+ return Array.from({ length: 100 }, (_, index) => {
62
+ const id = sampleSeed(seed, round, index)
63
+ return { id, appearance: deriveAppearance(id) }
64
+ })
65
+ }
package/src/avatar.css ADDED
@@ -0,0 +1,62 @@
1
+ .dca-robot {
2
+ display: inline-flex;
3
+ flex: none;
4
+ vertical-align: middle;
5
+ isolation: isolate;
6
+ }
7
+
8
+ .dca-canvas { overflow: visible; }
9
+ .dca-body-motion { transform-origin: 512px 760px; }
10
+ .dca-robot[data-motion='on'] .dca-body-motion {
11
+ transition: transform 260ms cubic-bezier(.22, .7, .3, 1);
12
+ }
13
+
14
+ /* The production rig owns arm geometry, paired paint layers and all arm motion. */
15
+ .dca-robot[data-compact='true'] .dca-decoration-detail { display: none; }
16
+ .dca-robot[data-exiting='true'] :is(.dca-part-prop-laptop, .dca-part-prop-sign) {
17
+ opacity: 0;
18
+ transition: opacity 140ms ease;
19
+ }
20
+
21
+ .dca-robot[data-pose='thinking'] .dca-body-motion { transform: rotate(-4deg); }
22
+ .dca-robot[data-pose='working'] .dca-body-motion { transform: translateY(12px) rotate(2deg); }
23
+ .dca-robot[data-pose='blocked'] .dca-body-motion { transform: translateY(22px) scaleY(.96); }
24
+ .dca-robot[data-pose='greeting'] .dca-body-motion { transform: rotate(-3deg); }
25
+
26
+ .dca-robot[data-motion='on'][data-pose='idle'] .dca-body-motion { animation: dca-breathe 5s ease-in-out infinite; }
27
+ .dca-robot[data-motion='on'][data-pose='idle'] .dca-part-caret { animation: dca-cursor 7s step-end infinite; }
28
+ .dca-robot[data-motion='on'][data-pose='working'] .dca-body-motion { animation: dca-work var(--dca-loop) ease-in-out infinite; }
29
+ .dca-robot[data-motion='on'][data-pose='done'] .dca-body-motion { animation: dca-celebrate 800ms ease-in-out both; }
30
+ .dca-robot[data-motion='on'][data-pose='acknowledge'] .dca-body-motion { animation: dca-nod 350ms ease-in-out both; }
31
+
32
+ .dca-robot[data-paused='true'] * { animation-play-state: paused !important; }
33
+ .dca-robot[data-exiting='true'] .dca-body-motion { animation: none !important; }
34
+ .dca-robot[data-motion='off'] * { animation: none !important; transition: none !important; }
35
+
36
+ @keyframes dca-breathe {
37
+ 0%, 100% { transform: translateY(0) scaleY(1); }
38
+ 50% { transform: translateY(-9px) scaleY(1.008); }
39
+ }
40
+ @keyframes dca-cursor { 0%, 93%, 100% { opacity: 1; } 95%, 98% { opacity: .12; } }
41
+ @keyframes dca-work {
42
+ 0%, 100% { transform: translateY(12px) rotate(2deg); }
43
+ 50% { transform: translateY(5px) rotate(0deg); }
44
+ }
45
+ @keyframes dca-celebrate {
46
+ 0%, 100% { transform: translateY(0); }
47
+ 20% { transform: translateY(10px) scaleY(.97); }
48
+ 55% { transform: translateY(-30px) rotate(-3deg); }
49
+ }
50
+ @keyframes dca-nod {
51
+ 0%, 100% { transform: translateY(0); }
52
+ 38% { transform: translateY(17px) scaleY(.97); }
53
+ 68% { transform: translateY(-8px); }
54
+ }
55
+
56
+
57
+ .dca-robot[data-pose='sleep'] .dca-body-motion { transform: translateY(18px) rotate(4deg); }
58
+ .dca-robot[data-pose='sleep'] .dca-part-light { opacity: .45; }
59
+ .dca-robot[data-motion='off'][data-pose='greeting'] :is(.dca-part-arm-r-w, .dca-part-arm-r-b) { transform: translate(-48px, 88px) rotate(-110deg) scaleY(.8); }
60
+ @media (prefers-reduced-motion: reduce) {
61
+ .dca-robot[data-mode='system'] * { animation: none !important; transition: none !important; }
62
+ }
@@ -0,0 +1,5 @@
1
+ export type AvatarState = 'idle' | 'thinking' | 'working' | 'waiting' | 'blocked' | 'done'
2
+ export type AvatarPose = AvatarState | 'greeting' | 'acknowledge' | 'sleep'
3
+ export type MotionMode = 'system' | 'on' | 'off'
4
+ export type AvatarExpression = 'base' | 'happy' | 'operator' | 'sleep'
5
+ export type AvatarGesture = 'blink' | 'look-left' | 'look-right' | 'antenna-bob'
@@ -0,0 +1,445 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, type AnimationEvent, type CSSProperties } from 'react'
2
+ import { Avatar } from '../Avatar.js'
3
+ import { deriveAppearance, mascotPaletteOf, type AvatarPose } from '../index.js'
4
+ import { useComposerAvatarBehavior } from './useComposerAvatarBehavior.js'
5
+ import { consumeMascotHandoff, recordMascotHandoff } from './mascotHandoff.js'
6
+ import { useComposerProfile } from './useComposerProfile.js'
7
+ import { useComposerMotion } from './useComposerMotion.js'
8
+ import { MASCOT_SIZE, MASCOT_SCALE, MASCOT_HIDDEN_RATIO, MASCOT_RAISE, MASCOT_SLEEP_AFTER_MS, MASCOT_WAVE_DURATION_MS, MASCOT_SPARKLES } from './constants.js'
9
+ import { useMascotActiveIdle } from './useMascotActiveIdle.js'
10
+ import type { ComposerMascotProps, ComposerMascotContext, MascotExpression, MascotLight } from './types.js'
11
+ export function ComposerMascot({ name, motion = 'system', theme = 'dark', focused = false, dragOver = false, bounceSignal = 0, interaction, reasoningEffort = 'off', speed = 'standard', contextMax = false, anchorOffset = 0, anchorPushSignal = 0, handoff = false, renderCharacter, renderMenu, onNameRendered }: ComposerMascotProps) {
12
+ const reduced = !useComposerMotion(motion)
13
+ const { avatar, profileTransition, profileTransitionRevision } = useComposerProfile(name, reduced)
14
+ useEffect(() => { onNameRendered?.(avatar) }, [avatar, onNameRendered])
15
+ const [menuPos, setMenuPos] = useState<{ x: number; y: number } | null>(null)
16
+ const [ambientSleeping, setSleeping] = useState(false)
17
+ const sleeping = ambientSleeping || interaction?.expression === 'sleep'
18
+ const [waving, setWaving] = useState(false)
19
+ const [greetingSequence, setGreetingSequence] = useState(0)
20
+ const [startled, setStartled] = useState(false)
21
+ const [launching, setLaunching] = useState(false)
22
+ const [cheering, setCheering] = useState(false)
23
+ const [sparkling, setSparkling] = useState(false)
24
+ const [shaking, setShaking] = useState(false)
25
+ const [nodding, setNodding] = useState(false)
26
+ const [landing, setLanding] = useState(false)
27
+ const [pushLift, setPushLift] = useState(false)
28
+ const rootRef = useRef<HTMLDivElement | null>(null)
29
+ const clearGestureRef = useRef<() => void>(() => {})
30
+
31
+ const baseExpression: MascotExpression =
32
+ interaction?.expression ?? (dragOver ? 'operator' : focused ? 'happy' : 'neutral')
33
+ const light: MascotLight = interaction?.light ?? 'default'
34
+ const hasMenu = renderMenu != null
35
+ const bubble = interaction?.bubble ?? null
36
+ const holdSign = interaction?.hold === 'sign'
37
+
38
+ const laptopActive =
39
+ !sleeping &&
40
+ !dragOver &&
41
+ !holdSign &&
42
+ bubble == null &&
43
+ baseExpression === 'operator' &&
44
+ light === 'default'
45
+ const expression: MascotExpression = sleeping ? 'sleep' : waving ? 'happy' : baseExpression
46
+ const semanticAvatarPose: AvatarPose = light === 'error'
47
+ ? 'blocked'
48
+ : light === 'success'
49
+ ? 'done'
50
+ : sleeping
51
+ ? 'sleep'
52
+ : waving
53
+ ? 'greeting'
54
+ : holdSign || bubble != null
55
+ ? 'waiting'
56
+ : laptopActive
57
+ ? 'working'
58
+ : 'idle'
59
+ const ambient =
60
+ !focused &&
61
+ !dragOver &&
62
+ !bubble &&
63
+ !holdSign &&
64
+ menuPos == null &&
65
+ baseExpression === 'neutral' &&
66
+ light === 'default'
67
+ const idle = useMascotActiveIdle({
68
+ enabled: ambient && !sleeping && !reduced,
69
+ onStart: () => clearGestureRef.current(),
70
+ onActivity: () => {
71
+ setSleeping((current) => {
72
+ if (current && !reduced) setStartled(true)
73
+ return false
74
+ })
75
+ }
76
+ })
77
+ const avatarBehavior = useComposerAvatarBehavior({
78
+ semanticPose: semanticAvatarPose,
79
+ baseExpression,
80
+ focused,
81
+ dragOver,
82
+ sleeping,
83
+ waving,
84
+ activeIdle: idle.activeIdle != null,
85
+ bounceSignal,
86
+ reducedMotion: reduced
87
+ })
88
+ clearGestureRef.current = avatarBehavior.clearGesture
89
+ const mascotPalette = mascotPaletteOf(deriveAppearance(avatar ?? ''))
90
+ const activity: ComposerMascotContext["activity"] = light === 'error'
91
+ ? 'error'
92
+ : light === 'success'
93
+ ? 'success'
94
+ : sleeping
95
+ ? 'sleeping'
96
+ : dragOver
97
+ ? 'dragging'
98
+ : holdSign
99
+ ? 'decision'
100
+ : baseExpression === 'operator'
101
+ ? 'working'
102
+ : focused
103
+ ? 'focused'
104
+ : 'idle'
105
+ const context: ComposerMascotContext = {
106
+ size: MASCOT_SIZE,
107
+ activity,
108
+ expression,
109
+ light,
110
+ submitRevision: bounceSignal,
111
+ reasoningEffort,
112
+ speed,
113
+ contextMax,
114
+ reducedMotion: reduced
115
+ }
116
+ useLayoutEffect(() => {
117
+ if (!handoff) return undefined
118
+ const el = rootRef.current
119
+ if (!el) return undefined
120
+ let timer = 0
121
+ const dy = reduced ? null : consumeMascotHandoff(el)
122
+ if (dy != null) {
123
+ const rising = dy > 0
124
+ el.style.transition = 'none'
125
+ el.style.transform = `translateY(${dy}px)`
126
+ void el.offsetHeight
127
+ el.style.transition = rising
128
+ ? 'transform 420ms cubic-bezier(0.34, 1.56, 0.64, 1)'
129
+ : 'transform 300ms cubic-bezier(0.55, 0, 0.8, 0.9)'
130
+ el.style.transform = 'translateY(0)'
131
+ if (rising) setStartled(true)
132
+ timer = window.setTimeout(() => {
133
+ el.style.transition = ''
134
+ el.style.transform = ''
135
+ if (!rising) setLanding(true)
136
+ }, rising ? 430 : 310)
137
+ }
138
+ return () => {
139
+ window.clearTimeout(timer)
140
+ recordMascotHandoff(el)
141
+ }
142
+ }, [handoff])
143
+
144
+ const previousAnchorOffsetRef = useRef(anchorOffset)
145
+ useLayoutEffect(() => {
146
+ const el = rootRef.current
147
+ const previousOffset = previousAnchorOffsetRef.current
148
+ previousAnchorOffsetRef.current = anchorOffset
149
+ if (!el || previousOffset === anchorOffset || reduced) return undefined
150
+
151
+ if (anchorOffset > previousOffset) {
152
+
153
+ el.style.transition = ''
154
+ el.style.transform = ''
155
+ setLanding(false)
156
+ return undefined
157
+ }
158
+
159
+ const currentVisualTop = el.getBoundingClientRect().top
160
+ const offsetDelta = anchorOffset - previousOffset
161
+ el.style.transition = 'none'
162
+ el.style.transform = ''
163
+ const targetTop = el.getBoundingClientRect().top
164
+ const dy = currentVisualTop + offsetDelta - targetTop
165
+ if (Math.abs(dy) < 1) return undefined
166
+
167
+ const rising = dy > 0
168
+ let timer = 0
169
+ el.style.transform = `translateY(${dy}px)`
170
+ void el.offsetHeight
171
+ el.style.transition = rising
172
+ ? 'transform 420ms cubic-bezier(0.34, 1.56, 0.64, 1)'
173
+ : 'transform 300ms cubic-bezier(0.55, 0, 0.8, 0.9)'
174
+ el.style.transform = 'translateY(0)'
175
+ if (rising) {
176
+ setStartled(true)
177
+ } else {
178
+
179
+ setStartled(false)
180
+ setPushLift(false)
181
+ }
182
+ timer = window.setTimeout(() => {
183
+ el.style.transition = ''
184
+ el.style.transform = ''
185
+ if (!rising) setLanding(true)
186
+ }, rising ? 430 : 310)
187
+
188
+ return () => window.clearTimeout(timer)
189
+ }, [anchorOffset])
190
+
191
+ const previousAnchorPushSignalRef = useRef(anchorPushSignal)
192
+ useEffect(() => {
193
+ if (anchorPushSignal === previousAnchorPushSignalRef.current) return
194
+ previousAnchorPushSignalRef.current = anchorPushSignal
195
+ if (!reduced) setPushLift(true)
196
+ }, [anchorPushSignal])
197
+
198
+ const prevBounceRef = useRef(bounceSignal)
199
+ useEffect(() => {
200
+ if (bounceSignal === prevBounceRef.current) return
201
+ prevBounceRef.current = bounceSignal
202
+ if (!reduced) setLaunching(true)
203
+ }, [bounceSignal])
204
+
205
+ const prevLightRef = useRef(light)
206
+ useEffect(() => {
207
+ const prev = prevLightRef.current
208
+ prevLightRef.current = light
209
+ if (light === prev || reduced) return
210
+ if (light === 'success') {
211
+ setCheering(true)
212
+ setSparkling(true)
213
+ } else if (light === 'error') {
214
+ setShaking(true)
215
+ }
216
+ }, [light])
217
+
218
+ useEffect(() => {
219
+ if (!ambient || reduced) {
220
+ setSleeping(false)
221
+ return
222
+ }
223
+ if (sleeping || idle.activeIdle) return
224
+ const timer = window.setTimeout(() => setSleeping(true), MASCOT_SLEEP_AFTER_MS)
225
+ return () => window.clearTimeout(timer)
226
+ }, [ambient, idle.activityRevision, sleeping, idle.activeIdle, reduced])
227
+
228
+ const wake = useCallback(() => {
229
+ setSleeping(false)
230
+ if (!reduced) setStartled(true)
231
+ }, [reduced])
232
+
233
+ useEffect(() => {
234
+ if (!waving) return
235
+ const timer = window.setTimeout(() => setWaving(false), MASCOT_WAVE_DURATION_MS)
236
+ return () => window.clearTimeout(timer)
237
+ }, [waving])
238
+
239
+ useEffect(() => {
240
+ if (!focused || reduced) return
241
+ const onKeyDown = (event: KeyboardEvent): void => {
242
+ if (event.ctrlKey || event.metaKey || event.altKey) return
243
+ setNodding(true)
244
+ }
245
+ window.addEventListener('keydown', onKeyDown)
246
+ return () => window.removeEventListener('keydown', onKeyDown)
247
+ }, [focused, reduced])
248
+
249
+ const onAnimationEnd = (event: AnimationEvent<HTMLDivElement>): void => {
250
+ idle.onAnimationEnd(event)
251
+ switch (event.animationName) {
252
+ case 'composer-mascot-launch':
253
+ setLaunching(false)
254
+ break
255
+ case 'composer-mascot-cheer':
256
+ setCheering(false)
257
+ break
258
+ case 'composer-mascot-sparkle':
259
+ setSparkling(false)
260
+ break
261
+ case 'composer-mascot-shake':
262
+ setShaking(false)
263
+ break
264
+ case 'composer-mascot-startle':
265
+ setStartled(false)
266
+ break
267
+ case 'composer-mascot-nod':
268
+ setNodding(false)
269
+ break
270
+ case 'composer-mascot-land':
271
+ setLanding(false)
272
+ break
273
+ case 'composer-mascot-push-lift':
274
+ setPushLift(false)
275
+ break
276
+ }
277
+ }
278
+
279
+ const poseTransform = sleeping
280
+ ? 'translateY(2px) rotate(2.6deg) scale(0.985)'
281
+ : light === 'error'
282
+ ? 'translateY(2px) rotate(-3deg) scale(0.98)'
283
+ : focused
284
+ ? 'scale(1.1)'
285
+ : 'scale(1)'
286
+
287
+ const shotClass = cheering
288
+ ? 'composer-mascot-cheer'
289
+ : shaking
290
+ ? 'composer-mascot-shake'
291
+ : pushLift
292
+ ? 'composer-mascot-push-lift'
293
+ : startled
294
+ ? 'composer-mascot-startle'
295
+ : landing
296
+ ? 'composer-mascot-land'
297
+ : launching
298
+ ? 'composer-mascot-launch'
299
+ : nodding
300
+ ? 'composer-mascot-nod'
301
+ : undefined
302
+
303
+ const loopClass = sleeping
304
+ ? 'composer-mascot-sleep-breathe'
305
+ : dragOver
306
+ ? 'composer-mascot-eager'
307
+ : baseExpression === 'operator' && light === 'default'
308
+ ? 'composer-mascot-think'
309
+ : 'composer-mascot-breathe'
310
+
311
+ const rootClassName =
312
+ [
313
+ idle.className,
314
+ sleeping ? 'composer-mascot-sleeping' : null,
315
+ ]
316
+ .filter(Boolean)
317
+ .join(' ') || undefined
318
+
319
+ const character = <Avatar name={avatar ?? ''} size={MASCOT_SIZE} state={avatarBehavior.pose}
320
+ expression={avatarBehavior.expression} gesture={avatarBehavior.gesture}
321
+ gestureSequence={avatarBehavior.gestureSequence} onGestureComplete={avatarBehavior.completeGesture}
322
+ eventSequence={bounceSignal + greetingSequence} motion={reduced ? 'off' : 'on'} />
323
+ return (
324
+ <div
325
+
326
+ aria-hidden={interaction ? undefined : true}
327
+ data-composer-mascot-motion={reduced ? 'off' : 'on'}
328
+ data-composer-avatar-pose={semanticAvatarPose}
329
+ ref={rootRef}
330
+ className={rootClassName}
331
+ data-mascot-name={avatar ?? ''}
332
+ data-mascot-theme={theme}
333
+ data-mascot-effort={reasoningEffort}
334
+ data-mascot-speed={speed}
335
+ data-mascot-context={contextMax ? 'max' : 'default'}
336
+ data-mascot-profile-transition={profileTransition ? 'active' : 'idle'}
337
+ {...idle.attributes}
338
+ data-mascot-anchor-offset={anchorOffset}
339
+ onAnimationEnd={onAnimationEnd}
340
+ style={{
341
+ '--mascot-body-dark': mascotPalette.bodyD,
342
+ '--mascot-body-mid': mascotPalette.bodyM,
343
+ '--mascot-body-light': mascotPalette.bodyL,
344
+ '--mascot-mark-dark': mascotPalette.markD,
345
+ '--mascot-mark-energy': mascotPalette.markM,
346
+ '--mascot-energy-accent': mascotPalette.accent,
347
+ '--mascot-profile-from-accent': profileTransition?.fromAccent ?? mascotPalette.accent,
348
+ '--mascot-profile-to-accent': profileTransition?.toAccent ?? mascotPalette.accent,
349
+ position: 'absolute',
350
+ right: '40px',
351
+ top: `${-(MASCOT_SIZE * (1 - MASCOT_HIDDEN_RATIO)) - MASCOT_RAISE - anchorOffset}px`,
352
+ zIndex: 0,
353
+ pointerEvents: 'none'
354
+ } as CSSProperties}
355
+ >
356
+ {bubble && (
357
+ <div
358
+ style={{
359
+ position: 'absolute',
360
+ right: 0,
361
+ bottom: 'calc(100% + 8px)',
362
+ zIndex: 5,
363
+ pointerEvents: 'auto'
364
+ }}
365
+ >
366
+ {bubble}
367
+ </div>
368
+ )}
369
+ <div
370
+ key={profileTransitionRevision}
371
+ className="composer-mascot-motion composer-mascot-stage"
372
+ style={{
373
+ '--mascot-idle-stage-scale': MASCOT_SCALE,
374
+ transformOrigin: 'bottom center',
375
+ transform: `scale(${MASCOT_SCALE})`,
376
+
377
+ filter: `drop-shadow(0 5.3px 7.3px color-mix(in srgb, ${mascotPalette.shadow} 20%, transparent))`
378
+ } as CSSProperties}
379
+ >
380
+ <div
381
+ className="composer-mascot-travel"
382
+ style={{
383
+ transformOrigin: 'bottom center',
384
+ transition: 'transform 280ms cubic-bezier(0.34, 1.56, 0.64, 1)',
385
+ transform: poseTransform
386
+ }}
387
+ >
388
+ <div className={shotClass ? `composer-mascot-lift ${shotClass}` : 'composer-mascot-lift'}>
389
+ <div className={`composer-mascot-body ${loopClass}`}>
390
+ <div
391
+ className="composer-mascot-jelly composer-mascot-trail"
392
+ style={{ pointerEvents: 'auto', cursor: hasMenu ? 'context-menu' : undefined }}
393
+ onMouseEnter={sleeping ? wake : undefined}
394
+ onClick={() => {
395
+ if (sleeping) {
396
+ wake()
397
+ return
398
+ }
399
+ if (!reduced) {
400
+ setWaving(true)
401
+ setGreetingSequence((value) => value + 1)
402
+ }
403
+ }}
404
+ onContextMenu={
405
+ hasMenu
406
+ ? (e) => {
407
+ e.preventDefault()
408
+ setMenuPos({ x: e.clientX, y: e.clientY })
409
+ }
410
+ : undefined
411
+ }
412
+ >
413
+ <div className="composer-mascot-fast-echo">
414
+ <div className="composer-mascot-character-stage">
415
+ {renderCharacter ? renderCharacter(character, context) : character}
416
+ </div>
417
+ </div>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ </div>
422
+
423
+ {sleeping && (
424
+ <div aria-hidden className="composer-mascot-zzz">
425
+ <span>z</span>
426
+ <span>z</span>
427
+ <span>z</span>
428
+ </div>
429
+ )}
430
+ {sparkling && (
431
+ <div aria-hidden className="composer-mascot-sparkles">
432
+ {MASCOT_SPARKLES.map((s, i) => (
433
+ <i
434
+ key={i}
435
+ style={{ '--dx': s.dx, '--dy': s.dy, animationDelay: s.delay } as CSSProperties}
436
+ />
437
+ ))}
438
+ </div>
439
+ )}
440
+ </div>
441
+
442
+ {menuPos && renderMenu?.(menuPos, () => setMenuPos(null))}
443
+ </div>
444
+ )
445
+ }
@@ -0,0 +1,5 @@
1
+ import { deriveAppearance, mascotPaletteOf } from '../index.js'
2
+ export function ComposerMascotShadow({ name = '' }: { name?: string }) {
3
+ const palette = mascotPaletteOf(deriveAppearance(name))
4
+ return <div aria-hidden className="composer-mascot-contact-shadow" style={{ background: `radial-gradient(50% 100% at 50% 0%, color-mix(in srgb, ${palette.shadow} 10%, transparent) 0%, transparent 72%)` }} />
5
+ }
@@ -0,0 +1,23 @@
1
+ import type { CSSProperties, ReactElement, ReactNode } from 'react'
2
+
3
+ /**
4
+ * The layer stack the active-idle styles animate: travel (x), lift (y), body (squash), trail (jets).
5
+ * ComposerMascot tags its own wrappers with these classes instead of rendering this stage.
6
+ */
7
+ export function MascotIdleStage({ className, style, children }: {
8
+ className?: string
9
+ style?: CSSProperties
10
+ children: ReactNode
11
+ }): ReactElement {
12
+ return (
13
+ <div className={className ? `composer-mascot-stage ${className}` : 'composer-mascot-stage'} style={style}>
14
+ <div className="composer-mascot-travel">
15
+ <div className="composer-mascot-lift">
16
+ <div className="composer-mascot-body">
17
+ <div className="composer-mascot-trail">{children}</div>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ )
23
+ }