@umituz/react-native-mascot 1.0.4 → 1.0.7

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 (39) hide show
  1. package/README.md +60 -0
  2. package/package.json +2 -1
  3. package/src/application/services/AnimationStateManager.ts +69 -0
  4. package/src/application/services/AppearanceManagement.ts +40 -0
  5. package/src/application/services/MascotService.ts +42 -33
  6. package/src/application/services/PersonalityManagement.ts +39 -0
  7. package/src/application/services/StateHistory.ts +55 -0
  8. package/src/application/services/StateMachine.ts +154 -0
  9. package/src/application/services/StateTransitions.ts +73 -0
  10. package/src/application.ts +40 -0
  11. package/src/assets/index.ts +14 -19
  12. package/src/core.ts +62 -0
  13. package/src/domain/entities/Mascot.ts +186 -127
  14. package/src/domain/types/AnimationStateTypes.ts +148 -0
  15. package/src/domain/types/MascotTypes.ts +9 -0
  16. package/src/domain/value-objects/AnimationState.ts +126 -0
  17. package/src/index.ts +9 -99
  18. package/src/infrastructure/controllers/AnimationController.ts +26 -122
  19. package/src/infrastructure/controllers/AnimationPlayer.ts +104 -0
  20. package/src/infrastructure/controllers/AnimationTimer.ts +62 -0
  21. package/src/infrastructure/controllers/EventManager.ts +108 -0
  22. package/src/infrastructure/di/Container.ts +73 -10
  23. package/src/infrastructure/managers/AssetManager.ts +134 -63
  24. package/src/infrastructure/managers/MascotBuilder.ts +89 -0
  25. package/src/infrastructure/managers/MascotFactory.ts +24 -176
  26. package/src/infrastructure/managers/MascotTemplates.ts +151 -0
  27. package/src/infrastructure/utils/LRUCache.ts +218 -0
  28. package/src/infrastructure.ts +24 -0
  29. package/src/presentation/components/LottieMascot.tsx +85 -0
  30. package/src/presentation/components/MascotView.tsx +42 -233
  31. package/src/presentation/components/SVGMascot.tsx +61 -0
  32. package/src/presentation/contexts/MascotContext.tsx +2 -3
  33. package/src/presentation/hooks/useMascot.ts +118 -39
  34. package/src/presentation/hooks/useMascotAnimation.ts +9 -15
  35. package/src/presentation/hooks/useMascotState.ts +213 -0
  36. package/src/presentation.ts +37 -0
  37. package/src/types.d.ts +4 -0
  38. package/src/application/index.ts +0 -8
  39. package/src/domain/value-objects/index.ts +0 -9
@@ -1,10 +1,18 @@
1
1
  /**
2
- * Mascot Factory
3
- * Creates pre-configured mascot instances
2
+ * Mascot Factory (100 lines)
3
+ * Factory methods for mascot creation
4
4
  */
5
5
 
6
6
  import { Mascot } from '../../domain/entities/Mascot';
7
7
  import type { MascotConfig, MascotType } from '../../domain/types/MascotTypes';
8
+ import { getTemplateConfig } from './MascotTemplates';
9
+ import { MascotBuilder } from './MascotBuilder';
10
+
11
+ export type MascotTemplate =
12
+ | 'friendly-bot'
13
+ | 'cute-pet'
14
+ | 'wise-owl'
15
+ | 'pixel-hero';
8
16
 
9
17
  export class MascotFactory {
10
18
  /**
@@ -14,7 +22,7 @@ export class MascotFactory {
14
22
  template: MascotTemplate,
15
23
  customizations?: Partial<MascotConfig>
16
24
  ): Mascot {
17
- const config = this._getTemplateConfig(template);
25
+ const config = getTemplateConfig(template);
18
26
  const finalConfig = this._mergeConfigs(config, customizations);
19
27
  return new Mascot(finalConfig);
20
28
  }
@@ -35,136 +43,23 @@ export class MascotFactory {
35
43
  type?: MascotType;
36
44
  baseColor?: string;
37
45
  }): Mascot {
38
- const config: MascotConfig = {
39
- id: options.id || 'simple-mascot',
40
- name: options.name || 'Simple Mascot',
41
- type: options.type || 'svg',
42
- personality: {
43
- mood: 'happy',
44
- energy: 0.7,
45
- friendliness: 0.8,
46
- playfulness: 0.6,
47
- },
48
- appearance: {
49
- baseColor: options.baseColor || '#FF6B6B',
50
- accentColor: '#4ECDC4',
51
- accessories: [],
52
- style: 'minimal',
53
- scale: 1,
54
- },
55
- animations: this._getDefaultAnimations(),
56
- interactive: true,
57
- touchEnabled: true,
58
- soundEnabled: false,
59
- };
60
-
61
- return new Mascot(config);
46
+ const builder = new MascotBuilder(options);
47
+ if (options.baseColor) {
48
+ builder.withBaseColor(options.baseColor);
49
+ }
50
+ return new Mascot(builder.build());
62
51
  }
63
52
 
64
- // Private Methods
65
- private static _getTemplateConfig(template: MascotTemplate): MascotConfig {
66
- const templates: Record<MascotTemplate, MascotConfig> = {
67
- 'friendly-bot': {
68
- id: 'friendly-bot',
69
- name: 'Friendly Bot',
70
- type: 'lottie',
71
- personality: {
72
- mood: 'happy',
73
- energy: 0.8,
74
- friendliness: 0.9,
75
- playfulness: 0.7,
76
- },
77
- appearance: {
78
- baseColor: '#4A90E2',
79
- accentColor: '#50E3C2',
80
- accessories: [],
81
- style: 'cartoon',
82
- scale: 1,
83
- },
84
- animations: this._getDefaultAnimations(),
85
- interactive: true,
86
- touchEnabled: true,
87
- soundEnabled: false,
88
- },
89
- 'cute-pet': {
90
- id: 'cute-pet',
91
- name: 'Cute Pet',
92
- type: 'svg',
93
- personality: {
94
- mood: 'excited',
95
- energy: 0.9,
96
- friendliness: 1.0,
97
- playfulness: 0.95,
98
- },
99
- appearance: {
100
- baseColor: '#FFB6C1',
101
- accentColor: '#FF69B4',
102
- secondaryColor: '#FFF',
103
- accessories: [
104
- { id: 'bow', type: 'bow', color: '#FF69B4', visible: true },
105
- ],
106
- style: 'cartoon',
107
- scale: 1,
108
- },
109
- animations: this._getDefaultAnimations(),
110
- interactive: true,
111
- touchEnabled: true,
112
- soundEnabled: false,
113
- },
114
- 'wise-owl': {
115
- id: 'wise-owl',
116
- name: 'Wise Owl',
117
- type: 'lottie',
118
- personality: {
119
- mood: 'thinking',
120
- energy: 0.4,
121
- friendliness: 0.7,
122
- playfulness: 0.3,
123
- },
124
- appearance: {
125
- baseColor: '#8B4513',
126
- accentColor: '#DAA520',
127
- accessories: [
128
- { id: 'glasses', type: 'glasses', visible: true },
129
- ],
130
- style: 'realistic',
131
- scale: 1,
132
- },
133
- animations: this._getDefaultAnimations(),
134
- interactive: true,
135
- touchEnabled: true,
136
- soundEnabled: false,
137
- },
138
- 'pixel-hero': {
139
- id: 'pixel-hero',
140
- name: 'Pixel Hero',
141
- type: 'svg',
142
- personality: {
143
- mood: 'happy',
144
- energy: 0.85,
145
- friendliness: 0.75,
146
- playfulness: 0.8,
147
- },
148
- appearance: {
149
- baseColor: '#3498DB',
150
- accentColor: '#E74C3C',
151
- secondaryColor: '#F1C40F',
152
- accessories: [
153
- { id: 'crown', type: 'crown', color: '#F1C40F', visible: true },
154
- ],
155
- style: 'pixel',
156
- scale: 1,
157
- },
158
- animations: this._getDefaultAnimations(),
159
- interactive: true,
160
- touchEnabled: true,
161
- soundEnabled: false,
162
- },
163
- };
164
-
165
- return templates[template];
53
+ /**
54
+ * Get a builder for fluent mascot creation
55
+ */
56
+ static builder(): MascotBuilder {
57
+ return new MascotBuilder();
166
58
  }
167
59
 
60
+ /**
61
+ * Merge base config with customizations
62
+ */
168
63
  private static _mergeConfigs(
169
64
  base: MascotConfig,
170
65
  custom?: Partial<MascotConfig>
@@ -189,51 +84,4 @@ export class MascotFactory {
189
84
  animations: custom.animations || base.animations,
190
85
  };
191
86
  }
192
-
193
- private static _getDefaultAnimations() {
194
- return [
195
- {
196
- id: 'idle',
197
- name: 'Idle',
198
- type: 'idle' as const,
199
- source: 'idle.json',
200
- loop: true,
201
- autoplay: true,
202
- },
203
- {
204
- id: 'wave',
205
- name: 'Wave',
206
- type: 'action' as const,
207
- source: 'wave.json',
208
- loop: false,
209
- },
210
- {
211
- id: 'jump',
212
- name: 'Jump',
213
- type: 'action' as const,
214
- source: 'jump.json',
215
- loop: false,
216
- },
217
- {
218
- id: 'success',
219
- name: 'Success',
220
- type: 'reaction' as const,
221
- source: 'success.json',
222
- loop: false,
223
- },
224
- {
225
- id: 'error',
226
- name: 'Error',
227
- type: 'reaction' as const,
228
- source: 'error.json',
229
- loop: false,
230
- },
231
- ];
232
- }
233
87
  }
234
-
235
- export type MascotTemplate =
236
- | 'friendly-bot'
237
- | 'cute-pet'
238
- | 'wise-owl'
239
- | 'pixel-hero';
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Mascot Templates (100 lines)
3
+ * Predefined mascot template configurations
4
+ */
5
+
6
+ import type { MascotConfig } from '../../domain/types/MascotTypes';
7
+ import type { MascotTemplate } from './MascotFactory';
8
+
9
+ export function getTemplateConfig(template: MascotTemplate): MascotConfig {
10
+ const templates: Record<MascotTemplate, MascotConfig> = {
11
+ 'friendly-bot': {
12
+ id: 'friendly-bot',
13
+ name: 'Friendly Bot',
14
+ type: 'lottie',
15
+ personality: {
16
+ mood: 'happy',
17
+ energy: 0.8,
18
+ friendliness: 0.9,
19
+ playfulness: 0.7,
20
+ },
21
+ appearance: {
22
+ baseColor: '#4A90E2',
23
+ accentColor: '#50E3C2',
24
+ accessories: [],
25
+ style: 'cartoon',
26
+ scale: 1,
27
+ },
28
+ animations: getDefaultAnimations(),
29
+ interactive: true,
30
+ touchEnabled: true,
31
+ soundEnabled: false,
32
+ },
33
+ 'cute-pet': {
34
+ id: 'cute-pet',
35
+ name: 'Cute Pet',
36
+ type: 'svg',
37
+ personality: {
38
+ mood: 'excited',
39
+ energy: 0.9,
40
+ friendliness: 1.0,
41
+ playfulness: 0.95,
42
+ },
43
+ appearance: {
44
+ baseColor: '#FFB6C1',
45
+ accentColor: '#FF69B4',
46
+ secondaryColor: '#FFF',
47
+ accessories: [
48
+ { id: 'bow', type: 'bow', color: '#FF69B4', visible: true },
49
+ ],
50
+ style: 'cartoon',
51
+ scale: 1,
52
+ },
53
+ animations: getDefaultAnimations(),
54
+ interactive: true,
55
+ touchEnabled: true,
56
+ soundEnabled: false,
57
+ },
58
+ 'wise-owl': {
59
+ id: 'wise-owl',
60
+ name: 'Wise Owl',
61
+ type: 'lottie',
62
+ personality: {
63
+ mood: 'thinking',
64
+ energy: 0.4,
65
+ friendliness: 0.7,
66
+ playfulness: 0.3,
67
+ },
68
+ appearance: {
69
+ baseColor: '#8B4513',
70
+ accentColor: '#DAA520',
71
+ accessories: [
72
+ { id: 'glasses', type: 'glasses', visible: true },
73
+ ],
74
+ style: 'realistic',
75
+ scale: 1,
76
+ },
77
+ animations: getDefaultAnimations(),
78
+ interactive: true,
79
+ touchEnabled: true,
80
+ soundEnabled: false,
81
+ },
82
+ 'pixel-hero': {
83
+ id: 'pixel-hero',
84
+ name: 'Pixel Hero',
85
+ type: 'svg',
86
+ personality: {
87
+ mood: 'happy',
88
+ energy: 0.85,
89
+ friendliness: 0.75,
90
+ playfulness: 0.8,
91
+ },
92
+ appearance: {
93
+ baseColor: '#3498DB',
94
+ accentColor: '#E74C3C',
95
+ secondaryColor: '#F1C40F',
96
+ accessories: [
97
+ { id: 'crown', type: 'crown', color: '#F1C40F', visible: true },
98
+ ],
99
+ style: 'pixel',
100
+ scale: 1,
101
+ },
102
+ animations: getDefaultAnimations(),
103
+ interactive: true,
104
+ touchEnabled: true,
105
+ soundEnabled: false,
106
+ },
107
+ };
108
+
109
+ return templates[template];
110
+ }
111
+
112
+ export function getDefaultAnimations() {
113
+ return [
114
+ {
115
+ id: 'idle',
116
+ name: 'Idle',
117
+ type: 'idle' as const,
118
+ source: 'idle.json',
119
+ loop: true,
120
+ autoplay: true,
121
+ },
122
+ {
123
+ id: 'wave',
124
+ name: 'Wave',
125
+ type: 'action' as const,
126
+ source: 'wave.json',
127
+ loop: false,
128
+ },
129
+ {
130
+ id: 'jump',
131
+ name: 'Jump',
132
+ type: 'action' as const,
133
+ source: 'jump.json',
134
+ loop: false,
135
+ },
136
+ {
137
+ id: 'success',
138
+ name: 'Success',
139
+ type: 'reaction' as const,
140
+ source: 'success.json',
141
+ loop: false,
142
+ },
143
+ {
144
+ id: 'error',
145
+ name: 'Error',
146
+ type: 'reaction' as const,
147
+ source: 'error.json',
148
+ loop: false,
149
+ },
150
+ ];
151
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * LRU (Least Recently Used) Cache Entry
3
+ * Efficient cache implementation with O(1) operations
4
+ */
5
+
6
+ interface LRUNode<K, V> {
7
+ key: K;
8
+ value: V;
9
+ prev: LRUNode<K, V> | null;
10
+ next: LRUNode<K, V> | null;
11
+ }
12
+
13
+ /**
14
+ * LRU Cache implementation
15
+ * Provides O(1) get/set operations with automatic eviction
16
+ */
17
+ export class LRUCache<K, V> {
18
+ private capacity: number;
19
+ private cache: Map<K, LRUNode<K, V>>;
20
+ private head: LRUNode<K, V> | null;
21
+ private tail: LRUNode<K, V> | null;
22
+ private _size: number;
23
+
24
+ constructor(capacity: number) {
25
+ this.capacity = capacity;
26
+ this.cache = new Map();
27
+ this.head = null;
28
+ this.tail = null;
29
+ this._size = 0;
30
+ }
31
+
32
+ /**
33
+ * Get value by key and move to front (most recently used)
34
+ */
35
+ get(key: K): V | undefined {
36
+ const node = this.cache.get(key);
37
+ if (!node) {
38
+ return undefined;
39
+ }
40
+
41
+ // Move to front (most recently used)
42
+ this.moveToFront(node);
43
+ return node.value;
44
+ }
45
+
46
+ /**
47
+ * Set value and evict least recently used if at capacity
48
+ */
49
+ set(key: K, value: V): void {
50
+ const existingNode = this.cache.get(key);
51
+
52
+ if (existingNode) {
53
+ // Update existing node
54
+ existingNode.value = value;
55
+ this.moveToFront(existingNode);
56
+ return;
57
+ }
58
+
59
+ // Create new node
60
+ const newNode: LRUNode<K, V> = {
61
+ key,
62
+ value,
63
+ prev: null,
64
+ next: null,
65
+ };
66
+
67
+ // Add to cache
68
+ this.cache.set(key, newNode);
69
+ this.addToFront(newNode);
70
+ this._size++;
71
+
72
+ // Evict if at capacity
73
+ if (this._size > this.capacity) {
74
+ this.removeLeastRecentlyUsed();
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Check if key exists
80
+ */
81
+ has(key: K): boolean {
82
+ return this.cache.has(key);
83
+ }
84
+
85
+ /**
86
+ * Delete specific key
87
+ */
88
+ delete(key: K): boolean {
89
+ const node = this.cache.get(key);
90
+ if (!node) {
91
+ return false;
92
+ }
93
+
94
+ this.removeNode(node);
95
+ this.cache.delete(key);
96
+ this._size--;
97
+ return true;
98
+ }
99
+
100
+ /**
101
+ * Clear all entries
102
+ */
103
+ clear(): void {
104
+ this.cache.clear();
105
+ this.head = null;
106
+ this.tail = null;
107
+ this._size = 0;
108
+ }
109
+
110
+ /**
111
+ * Get current size
112
+ */
113
+ size(): number {
114
+ return this._size;
115
+ }
116
+
117
+ /**
118
+ * Get all keys in order (most recent to least recent)
119
+ */
120
+ keys(): K[] {
121
+ const keys: K[] = [];
122
+ let current = this.head;
123
+ while (current) {
124
+ keys.push(current.key);
125
+ current = current.next;
126
+ }
127
+ return keys;
128
+ }
129
+
130
+ /**
131
+ * Get cache statistics
132
+ */
133
+ getStats(): { size: number; capacity: number; usage: number } {
134
+ return {
135
+ size: this._size,
136
+ capacity: this.capacity,
137
+ usage: this._size / this.capacity,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Add node to front of list (most recently used)
143
+ */
144
+ private addToFront(node: LRUNode<K, V>): void {
145
+ node.prev = null;
146
+ node.next = this.head;
147
+
148
+ if (this.head) {
149
+ this.head.prev = node;
150
+ }
151
+
152
+ this.head = node;
153
+
154
+ if (!this.tail) {
155
+ this.tail = node;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Move existing node to front
161
+ */
162
+ private moveToFront(node: LRUNode<K, V>): void {
163
+ if (node === this.head) {
164
+ return;
165
+ }
166
+
167
+ // Remove node from current position
168
+ this.removeNode(node);
169
+
170
+ // Add to front
171
+ node.prev = null;
172
+ node.next = this.head;
173
+
174
+ if (this.head) {
175
+ this.head.prev = node;
176
+ }
177
+
178
+ this.head = node;
179
+
180
+ // If node was tail, update tail
181
+ if (this.tail === null) {
182
+ this.tail = node;
183
+ } else if (this.tail.prev === node) {
184
+ this.tail = node;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Remove node from list (without deleting from cache)
190
+ */
191
+ private removeNode(node: LRUNode<K, V>): void {
192
+ if (node.prev) {
193
+ node.prev.next = node.next;
194
+ } else {
195
+ this.head = node.next;
196
+ }
197
+
198
+ if (node.next) {
199
+ node.next.prev = node.prev;
200
+ } else {
201
+ this.tail = node.prev;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Remove least recently used (tail) node
207
+ */
208
+ private removeLeastRecentlyUsed(): void {
209
+ if (!this.tail) {
210
+ return;
211
+ }
212
+
213
+ const lruKey = this.tail.key;
214
+ this.removeNode(this.tail);
215
+ this.cache.delete(lruKey);
216
+ this._size--;
217
+ }
218
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Infrastructure Layer Exports (60 lines)
3
+ * Controllers, repositories, managers, and utilities
4
+ */
5
+
6
+ // Infrastructure - DI
7
+ export { DIContainer } from './infrastructure/di/Container';
8
+
9
+ // Infrastructure - Repositories
10
+ export { MascotRepository } from './infrastructure/repositories/MascotRepository';
11
+
12
+ // Infrastructure - Controllers
13
+ export { AnimationController } from './infrastructure/controllers/AnimationController';
14
+ export { AnimationPlayer } from './infrastructure/controllers/AnimationPlayer';
15
+ export { EventManager } from './infrastructure/controllers/EventManager';
16
+ export { AnimationTimer } from './infrastructure/controllers/AnimationTimer';
17
+
18
+ // Infrastructure - Managers
19
+ export { AssetManager } from './infrastructure/managers/AssetManager';
20
+ export { MascotFactory, type MascotTemplate as FactoryMascotTemplate } from './infrastructure/managers/MascotFactory';
21
+ export { MascotBuilder } from './infrastructure/managers/MascotBuilder';
22
+
23
+ // Infrastructure - Utils
24
+ export { LRUCache } from './infrastructure/utils/LRUCache';
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Lottie Mascot Component (80 lines)
3
+ * Lottie-specific rendering with auto-play and caching
4
+ */
5
+
6
+ import React, { memo, useMemo, useRef, useEffect } from 'react';
7
+ import { View, ViewStyle } from 'react-native';
8
+ import LottieView from 'lottie-react-native';
9
+ import type { AnimationObject } from 'lottie-react-native';
10
+ import type { Mascot } from '../../domain/entities/Mascot';
11
+ import type { MascotAnimation } from '../../domain/types/MascotTypes';
12
+
13
+ type LottieAnimationSource = string | AnimationObject | { uri: string };
14
+
15
+ export interface LottieMascotProps {
16
+ mascot: Mascot;
17
+ animation?: MascotAnimation | null;
18
+ resizeMode?: 'cover' | 'contain' | 'center';
19
+ onAnimationFinish?: () => void;
20
+ }
21
+
22
+ const LottieMascotComponent: React.FC<LottieMascotProps> = memo(({
23
+ mascot,
24
+ animation,
25
+ resizeMode = 'contain',
26
+ onAnimationFinish,
27
+ }) => {
28
+ const lottieRef = useRef<LottieView>(null);
29
+
30
+ // Memoize source to prevent reloads
31
+ const source = useMemo(() => {
32
+ return animation?.source || mascot.animations.find((a: MascotAnimation) => a.type === 'idle')?.source;
33
+ }, [animation?.source, mascot.animations]);
34
+
35
+ // Play animation when source changes
36
+ useEffect(() => {
37
+ if (source && lottieRef.current) {
38
+ lottieRef.current.play();
39
+ }
40
+ }, [source]);
41
+
42
+ if (!source) {
43
+ return (
44
+ <FallbackMascot
45
+ mascot={mascot}
46
+ size={0}
47
+ style={{ width: '100%', height: '100%', backgroundColor: mascot.appearance.baseColor }}
48
+ />
49
+ );
50
+ }
51
+
52
+ return (
53
+ <LottieView
54
+ ref={lottieRef}
55
+ source={source as LottieAnimationSource}
56
+ style={{ width: '100%', height: '100%' }}
57
+ resizeMode={resizeMode}
58
+ autoPlay={animation?.autoplay}
59
+ loop={animation?.loop}
60
+ onAnimationFinish={onAnimationFinish}
61
+ />
62
+ );
63
+ });
64
+
65
+ LottieMascotComponent.displayName = 'LottieMascot';
66
+
67
+ export const LottieMascot = LottieMascotComponent;
68
+
69
+ // Fallback component for when no source is available
70
+ interface FallbackMascotProps {
71
+ mascot: Mascot;
72
+ size?: number;
73
+ style?: ViewStyle;
74
+ }
75
+
76
+ const FallbackMascotComponent: React.FC<FallbackMascotProps> = ({ mascot, style }) => (
77
+ <View style={[{ backgroundColor: mascot.appearance.baseColor, borderRadius: 100, justifyContent: 'center', alignItems: 'center' }, style]}>
78
+ <View style={{ width: 10, height: 10, backgroundColor: '#000', borderRadius: 5, top: '40%', position: 'absolute' }} />
79
+ <View style={{ width: 10, height: 10, backgroundColor: '#000', borderRadius: 5, top: '40%', position: 'absolute' }} />
80
+ </View>
81
+ );
82
+
83
+ FallbackMascotComponent.displayName = 'FallbackMascot';
84
+
85
+ export const FallbackMascot = memo(FallbackMascotComponent);