react-achievements 3.9.3 → 4.0.0

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 (52) hide show
  1. package/README.md +148 -101
  2. package/dist/headless.cjs +317 -0
  3. package/dist/headless.cjs.map +1 -0
  4. package/dist/headless.d.ts +176 -0
  5. package/dist/headless.esm.js +222 -0
  6. package/dist/headless.esm.js.map +1 -0
  7. package/dist/index.cjs +839 -881
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +163 -153
  10. package/dist/index.esm.js +835 -883
  11. package/dist/index.esm.js.map +1 -1
  12. package/dist/web.cjs +1416 -0
  13. package/dist/web.cjs.map +1 -0
  14. package/dist/web.d.ts +534 -0
  15. package/dist/web.esm.js +1306 -0
  16. package/dist/web.esm.js.map +1 -0
  17. package/package.json +13 -28
  18. package/dist/types/__mocks__/confetti-wrapper.d.ts +0 -5
  19. package/dist/types/__mocks__/react-confetti.d.ts +0 -3
  20. package/dist/types/__mocks__/react-toastify.d.ts +0 -13
  21. package/dist/types/core/components/BadgesButton.d.ts +0 -25
  22. package/dist/types/core/components/BadgesButtonWithModal.d.ts +0 -53
  23. package/dist/types/core/components/BadgesModal.d.ts +0 -14
  24. package/dist/types/core/components/ConfettiWrapper.d.ts +0 -6
  25. package/dist/types/core/errors/AchievementErrors.d.ts +0 -55
  26. package/dist/types/core/hooks/useWindowSize.d.ts +0 -16
  27. package/dist/types/core/icons/defaultIcons.d.ts +0 -8
  28. package/dist/types/core/storage/AsyncStorageAdapter.d.ts +0 -48
  29. package/dist/types/core/storage/IndexedDBStorage.d.ts +0 -29
  30. package/dist/types/core/storage/LocalStorage.d.ts +0 -16
  31. package/dist/types/core/storage/MemoryStorage.d.ts +0 -11
  32. package/dist/types/core/storage/OfflineQueueStorage.d.ts +0 -42
  33. package/dist/types/core/storage/RestApiStorage.d.ts +0 -20
  34. package/dist/types/core/styles/defaultStyles.d.ts +0 -2
  35. package/dist/types/core/types.d.ts +0 -115
  36. package/dist/types/core/ui/BuiltInConfetti.d.ts +0 -7
  37. package/dist/types/core/ui/BuiltInModal.d.ts +0 -7
  38. package/dist/types/core/ui/BuiltInNotification.d.ts +0 -7
  39. package/dist/types/core/ui/LegacyWrappers.d.ts +0 -21
  40. package/dist/types/core/ui/interfaces.d.ts +0 -127
  41. package/dist/types/core/ui/legacyDetector.d.ts +0 -40
  42. package/dist/types/core/ui/themes.d.ts +0 -14
  43. package/dist/types/core/utils/configNormalizer.d.ts +0 -3
  44. package/dist/types/core/utils/dataExport.d.ts +0 -34
  45. package/dist/types/core/utils/dataImport.d.ts +0 -50
  46. package/dist/types/hooks/useAchievementEngine.d.ts +0 -36
  47. package/dist/types/hooks/useAchievements.d.ts +0 -1
  48. package/dist/types/hooks/useSimpleAchievements.d.ts +0 -63
  49. package/dist/types/index.d.ts +0 -36
  50. package/dist/types/providers/AchievementProvider.d.ts +0 -47
  51. package/dist/types/setupTests.d.ts +0 -1
  52. package/dist/types/utils/achievementHelpers.d.ts +0 -135
@@ -0,0 +1,1306 @@
1
+ import { AchievementEngine } from 'achievements-engine';
2
+ export { AchievementBuilder, AchievementEngine, AchievementError, AsyncStorageAdapter, ConfigurationError, ImportValidationError, IndexedDBStorage, LocalStorage, MemoryStorage, OfflineQueueStorage, RestApiStorage, StorageError, StorageQuotaError, StorageType, SyncError, createConfigHash, exportAchievementData, importAchievementData, isAchievementError, isRecoverableError, isSimpleConfig, normalizeAchievements } from 'achievements-engine';
3
+ import React, { createContext, useState, useCallback, useEffect, useContext, useMemo, useRef } from 'react';
4
+
5
+ // Type guard to detect async storage
6
+ function isAsyncStorage(storage) {
7
+ // Check if methods return Promises
8
+ const testResult = storage.getMetrics();
9
+ return testResult && typeof testResult.then === 'function';
10
+ }
11
+ var StorageType;
12
+ (function (StorageType) {
13
+ StorageType["Local"] = "local";
14
+ StorageType["Memory"] = "memory";
15
+ StorageType["IndexedDB"] = "indexeddb";
16
+ StorageType["RestAPI"] = "restapi"; // Asynchronous REST API storage
17
+ })(StorageType || (StorageType = {}));
18
+
19
+ /******************************************************************************
20
+ Copyright (c) Microsoft Corporation.
21
+
22
+ Permission to use, copy, modify, and/or distribute this software for any
23
+ purpose with or without fee is hereby granted.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
26
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
27
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
28
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
29
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
30
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
31
+ PERFORMANCE OF THIS SOFTWARE.
32
+ ***************************************************************************** */
33
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
34
+
35
+
36
+ function __rest(s, e) {
37
+ var t = {};
38
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
39
+ t[p] = s[p];
40
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
41
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
42
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
43
+ t[p[i]] = s[p[i]];
44
+ }
45
+ return t;
46
+ }
47
+
48
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
49
+ var e = new Error(message);
50
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
51
+ };
52
+
53
+ const warnedMessages = new Set();
54
+ function warnDeprecation(message) {
55
+ var _a, _b;
56
+ const isProduction = typeof globalThis !== 'undefined' &&
57
+ ((_b = (_a = globalThis.process) === null || _a === void 0 ? void 0 : _a.env) === null || _b === void 0 ? void 0 : _b.NODE_ENV) === 'production';
58
+ if (isProduction || warnedMessages.has(message)) {
59
+ return;
60
+ }
61
+ warnedMessages.add(message);
62
+ console.warn(`[react-achievements] ${message}`);
63
+ }
64
+
65
+ const AchievementContext = createContext(undefined);
66
+ const getAllAchievementRecord = (engine) => {
67
+ return Object.fromEntries(engine.getAllAchievements().map((achievement) => [
68
+ achievement.achievementId,
69
+ achievement,
70
+ ]));
71
+ };
72
+ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'local', children, onError, useBuiltInUI, restApiConfig, engine: externalEngine, eventMapping, icons = {}, }) => {
73
+ if (useBuiltInUI !== undefined) {
74
+ warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 4.2.');
75
+ }
76
+ if (achievementsConfig && externalEngine) {
77
+ throw new Error('Cannot provide both "achievements" and "engine" props to AchievementProvider.\n\n' +
78
+ 'Choose one pattern:\n' +
79
+ '1. Direct metric tracking: <AchievementProvider achievements={config}>\n' +
80
+ '2. Event-based tracking: <AchievementProvider engine={myEngine}>');
81
+ }
82
+ const isProviderCreatedEngine = Boolean(achievementsConfig);
83
+ const [engine] = useState(() => {
84
+ if (externalEngine) {
85
+ return externalEngine;
86
+ }
87
+ if (!achievementsConfig) {
88
+ throw new Error('AchievementProvider requires either "achievements" or "engine" prop.\n\n' +
89
+ '1. Direct metric tracking: <AchievementProvider achievements={config}>\n' +
90
+ '2. Event-based tracking: <AchievementProvider engine={myEngine}>');
91
+ }
92
+ return new AchievementEngine({
93
+ achievements: achievementsConfig,
94
+ storage: storage,
95
+ restApiConfig,
96
+ onError: onError,
97
+ eventMapping,
98
+ });
99
+ });
100
+ const [achievementState, setAchievementState] = useState(() => ({
101
+ unlocked: [...engine.getUnlocked()],
102
+ all: getAllAchievementRecord(engine),
103
+ }));
104
+ const syncAchievementState = useCallback(() => {
105
+ setAchievementState({
106
+ unlocked: [...engine.getUnlocked()],
107
+ all: getAllAchievementRecord(engine),
108
+ });
109
+ }, [engine]);
110
+ useEffect(() => {
111
+ return () => {
112
+ if (!externalEngine) {
113
+ engine.destroy();
114
+ }
115
+ };
116
+ }, [engine, externalEngine]);
117
+ useEffect(() => {
118
+ const unsubscribeUnlocked = engine.on('achievement:unlocked', syncAchievementState);
119
+ const unsubscribeStateChanged = engine.on('state:changed', syncAchievementState);
120
+ return () => {
121
+ unsubscribeUnlocked();
122
+ unsubscribeStateChanged();
123
+ };
124
+ }, [engine, syncAchievementState]);
125
+ const update = (newMetrics) => {
126
+ engine.update(newMetrics);
127
+ };
128
+ const reset = () => {
129
+ engine.reset();
130
+ syncAchievementState();
131
+ };
132
+ const getState = () => {
133
+ const metrics = engine.getMetrics();
134
+ const unlocked = engine.getUnlocked();
135
+ const metricsInArrayFormat = {};
136
+ Object.entries(metrics).forEach(([key, value]) => {
137
+ metricsInArrayFormat[key] = Array.isArray(value) ? value : [value];
138
+ });
139
+ return {
140
+ metrics: metricsInArrayFormat,
141
+ unlocked: [...unlocked],
142
+ };
143
+ };
144
+ const exportData = () => {
145
+ return engine.export();
146
+ };
147
+ const importData = (jsonString, options) => {
148
+ const result = engine.import(jsonString, options);
149
+ syncAchievementState();
150
+ return result;
151
+ };
152
+ const getAllAchievements = () => {
153
+ return engine.getAllAchievements();
154
+ };
155
+ return (React.createElement(AchievementContext.Provider, { value: {
156
+ update,
157
+ achievements: achievementState,
158
+ reset,
159
+ getState,
160
+ exportData,
161
+ importData,
162
+ getAllAchievements,
163
+ engine,
164
+ icons,
165
+ _isLegacyPattern: isProviderCreatedEngine,
166
+ } }, children));
167
+ };
168
+
169
+ /**
170
+ * Access the active AchievementEngine instance.
171
+ *
172
+ * In v4 this works with both provider-created engines (`achievements` prop) and
173
+ * injected engines (`engine` prop).
174
+ */
175
+ const useAchievementEngine = () => {
176
+ const context = useContext(AchievementContext);
177
+ if (!context) {
178
+ throw new Error('useAchievementEngine must be used within an AchievementProvider.\n\n' +
179
+ 'Wrap your component tree:\n' +
180
+ '<AchievementProvider achievements={achievements}>\n' +
181
+ ' <YourComponent />\n' +
182
+ '</AchievementProvider>');
183
+ }
184
+ return context.engine;
185
+ };
186
+
187
+ /**
188
+ * Built-in theme presets
189
+ */
190
+ const builtInThemes = {
191
+ /**
192
+ * Modern theme - Dark gradients with vibrant accents
193
+ * Inspired by contemporary achievement systems (Discord, Steam, Xbox)
194
+ */
195
+ modern: {
196
+ name: 'modern',
197
+ notification: {
198
+ background: 'linear-gradient(135deg, rgba(30, 30, 50, 0.98) 0%, rgba(50, 50, 70, 0.98) 100%)',
199
+ textColor: '#ffffff',
200
+ accentColor: '#4CAF50',
201
+ borderRadius: '12px',
202
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
203
+ fontSize: {
204
+ header: '12px',
205
+ title: '18px',
206
+ description: '14px',
207
+ },
208
+ },
209
+ modal: {
210
+ overlayColor: 'rgba(0, 0, 0, 0.85)',
211
+ background: 'linear-gradient(135deg, #1e1e32 0%, #323246 100%)',
212
+ textColor: '#ffffff',
213
+ accentColor: '#4CAF50',
214
+ borderRadius: '16px',
215
+ headerFontSize: '28px',
216
+ },
217
+ confetti: {
218
+ colors: ['#FFD700', '#4CAF50', '#2196F3', '#FF6B6B'],
219
+ particleCount: 50,
220
+ shapes: ['circle', 'square'],
221
+ },
222
+ },
223
+ /**
224
+ * Minimal theme - Clean, light design with subtle accents
225
+ * Perfect for professional or minimalist applications
226
+ */
227
+ minimal: {
228
+ name: 'minimal',
229
+ notification: {
230
+ background: 'rgba(255, 255, 255, 0.98)',
231
+ textColor: '#333333',
232
+ accentColor: '#4CAF50',
233
+ borderRadius: '8px',
234
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
235
+ fontSize: {
236
+ header: '11px',
237
+ title: '16px',
238
+ description: '13px',
239
+ },
240
+ },
241
+ modal: {
242
+ overlayColor: 'rgba(0, 0, 0, 0.5)',
243
+ background: '#ffffff',
244
+ textColor: '#333333',
245
+ accentColor: '#4CAF50',
246
+ borderRadius: '12px',
247
+ headerFontSize: '24px',
248
+ },
249
+ confetti: {
250
+ colors: ['#4CAF50', '#2196F3'],
251
+ particleCount: 30,
252
+ shapes: ['circle'],
253
+ },
254
+ },
255
+ /**
256
+ * Gamified theme - Modern gaming aesthetic with sci-fi colors
257
+ * Dark navy backgrounds with cyan and orange accents (2024 gaming trend)
258
+ * Features square/badge-shaped achievement cards
259
+ */
260
+ gamified: {
261
+ name: 'gamified',
262
+ notification: {
263
+ background: 'linear-gradient(135deg, rgba(5, 8, 22, 0.98) 0%, rgba(15, 23, 42, 0.98) 100%)',
264
+ textColor: '#22d3ee', // Bright cyan
265
+ accentColor: '#f97316', // Bright orange
266
+ borderRadius: '6px',
267
+ boxShadow: '0 8px 32px rgba(34, 211, 238, 0.4), 0 0 20px rgba(249, 115, 22, 0.3)',
268
+ fontSize: {
269
+ header: '13px',
270
+ title: '20px',
271
+ description: '15px',
272
+ },
273
+ },
274
+ modal: {
275
+ overlayColor: 'rgba(5, 8, 22, 0.85)',
276
+ background: 'linear-gradient(135deg, #0f172a 0%, #050816 100%)',
277
+ textColor: '#22d3ee', // Bright cyan
278
+ accentColor: '#f97316', // Bright orange
279
+ borderRadius: '8px',
280
+ headerFontSize: '32px',
281
+ achievementCardBorderRadius: '8px', // Square badge-like cards
282
+ achievementLayout: 'badge', // Use badge/grid layout instead of horizontal list
283
+ },
284
+ confetti: {
285
+ colors: ['#22d3ee', '#f97316', '#a855f7', '#eab308'], // Cyan, orange, purple, yellow
286
+ particleCount: 100,
287
+ shapes: ['circle', 'square'],
288
+ },
289
+ },
290
+ };
291
+ /**
292
+ * Retrieve a theme by name (internal use only)
293
+ * Only checks built-in themes
294
+ *
295
+ * @param name - Theme name (built-in only)
296
+ * @returns Theme configuration or undefined if not found
297
+ * @internal
298
+ */
299
+ function getTheme(name) {
300
+ return builtInThemes[name];
301
+ }
302
+
303
+ const defaultAchievementIcons = {
304
+ // Essential fallback icons for system use
305
+ default: '⭐', // Fallback when no icon is provided
306
+ loading: '⏳', // For loading states
307
+ error: '⚠️', // For error states
308
+ success: '✅', // For success states
309
+ // A few common icons for backward compatibility
310
+ trophy: '🏆',
311
+ star: '⭐',
312
+ };
313
+
314
+ /**
315
+ * Built-in notification component
316
+ * Modern, theme-aware achievement notification with smooth animations
317
+ */
318
+ const BuiltInNotification = ({ achievement, onClose, duration = 5000, position = 'top-center', theme = 'modern', icons = {}, stackIndex = 0, }) => {
319
+ var _a, _b, _c;
320
+ const [isVisible, setIsVisible] = useState(false);
321
+ const [isExiting, setIsExiting] = useState(false);
322
+ // Merge custom icons with defaults
323
+ const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
324
+ // Get theme configuration
325
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
326
+ const { notification: themeStyles } = themeConfig;
327
+ useEffect(() => {
328
+ // Slide in animation
329
+ const showTimer = setTimeout(() => setIsVisible(true), 10);
330
+ // Auto-dismiss
331
+ const dismissTimer = setTimeout(() => {
332
+ setIsExiting(true);
333
+ setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
334
+ }, duration);
335
+ return () => {
336
+ clearTimeout(showTimer);
337
+ clearTimeout(dismissTimer);
338
+ };
339
+ }, [duration, onClose]);
340
+ const getPositionStyles = () => {
341
+ const stackedOffset = 20 + stackIndex * 104;
342
+ const base = {
343
+ position: 'fixed',
344
+ zIndex: 9999,
345
+ };
346
+ switch (position) {
347
+ case 'top-center':
348
+ return Object.assign(Object.assign({}, base), { top: stackedOffset, left: '50%', transform: 'translateX(-50%)' });
349
+ case 'top-left':
350
+ return Object.assign(Object.assign({}, base), { top: stackedOffset, left: 20 });
351
+ case 'top-right':
352
+ return Object.assign(Object.assign({}, base), { top: stackedOffset, right: 20 });
353
+ case 'bottom-center':
354
+ return Object.assign(Object.assign({}, base), { bottom: stackedOffset, left: '50%', transform: 'translateX(-50%)' });
355
+ case 'bottom-left':
356
+ return Object.assign(Object.assign({}, base), { bottom: stackedOffset, left: 20 });
357
+ case 'bottom-right':
358
+ return Object.assign(Object.assign({}, base), { bottom: stackedOffset, right: 20 });
359
+ default:
360
+ return Object.assign(Object.assign({}, base), { top: stackedOffset, left: '50%', transform: 'translateX(-50%)' });
361
+ }
362
+ };
363
+ const containerStyles = Object.assign(Object.assign({}, getPositionStyles()), { background: themeStyles.background, borderRadius: themeStyles.borderRadius, boxShadow: themeStyles.boxShadow, padding: '16px 24px', minWidth: '320px', maxWidth: '500px', display: 'flex', alignItems: 'center', gap: '16px', opacity: isVisible && !isExiting ? 1 : 0, transform: position.startsWith('top')
364
+ ? `translateY(${isVisible && !isExiting ? '0' : '-20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`
365
+ : `translateY(${isVisible && !isExiting ? '0' : '20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`, transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', pointerEvents: isVisible ? 'auto' : 'none' });
366
+ const iconStyles = {
367
+ fontSize: '48px',
368
+ lineHeight: 1,
369
+ flexShrink: 0,
370
+ };
371
+ const contentStyles = {
372
+ flex: 1,
373
+ color: themeStyles.textColor,
374
+ minWidth: 0,
375
+ };
376
+ const headerStyles = {
377
+ fontSize: ((_a = themeStyles.fontSize) === null || _a === void 0 ? void 0 : _a.header) || '12px',
378
+ textTransform: 'uppercase',
379
+ letterSpacing: '1px',
380
+ opacity: 0.8,
381
+ marginBottom: '4px',
382
+ color: themeStyles.accentColor,
383
+ fontWeight: 600,
384
+ };
385
+ const titleStyles = {
386
+ fontSize: ((_b = themeStyles.fontSize) === null || _b === void 0 ? void 0 : _b.title) || '18px',
387
+ fontWeight: 'bold',
388
+ marginBottom: '4px',
389
+ overflow: 'hidden',
390
+ textOverflow: 'ellipsis',
391
+ whiteSpace: 'nowrap',
392
+ };
393
+ const descriptionStyles = {
394
+ fontSize: ((_c = themeStyles.fontSize) === null || _c === void 0 ? void 0 : _c.description) || '14px',
395
+ opacity: 0.9,
396
+ overflow: 'hidden',
397
+ textOverflow: 'ellipsis',
398
+ display: '-webkit-box',
399
+ WebkitLineClamp: 2,
400
+ WebkitBoxOrient: 'vertical',
401
+ };
402
+ const closeButtonStyles = {
403
+ background: 'none',
404
+ border: 'none',
405
+ color: themeStyles.textColor,
406
+ fontSize: '24px',
407
+ cursor: 'pointer',
408
+ opacity: 0.6,
409
+ transition: 'opacity 0.2s',
410
+ padding: '4px',
411
+ flexShrink: 0,
412
+ lineHeight: 1,
413
+ };
414
+ // If achievementIconKey exists but not in mergedIcons, use it directly (might be an emoji)
415
+ // Otherwise, look up in mergedIcons or fall back to default
416
+ const icon = (achievement.achievementIconKey &&
417
+ mergedIcons[achievement.achievementIconKey]) ||
418
+ achievement.achievementIconKey ||
419
+ mergedIcons.default ||
420
+ '⭐';
421
+ return (React.createElement("div", { style: containerStyles, "data-testid": "built-in-notification" },
422
+ React.createElement("div", { style: iconStyles }, icon),
423
+ React.createElement("div", { style: contentStyles },
424
+ React.createElement("div", { style: headerStyles }, "Achievement Unlocked!"),
425
+ React.createElement("div", { style: titleStyles }, achievement.achievementTitle),
426
+ achievement.achievementDescription && (React.createElement("div", { style: descriptionStyles }, achievement.achievementDescription))),
427
+ React.createElement("button", { onClick: () => {
428
+ setIsExiting(true);
429
+ setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
430
+ }, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close notification" }, "\u00D7")));
431
+ };
432
+
433
+ /**
434
+ * Hook to track window dimensions
435
+ * Replacement for react-use's useWindowSize
436
+ *
437
+ * @returns Object with width and height properties
438
+ *
439
+ * @example
440
+ * ```tsx
441
+ * const { width, height } = useWindowSize();
442
+ * console.log(`Window size: ${width}x${height}`);
443
+ * ```
444
+ */
445
+ function useWindowSize() {
446
+ const [size, setSize] = useState({
447
+ width: typeof window !== 'undefined' ? window.innerWidth : 0,
448
+ height: typeof window !== 'undefined' ? window.innerHeight : 0,
449
+ });
450
+ useEffect(() => {
451
+ // Handle SSR - window may not be defined
452
+ if (typeof window === 'undefined') {
453
+ return;
454
+ }
455
+ const handleResize = () => {
456
+ setSize({
457
+ width: window.innerWidth,
458
+ height: window.innerHeight,
459
+ });
460
+ };
461
+ // Set initial size
462
+ handleResize();
463
+ // Add event listener
464
+ window.addEventListener('resize', handleResize);
465
+ // Cleanup
466
+ return () => {
467
+ window.removeEventListener('resize', handleResize);
468
+ };
469
+ }, []);
470
+ return size;
471
+ }
472
+
473
+ /**
474
+ * Built-in confetti component
475
+ * Lightweight CSS-based confetti animation
476
+ */
477
+ const BuiltInConfetti = ({ show, duration = 5000, particleCount = 50, colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'], }) => {
478
+ const [isVisible, setIsVisible] = useState(false);
479
+ const { width, height } = useWindowSize();
480
+ useEffect(() => {
481
+ if (show) {
482
+ setIsVisible(true);
483
+ const timer = setTimeout(() => setIsVisible(false), duration);
484
+ return () => clearTimeout(timer);
485
+ }
486
+ else {
487
+ setIsVisible(false);
488
+ }
489
+ }, [show, duration]);
490
+ if (!isVisible)
491
+ return null;
492
+ const containerStyles = {
493
+ position: 'fixed',
494
+ top: 0,
495
+ left: 0,
496
+ width: '100%',
497
+ height: '100%',
498
+ pointerEvents: 'none',
499
+ zIndex: 10001,
500
+ overflow: 'hidden',
501
+ };
502
+ // Generate particles
503
+ const particles = Array.from({ length: particleCount }, (_, i) => {
504
+ const color = colors[Math.floor(Math.random() * colors.length)];
505
+ const startX = Math.random() * width;
506
+ const rotation = Math.random() * 360;
507
+ const fallDuration = 3 + Math.random() * 2; // 3-5 seconds
508
+ const delay = Math.random() * 0.5; // 0-0.5s delay
509
+ const shape = Math.random() > 0.5 ? 'circle' : 'square';
510
+ const particleStyles = {
511
+ position: 'absolute',
512
+ top: '-20px',
513
+ left: `${startX}px`,
514
+ width: '10px',
515
+ height: '10px',
516
+ backgroundColor: color,
517
+ borderRadius: shape === 'circle' ? '50%' : '0',
518
+ transform: `rotate(${rotation}deg)`,
519
+ animation: `confettiFall ${fallDuration}s linear ${delay}s forwards`,
520
+ opacity: 0.9,
521
+ };
522
+ return React.createElement("div", { key: i, style: particleStyles, "data-testid": "confetti-particle" });
523
+ });
524
+ return (React.createElement(React.Fragment, null,
525
+ React.createElement("style", null, `
526
+ @keyframes confettiFall {
527
+ 0% {
528
+ transform: translateY(0) rotate(0deg);
529
+ opacity: 1;
530
+ }
531
+ 100% {
532
+ transform: translateY(${height + 50}px) rotate(720deg);
533
+ opacity: 0;
534
+ }
535
+ }
536
+ `),
537
+ React.createElement("div", { style: containerStyles, "data-testid": "built-in-confetti" }, particles)));
538
+ };
539
+
540
+ const NOTIFICATION_DURATION_MS = 5000;
541
+ const AchievementUIContext = createContext({
542
+ icons: {},
543
+ ui: {},
544
+ });
545
+ const AchievementEffects = ({ icons, ui }) => {
546
+ const engine = useAchievementEngine();
547
+ const seenAchievementsRef = useRef(new Set(engine.getUnlocked()));
548
+ const confettiTimerRef = useRef(null);
549
+ const [showConfetti, setShowConfetti] = useState(false);
550
+ const [notifications, setNotifications] = useState([]);
551
+ useEffect(() => {
552
+ const unsubscribeUnlocked = engine.on('achievement:unlocked', (event) => {
553
+ if (seenAchievementsRef.current.has(event.achievementId)) {
554
+ return;
555
+ }
556
+ seenAchievementsRef.current.add(event.achievementId);
557
+ const unlockedAchievement = {
558
+ achievementId: event.achievementId,
559
+ achievementTitle: event.achievementTitle,
560
+ achievementDescription: event.achievementDescription,
561
+ achievementIconKey: event.achievementIconKey,
562
+ isUnlocked: true,
563
+ };
564
+ if (ui.enableNotifications !== false) {
565
+ setNotifications((currentNotifications) => {
566
+ if (currentNotifications.some((notification) => notification.achievementId === unlockedAchievement.achievementId)) {
567
+ return currentNotifications;
568
+ }
569
+ return [...currentNotifications, unlockedAchievement];
570
+ });
571
+ }
572
+ if (ui.enableConfetti !== false) {
573
+ if (confettiTimerRef.current) {
574
+ clearTimeout(confettiTimerRef.current);
575
+ }
576
+ setShowConfetti(true);
577
+ confettiTimerRef.current = setTimeout(() => {
578
+ setShowConfetti(false);
579
+ confettiTimerRef.current = null;
580
+ }, NOTIFICATION_DURATION_MS);
581
+ }
582
+ });
583
+ const unsubscribeStateChanged = engine.on('state:changed', () => {
584
+ const unlocked = new Set(engine.getUnlocked());
585
+ seenAchievementsRef.current.forEach((achievementId) => {
586
+ if (!unlocked.has(achievementId)) {
587
+ seenAchievementsRef.current.delete(achievementId);
588
+ }
589
+ });
590
+ });
591
+ return () => {
592
+ unsubscribeUnlocked();
593
+ unsubscribeStateChanged();
594
+ if (confettiTimerRef.current) {
595
+ clearTimeout(confettiTimerRef.current);
596
+ }
597
+ };
598
+ }, [engine, ui.enableConfetti, ui.enableNotifications]);
599
+ const NotificationComponent = ui.NotificationComponent || BuiltInNotification;
600
+ const ConfettiComponentResolved = ui.ConfettiComponent || BuiltInConfetti;
601
+ return (React.createElement(React.Fragment, null,
602
+ ui.enableNotifications !== false &&
603
+ notifications.map((notification, index) => (React.createElement(NotificationComponent, { key: notification.achievementId, achievement: notification, onClose: () => setNotifications((currentNotifications) => currentNotifications.filter((currentNotification) => currentNotification.achievementId !== notification.achievementId)), duration: NOTIFICATION_DURATION_MS, position: ui.notificationPosition || 'top-center', theme: ui.theme || 'modern', icons: icons, stackIndex: index }))),
604
+ ui.enableConfetti !== false && (React.createElement(ConfettiComponentResolved, { show: showConfetti, duration: NOTIFICATION_DURATION_MS }))));
605
+ };
606
+ const AchievementProvider = (_a) => {
607
+ var { children, icons = {}, ui = {}, useBuiltInUI } = _a, providerProps = __rest(_a, ["children", "icons", "ui", "useBuiltInUI"]);
608
+ if (useBuiltInUI !== undefined) {
609
+ warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 4.2.');
610
+ }
611
+ const uiContextValue = useMemo(() => ({ icons, ui }), [icons, ui]);
612
+ return (React.createElement(AchievementUIContext.Provider, { value: uiContextValue },
613
+ React.createElement(AchievementProvider$1, Object.assign({}, providerProps, { icons: icons }),
614
+ children,
615
+ React.createElement(AchievementEffects, { icons: icons, ui: ui }))));
616
+ };
617
+
618
+ const useAchievements = () => {
619
+ const context = useContext(AchievementContext);
620
+ if (!context) {
621
+ throw new Error('useAchievements must be used within an AchievementProvider');
622
+ }
623
+ return context;
624
+ };
625
+
626
+ const useAchievementState = () => {
627
+ const { achievements, getAllAchievements, getState } = useAchievements();
628
+ const allAchievements = getAllAchievements();
629
+ const unlockedIds = achievements.unlocked;
630
+ const unlockedAchievementSet = new Set(unlockedIds);
631
+ const unlockedAchievements = allAchievements.filter((achievement) => unlockedAchievementSet.has(achievement.achievementId));
632
+ return {
633
+ unlockedIds,
634
+ unlockedAchievements,
635
+ allAchievements,
636
+ unlockedCount: unlockedIds.length,
637
+ totalCount: allAchievements.length,
638
+ metrics: getState().metrics,
639
+ };
640
+ };
641
+
642
+ const defaultStyles = {
643
+ badgesButton: {
644
+ backgroundColor: '#4CAF50',
645
+ color: 'white',
646
+ padding: '10px 20px',
647
+ border: 'none',
648
+ borderRadius: '20px',
649
+ cursor: 'pointer',
650
+ display: 'flex',
651
+ alignItems: 'center',
652
+ gap: '8px',
653
+ fontSize: '16px',
654
+ boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
655
+ transition: 'transform 0.2s ease-in-out',
656
+ },
657
+ badgesModal: {
658
+ overlay: {
659
+ position: 'fixed',
660
+ top: 0,
661
+ right: 0,
662
+ bottom: 0,
663
+ left: 0,
664
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
665
+ display: 'flex',
666
+ alignItems: 'center',
667
+ justifyContent: 'center',
668
+ zIndex: 10000,
669
+ },
670
+ content: {
671
+ position: 'relative',
672
+ background: '#fff',
673
+ borderRadius: '8px',
674
+ padding: '20px',
675
+ maxWidth: '500px',
676
+ width: '90%',
677
+ maxHeight: '80vh',
678
+ overflow: 'auto',
679
+ },
680
+ header: {
681
+ display: 'flex',
682
+ justifyContent: 'space-between',
683
+ alignItems: 'center',
684
+ marginBottom: '20px',
685
+ },
686
+ closeButton: {
687
+ background: 'none',
688
+ border: 'none',
689
+ fontSize: '24px',
690
+ cursor: 'pointer',
691
+ padding: '0',
692
+ },
693
+ achievementList: {
694
+ display: 'flex',
695
+ flexDirection: 'column',
696
+ gap: '16px',
697
+ },
698
+ achievementItem: {
699
+ display: 'flex',
700
+ gap: '16px',
701
+ padding: '16px',
702
+ borderRadius: '8px',
703
+ backgroundColor: '#f5f5f5',
704
+ alignItems: 'center',
705
+ },
706
+ achievementTitle: {
707
+ margin: '0',
708
+ fontSize: '18px',
709
+ fontWeight: 'bold',
710
+ },
711
+ achievementDescription: {
712
+ margin: '4px 0 0 0',
713
+ color: '#666',
714
+ },
715
+ achievementIcon: {
716
+ fontSize: '32px',
717
+ display: 'flex',
718
+ alignItems: 'center',
719
+ justifyContent: 'center',
720
+ },
721
+ lockIcon: {
722
+ fontSize: '24px',
723
+ position: 'absolute',
724
+ top: '50%',
725
+ right: '16px',
726
+ transform: 'translateY(-50%)',
727
+ },
728
+ lockedAchievementItem: {
729
+ display: 'flex',
730
+ gap: '16px',
731
+ padding: '16px',
732
+ borderRadius: '8px',
733
+ backgroundColor: '#e0e0e0',
734
+ alignItems: 'center',
735
+ opacity: 0.5,
736
+ },
737
+ },
738
+ levelProgress: {
739
+ container: {
740
+ display: 'flex',
741
+ flexDirection: 'column',
742
+ gap: '8px',
743
+ padding: '12px 16px',
744
+ },
745
+ header: {
746
+ display: 'flex',
747
+ alignItems: 'center',
748
+ justifyContent: 'space-between',
749
+ gap: '12px',
750
+ fontWeight: 600,
751
+ },
752
+ levelLabel: {
753
+ fontSize: '14px',
754
+ },
755
+ valueText: {
756
+ fontSize: '12px',
757
+ opacity: 0.8,
758
+ },
759
+ progressTrack: {
760
+ position: 'relative',
761
+ height: '10px',
762
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
763
+ borderRadius: '999px',
764
+ overflow: 'hidden',
765
+ },
766
+ progressBar: {
767
+ height: '100%',
768
+ borderRadius: '999px',
769
+ transition: 'width 0.2s ease-in-out',
770
+ },
771
+ progressText: {
772
+ fontSize: '12px',
773
+ opacity: 0.8,
774
+ textAlign: 'right',
775
+ },
776
+ },
777
+ };
778
+
779
+ const resolveIcon = (achievement, icons) => {
780
+ return ((achievement.achievementIconKey && icons[achievement.achievementIconKey]) ||
781
+ achievement.achievementIconKey ||
782
+ icons.default ||
783
+ '⭐');
784
+ };
785
+ const AchievementsList = ({ achievements, showLocked = true, showUnlockConditions = false, icons = {}, styles = {}, emptyState, className, renderAchievement, }) => {
786
+ const context = useContext(AchievementContext);
787
+ const uiContext = useContext(AchievementUIContext);
788
+ const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
789
+ const sourceAchievements = achievements || (context === null || context === void 0 ? void 0 : context.getAllAchievements());
790
+ if (!sourceAchievements) {
791
+ throw new Error('AchievementsList requires either an achievements prop or an AchievementProvider parent.');
792
+ }
793
+ const achievementsToDisplay = showLocked
794
+ ? sourceAchievements
795
+ : sourceAchievements.filter((achievement) => achievement.isUnlocked);
796
+ if (achievementsToDisplay.length === 0) {
797
+ return (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, emptyState || 'No achievements configured.'));
798
+ }
799
+ return (React.createElement("div", { className: className, style: Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementList), styles === null || styles === void 0 ? void 0 : styles.achievementList), "data-testid": "achievements-list" }, achievementsToDisplay.map((achievement, index) => {
800
+ const isLocked = !achievement.isUnlocked;
801
+ const icon = resolveIcon(achievement, mergedIcons);
802
+ if (renderAchievement) {
803
+ return (React.createElement(React.Fragment, { key: achievement.achievementId }, renderAchievement({ achievement, isLocked, icon, index })));
804
+ }
805
+ return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign(Object.assign({}, (isLocked
806
+ ? Object.assign(Object.assign({}, defaultStyles.badgesModal.lockedAchievementItem), styles === null || styles === void 0 ? void 0 : styles.lockedAchievementItem) : defaultStyles.badgesModal.achievementItem)), styles === null || styles === void 0 ? void 0 : styles.achievementItem), { position: 'relative' }), "data-testid": "achievement-list-item", "data-unlocked": achievement.isUnlocked ? 'true' : 'false' },
807
+ React.createElement("div", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementIcon), styles === null || styles === void 0 ? void 0 : styles.achievementIcon), { opacity: isLocked ? 0.4 : 1 }) }, icon),
808
+ React.createElement("div", { style: { flex: 1 } },
809
+ React.createElement("h3", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementTitle), styles === null || styles === void 0 ? void 0 : styles.achievementTitle), { color: isLocked ? '#999' : undefined }) }, achievement.achievementTitle),
810
+ achievement.achievementDescription && (React.createElement("p", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementDescription), styles === null || styles === void 0 ? void 0 : styles.achievementDescription), { color: isLocked ? '#aaa' : '#666' }) },
811
+ achievement.achievementDescription,
812
+ showUnlockConditions && isLocked && (React.createElement("span", { style: {
813
+ display: 'block',
814
+ fontSize: '12px',
815
+ marginTop: '4px',
816
+ fontStyle: 'italic',
817
+ color: '#888',
818
+ } },
819
+ "\uD83D\uDD13 ",
820
+ achievement.achievementDescription))))),
821
+ isLocked && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.lockIcon), styles === null || styles === void 0 ? void 0 : styles.lockIcon) }, "\uD83D\uDD12"))));
822
+ })));
823
+ };
824
+
825
+ const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achievements', styles = {}, icons = {}, showLocked = true, showUnlockConditions = false, emptyState, renderAchievement, theme, }) => {
826
+ const context = useContext(AchievementContext);
827
+ const uiContext = useContext(AchievementUIContext);
828
+ useEffect(() => {
829
+ if (!isOpen || typeof document === 'undefined') {
830
+ return;
831
+ }
832
+ const previousOverflow = document.body.style.overflow;
833
+ document.body.style.overflow = 'hidden';
834
+ const handleKeyDown = (event) => {
835
+ if (event.key === 'Escape') {
836
+ onClose();
837
+ }
838
+ };
839
+ document.addEventListener('keydown', handleKeyDown);
840
+ return () => {
841
+ document.body.style.overflow = previousOverflow;
842
+ document.removeEventListener('keydown', handleKeyDown);
843
+ };
844
+ }, [isOpen, onClose]);
845
+ if (!isOpen)
846
+ return null;
847
+ const CustomModal = uiContext.ui.ModalComponent;
848
+ const sourceAchievements = achievements || (context === null || context === void 0 ? void 0 : context.getAllAchievements());
849
+ const modalAchievements = showLocked
850
+ ? sourceAchievements
851
+ : sourceAchievements === null || sourceAchievements === void 0 ? void 0 : sourceAchievements.filter((achievement) => achievement.isUnlocked);
852
+ const resolvedTheme = theme || uiContext.ui.theme || 'modern';
853
+ const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
854
+ if (CustomModal) {
855
+ if (!modalAchievements) {
856
+ throw new Error('AchievementsModal requires either an achievements prop or an AchievementProvider parent.');
857
+ }
858
+ return (React.createElement(CustomModal, { isOpen: isOpen, onClose: onClose, achievements: modalAchievements, icons: mergedIcons, theme: resolvedTheme }));
859
+ }
860
+ return (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.overlay), styles === null || styles === void 0 ? void 0 : styles.overlay), role: "presentation", onClick: onClose, "data-testid": "achievements-modal-overlay" },
861
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.content), styles === null || styles === void 0 ? void 0 : styles.content), role: "dialog", "aria-modal": "true", "aria-labelledby": "achievements-modal-title", onClick: (event) => event.stopPropagation(), "data-testid": "achievements-modal" },
862
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.header), styles === null || styles === void 0 ? void 0 : styles.header) },
863
+ React.createElement("h2", { id: "achievements-modal-title", style: { margin: 0 } }, title),
864
+ React.createElement("button", { onClick: onClose, style: Object.assign(Object.assign({}, defaultStyles.badgesModal.closeButton), styles === null || styles === void 0 ? void 0 : styles.closeButton), "aria-label": "Close" }, "\u00D7")),
865
+ React.createElement(AchievementsList, { achievements: modalAchievements, showLocked: showLocked, showUnlockConditions: showUnlockConditions, icons: icons, styles: styles, emptyState: emptyState, renderAchievement: renderAchievement }))));
866
+ };
867
+
868
+ const getPositionStyles$1 = (position) => {
869
+ const base = {
870
+ position: 'fixed',
871
+ margin: '20px',
872
+ zIndex: 1000,
873
+ };
874
+ switch (position) {
875
+ case 'top-left':
876
+ return Object.assign(Object.assign({}, base), { top: 0, left: 0 });
877
+ case 'top-right':
878
+ return Object.assign(Object.assign({}, base), { top: 0, right: 0 });
879
+ case 'bottom-left':
880
+ return Object.assign(Object.assign({}, base), { bottom: 0, left: 0 });
881
+ case 'bottom-right':
882
+ return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
883
+ }
884
+ };
885
+ const AchievementsWidget = ({ position = 'bottom-right', placement = 'fixed', showAllAchievements = true, showUnlockConditions = false, showCount = true, icons, theme, label = 'Achievements', icon = '🏆', triggerClassName, renderTrigger, buttonStyles, modalStyles, modalTitle, emptyState, renderAchievement, }) => {
886
+ const uiContext = useContext(AchievementUIContext);
887
+ const [isModalOpen, setIsModalOpen] = useState(false);
888
+ const { unlockedAchievements, allAchievements, unlockedCount, totalCount } = useAchievementState();
889
+ const resolvedTheme = theme || uiContext.ui.theme || 'modern';
890
+ const themeConfig = getTheme(resolvedTheme) || builtInThemes.modern;
891
+ const modalAchievements = showAllAchievements ? allAchievements : unlockedAchievements;
892
+ const openModal = () => setIsModalOpen(true);
893
+ const buttonBaseStyles = placement === 'inline'
894
+ ? Object.assign({ backgroundColor: 'transparent', color: 'inherit', padding: '10px 12px', border: 'none', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', font: 'inherit', width: '100%', textAlign: 'left' }, buttonStyles) : Object.assign(Object.assign({ backgroundColor: themeConfig.notification.accentColor, color: 'white', padding: '10px 20px', border: 'none', borderRadius: '20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '16px', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', transition: 'transform 0.2s ease-in-out' }, getPositionStyles$1(position)), buttonStyles);
895
+ const buttonProps = {
896
+ type: 'button',
897
+ onClick: openModal,
898
+ style: buttonBaseStyles,
899
+ className: triggerClassName,
900
+ 'data-placement': placement,
901
+ 'data-testid': 'achievements-widget-button',
902
+ 'aria-label': `${label}: ${unlockedCount} of ${totalCount} achievements unlocked`,
903
+ };
904
+ return (React.createElement(React.Fragment, null,
905
+ renderTrigger ? (renderTrigger({
906
+ open: openModal,
907
+ label,
908
+ unlockedCount,
909
+ totalCount,
910
+ unlockedAchievements,
911
+ allAchievements,
912
+ buttonProps,
913
+ })) : (React.createElement("button", Object.assign({}, buttonProps, { onMouseEnter: (event) => {
914
+ if (placement !== 'inline') {
915
+ event.currentTarget.style.transform = 'scale(1.05)';
916
+ }
917
+ }, onMouseLeave: (event) => {
918
+ if (placement !== 'inline') {
919
+ event.currentTarget.style.transform = 'scale(1)';
920
+ }
921
+ } }),
922
+ React.createElement("span", null, icon),
923
+ React.createElement("span", { style: { flex: 1 } }, label),
924
+ showCount && React.createElement("span", null, unlockedCount))),
925
+ React.createElement(AchievementsModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: modalAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles, title: modalTitle, emptyState: emptyState, renderAchievement: renderAchievement, theme: resolvedTheme })));
926
+ };
927
+
928
+ const getPositionStyles = (position) => {
929
+ const base = {
930
+ position: 'fixed',
931
+ margin: '20px',
932
+ zIndex: 1000,
933
+ };
934
+ switch (position) {
935
+ case 'top-left':
936
+ return Object.assign(Object.assign({}, base), { top: 0, left: 0 });
937
+ case 'top-right':
938
+ return Object.assign(Object.assign({}, base), { top: 0, right: 0 });
939
+ case 'bottom-left':
940
+ return Object.assign(Object.assign({}, base), { bottom: 0, left: 0 });
941
+ case 'bottom-right':
942
+ return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
943
+ }
944
+ };
945
+ /**
946
+ * @deprecated Use `AchievementsWidget` for new integrations. This v3
947
+ * compatibility wrapper will be removed in 4.2.
948
+ */
949
+ const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
950
+ React.useEffect(() => {
951
+ warnDeprecation('`BadgesButton` is deprecated. Use `AchievementsWidget` instead. `BadgesButton` will be removed in 4.2.');
952
+ }, []);
953
+ // Get theme configuration for consistent styling
954
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
955
+ const accentColor = themeConfig.notification.accentColor;
956
+ // Different styling for fixed vs inline placement
957
+ const baseStyles = placement === 'inline'
958
+ ? Object.assign({
959
+ // Inline mode: looks like a navigation item
960
+ backgroundColor: 'transparent', color: themeConfig.notification.textColor, padding: '12px 16px', border: 'none', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', fontSize: '15px', width: '100%', textAlign: 'left', transition: 'background-color 0.2s ease-in-out' }, styles) : Object.assign(Object.assign({
961
+ // Fixed mode: floating button
962
+ backgroundColor: accentColor, color: 'white', padding: '10px 20px', border: 'none', borderRadius: '20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '16px', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', transition: 'transform 0.2s ease-in-out' }, getPositionStyles(position)), styles);
963
+ return (React.createElement("button", { onClick: onClick, style: baseStyles, onMouseEnter: (e) => {
964
+ if (placement === 'inline') {
965
+ // Inline mode: subtle background color change
966
+ e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
967
+ }
968
+ else {
969
+ // Fixed mode: scale transformation
970
+ e.target.style.transform = 'scale(1.05)';
971
+ }
972
+ }, onMouseLeave: (e) => {
973
+ if (placement === 'inline') {
974
+ e.target.style.backgroundColor = 'transparent';
975
+ }
976
+ else {
977
+ e.target.style.transform = 'scale(1)';
978
+ }
979
+ }, "data-placement": placement, "data-testid": "badges-button" },
980
+ "\uD83C\uDFC6 Achievements (",
981
+ unlockedAchievements.length,
982
+ ")"));
983
+ };
984
+
985
+ /**
986
+ * @deprecated Use `AchievementsModal`, `AchievementsWidget`, or
987
+ * `AchievementsList` for new integrations. This v3 compatibility wrapper will
988
+ * be removed in 4.2.
989
+ */
990
+ const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, showAllAchievements = false, showUnlockConditions = false, allAchievements, }) => {
991
+ useEffect(() => {
992
+ warnDeprecation('`BadgesModal` is deprecated. Use `AchievementsWidget` or `AchievementsList` instead. `BadgesModal` will be removed in 4.2.');
993
+ }, []);
994
+ const achievementsToDisplay = showAllAchievements && allAchievements
995
+ ? allAchievements
996
+ : achievements.map((achievement) => (Object.assign(Object.assign({}, achievement), { isUnlocked: true })));
997
+ return (React.createElement(AchievementsModal, { isOpen: isOpen, onClose: onClose, achievements: achievementsToDisplay, styles: styles, icons: icons, showUnlockConditions: showUnlockConditions }));
998
+ };
999
+
1000
+ /**
1001
+ * @deprecated Use `AchievementsWidget` for new integrations. This v3
1002
+ * compatibility wrapper will be removed in 4.2.
1003
+ */
1004
+ const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right', placement = 'fixed', showAllAchievements = false, allAchievements, showUnlockConditions = false, icons, theme = 'modern', buttonStyles, modalStyles, }) => {
1005
+ const [isModalOpen, setIsModalOpen] = useState(false);
1006
+ return (React.createElement(React.Fragment, null,
1007
+ React.createElement(BadgesButton, { onClick: () => setIsModalOpen(true), unlockedAchievements: unlockedAchievements, position: position, placement: placement, theme: theme, styles: buttonStyles }),
1008
+ React.createElement(BadgesModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: unlockedAchievements, showAllAchievements: showAllAchievements, allAchievements: allAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles })));
1009
+ };
1010
+
1011
+ /**
1012
+ * @deprecated Use the provider `ui.ConfettiComponent` option or the built-in
1013
+ * confetti default. This v3 compatibility wrapper will be removed in 4.2.
1014
+ */
1015
+ const ConfettiWrapper = ({ show }) => {
1016
+ useEffect(() => {
1017
+ warnDeprecation('`ConfettiWrapper` is deprecated. Use the provider `ui.ConfettiComponent` option or built-in confetti defaults instead. `ConfettiWrapper` will be removed in 4.2.');
1018
+ }, []);
1019
+ return React.createElement(BuiltInConfetti, { show: show, particleCount: 200 });
1020
+ };
1021
+
1022
+ const clamp = (value, min, max) => {
1023
+ return Math.min(Math.max(value, min), max);
1024
+ };
1025
+ const LevelProgress = ({ level, currentXP, nextLevelXP, label = 'Level', valueLabel, showValues = true, showPercent = true, theme = 'modern', styles = {}, className, }) => {
1026
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
1027
+ const safeMax = nextLevelXP > 0 ? nextLevelXP : 1;
1028
+ const clampedCurrent = clamp(currentXP, 0, safeMax);
1029
+ const progress = Math.round((clampedCurrent / safeMax) * 100);
1030
+ const computedValueLabel = valueLabel !== null && valueLabel !== void 0 ? valueLabel : `${clampedCurrent} / ${safeMax} XP`;
1031
+ const containerStyles = Object.assign(Object.assign(Object.assign({}, defaultStyles.levelProgress.container), { background: themeConfig.notification.background, color: themeConfig.notification.textColor, borderRadius: themeConfig.notification.borderRadius, boxShadow: themeConfig.notification.boxShadow }), styles === null || styles === void 0 ? void 0 : styles.container);
1032
+ const progressBarStyles = Object.assign(Object.assign(Object.assign({}, defaultStyles.levelProgress.progressBar), { backgroundColor: themeConfig.notification.accentColor, width: `${progress}%` }), styles === null || styles === void 0 ? void 0 : styles.progressBar);
1033
+ return (React.createElement("div", { className: className, style: containerStyles, "data-testid": "level-progress" },
1034
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.header), styles === null || styles === void 0 ? void 0 : styles.header) },
1035
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.levelLabel), styles === null || styles === void 0 ? void 0 : styles.levelLabel) },
1036
+ label,
1037
+ " ",
1038
+ level),
1039
+ showValues && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.valueText), styles === null || styles === void 0 ? void 0 : styles.valueText) }, computedValueLabel))),
1040
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.progressTrack), styles === null || styles === void 0 ? void 0 : styles.progressTrack), role: "progressbar", "aria-valuemin": 0, "aria-valuemax": safeMax, "aria-valuenow": clampedCurrent, "aria-label": "Level progress" },
1041
+ React.createElement("div", { style: progressBarStyles })),
1042
+ showPercent && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.progressText), styles === null || styles === void 0 ? void 0 : styles.progressText) },
1043
+ progress,
1044
+ "%"))));
1045
+ };
1046
+
1047
+ /**
1048
+ * A simplified hook for achievement tracking.
1049
+ * Provides the v4 happy path for direct metric updates plus explicit state names.
1050
+ */
1051
+ const useSimpleAchievements = () => {
1052
+ const { update, reset, getState, exportData, importData } = useAchievements();
1053
+ const achievementState = useAchievementState();
1054
+ const track = (metric, value) => update({ [metric]: value });
1055
+ const increment = (metric, amount = 1) => {
1056
+ const currentState = getState();
1057
+ const currentMetricArray = currentState.metrics[metric] || [0];
1058
+ const currentValue = Array.isArray(currentMetricArray)
1059
+ ? currentMetricArray[0]
1060
+ : currentMetricArray;
1061
+ const newValue = (typeof currentValue === 'number' ? currentValue : 0) + amount;
1062
+ update({ [metric]: newValue });
1063
+ };
1064
+ const trackMultiple = (metrics) => update(metrics);
1065
+ return {
1066
+ track,
1067
+ increment,
1068
+ trackMultiple,
1069
+ unlockedIds: achievementState.unlockedIds,
1070
+ unlockedAchievements: achievementState.unlockedAchievements,
1071
+ allAchievements: achievementState.allAchievements,
1072
+ unlockedCount: achievementState.unlockedCount,
1073
+ totalCount: achievementState.totalCount,
1074
+ metrics: achievementState.metrics,
1075
+ reset,
1076
+ getState,
1077
+ exportData,
1078
+ importData,
1079
+ getAllAchievements: () => achievementState.allAchievements,
1080
+ /**
1081
+ * @deprecated Use `unlockedIds` instead. This alias will be removed in 4.2.
1082
+ */
1083
+ unlocked: achievementState.unlockedIds,
1084
+ /**
1085
+ * @deprecated Use `allAchievements` instead. This alias will be removed in 4.2.
1086
+ */
1087
+ all: achievementState.allAchievements,
1088
+ };
1089
+ };
1090
+
1091
+ /**
1092
+ * Built-in modal component
1093
+ * Modern, theme-aware achievement modal with smooth animations
1094
+ */
1095
+ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', }) => {
1096
+ // Merge custom icons with defaults
1097
+ const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
1098
+ // Get theme configuration
1099
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
1100
+ const { modal: themeStyles } = themeConfig;
1101
+ useEffect(() => {
1102
+ if (isOpen) {
1103
+ // Lock body scroll when modal is open
1104
+ document.body.style.overflow = 'hidden';
1105
+ }
1106
+ else {
1107
+ // Restore body scroll
1108
+ document.body.style.overflow = '';
1109
+ }
1110
+ return () => {
1111
+ document.body.style.overflow = '';
1112
+ };
1113
+ }, [isOpen]);
1114
+ if (!isOpen)
1115
+ return null;
1116
+ const overlayStyles = {
1117
+ position: 'fixed',
1118
+ top: 0,
1119
+ left: 0,
1120
+ right: 0,
1121
+ bottom: 0,
1122
+ backgroundColor: themeStyles.overlayColor,
1123
+ display: 'flex',
1124
+ alignItems: 'center',
1125
+ justifyContent: 'center',
1126
+ zIndex: 10000,
1127
+ animation: 'fadeIn 0.3s ease-in-out',
1128
+ };
1129
+ const modalStyles = {
1130
+ background: themeStyles.background,
1131
+ borderRadius: themeStyles.borderRadius,
1132
+ padding: '32px',
1133
+ maxWidth: '600px',
1134
+ width: '90%',
1135
+ maxHeight: '80vh',
1136
+ overflow: 'auto',
1137
+ boxShadow: '0 20px 60px rgba(0, 0, 0, 0.5)',
1138
+ animation: 'scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
1139
+ position: 'relative',
1140
+ };
1141
+ const headerStyles = {
1142
+ display: 'flex',
1143
+ justifyContent: 'space-between',
1144
+ alignItems: 'center',
1145
+ marginBottom: '24px',
1146
+ };
1147
+ const titleStyles = {
1148
+ margin: 0,
1149
+ color: themeStyles.textColor,
1150
+ fontSize: themeStyles.headerFontSize || '28px',
1151
+ fontWeight: 'bold',
1152
+ };
1153
+ const closeButtonStyles = {
1154
+ background: 'none',
1155
+ border: 'none',
1156
+ fontSize: '32px',
1157
+ cursor: 'pointer',
1158
+ color: themeStyles.textColor,
1159
+ opacity: 0.6,
1160
+ transition: 'opacity 0.2s',
1161
+ padding: 0,
1162
+ lineHeight: 1,
1163
+ };
1164
+ const isBadgeLayout = themeStyles.achievementLayout === 'badge';
1165
+ const listStyles = isBadgeLayout
1166
+ ? {
1167
+ display: 'grid',
1168
+ gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
1169
+ gap: '16px',
1170
+ }
1171
+ : {
1172
+ display: 'flex',
1173
+ flexDirection: 'column',
1174
+ gap: '12px',
1175
+ };
1176
+ const getAchievementItemStyles = (isUnlocked) => {
1177
+ const baseStyles = {
1178
+ borderRadius: themeStyles.achievementCardBorderRadius || '12px',
1179
+ backgroundColor: isUnlocked
1180
+ ? `${themeStyles.accentColor}1A` // 10% opacity
1181
+ : 'rgba(255, 255, 255, 0.05)',
1182
+ border: `2px solid ${isUnlocked ? themeStyles.accentColor : 'rgba(255, 255, 255, 0.1)'}`,
1183
+ opacity: isUnlocked ? 1 : 0.5,
1184
+ transition: 'all 0.2s',
1185
+ };
1186
+ if (isBadgeLayout) {
1187
+ // Badge layout: vertical, centered, square-ish
1188
+ return Object.assign(Object.assign({}, baseStyles), { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: '20px 12px', aspectRatio: '1 / 1.1', minHeight: '160px' });
1189
+ }
1190
+ else {
1191
+ // Horizontal layout (default)
1192
+ return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: '16px', padding: '16px' });
1193
+ }
1194
+ };
1195
+ const getIconContainerStyles = (isUnlocked) => {
1196
+ if (isBadgeLayout) {
1197
+ return {
1198
+ fontSize: '48px',
1199
+ lineHeight: 1,
1200
+ marginBottom: '8px',
1201
+ opacity: isUnlocked ? 1 : 0.3,
1202
+ };
1203
+ }
1204
+ return {
1205
+ fontSize: '40px',
1206
+ flexShrink: 0,
1207
+ lineHeight: 1,
1208
+ opacity: isUnlocked ? 1 : 0.3,
1209
+ };
1210
+ };
1211
+ const contentStyles = isBadgeLayout
1212
+ ? {
1213
+ width: '100%',
1214
+ }
1215
+ : {
1216
+ flex: 1,
1217
+ minWidth: 0,
1218
+ };
1219
+ const achievementTitleStyles = isBadgeLayout
1220
+ ? {
1221
+ margin: '0 0 4px 0',
1222
+ color: themeStyles.textColor,
1223
+ fontSize: '14px',
1224
+ fontWeight: 'bold',
1225
+ lineHeight: '1.3',
1226
+ }
1227
+ : {
1228
+ margin: '0 0 8px 0',
1229
+ color: themeStyles.textColor,
1230
+ fontSize: '18px',
1231
+ fontWeight: 'bold',
1232
+ overflow: 'hidden',
1233
+ textOverflow: 'ellipsis',
1234
+ whiteSpace: 'nowrap',
1235
+ };
1236
+ const achievementDescriptionStyles = isBadgeLayout
1237
+ ? {
1238
+ margin: 0,
1239
+ color: themeStyles.textColor,
1240
+ opacity: 0.7,
1241
+ fontSize: '11px',
1242
+ lineHeight: '1.3',
1243
+ }
1244
+ : {
1245
+ margin: 0,
1246
+ color: themeStyles.textColor,
1247
+ opacity: 0.8,
1248
+ fontSize: '14px',
1249
+ };
1250
+ const getLockIconStyles = () => {
1251
+ if (isBadgeLayout) {
1252
+ return {
1253
+ position: 'absolute',
1254
+ top: '8px',
1255
+ right: '8px',
1256
+ fontSize: '18px',
1257
+ opacity: 0.6,
1258
+ };
1259
+ }
1260
+ return {
1261
+ fontSize: '24px',
1262
+ flexShrink: 0,
1263
+ opacity: 0.5,
1264
+ };
1265
+ };
1266
+ return (React.createElement(React.Fragment, null,
1267
+ React.createElement("style", null, `
1268
+ @keyframes fadeIn {
1269
+ from { opacity: 0; }
1270
+ to { opacity: 1; }
1271
+ }
1272
+ @keyframes scaleIn {
1273
+ from {
1274
+ transform: scale(0.9);
1275
+ opacity: 0;
1276
+ }
1277
+ to {
1278
+ transform: scale(1);
1279
+ opacity: 1;
1280
+ }
1281
+ }
1282
+ `),
1283
+ React.createElement("div", { style: overlayStyles, onClick: onClose, "data-testid": "built-in-modal-overlay" },
1284
+ React.createElement("div", { style: modalStyles, onClick: (e) => e.stopPropagation(), "data-testid": "built-in-modal" },
1285
+ React.createElement("div", { style: headerStyles },
1286
+ React.createElement("h2", { style: titleStyles }, "\uD83C\uDFC6 Achievements"),
1287
+ React.createElement("button", { onClick: onClose, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close modal" }, "\u00D7")),
1288
+ React.createElement("div", { style: listStyles }, achievements.length === 0 ? (React.createElement("div", { style: { textAlign: 'center', padding: '40px 20px', color: themeStyles.textColor, opacity: 0.6 } }, "No achievements yet. Start exploring to unlock them!")) : (achievements.map((achievement) => {
1289
+ // If achievementIconKey exists but not in mergedIcons, use it directly (might be an emoji)
1290
+ // Otherwise, look up in mergedIcons or fall back to default
1291
+ const icon = (achievement.achievementIconKey &&
1292
+ mergedIcons[achievement.achievementIconKey]) ||
1293
+ achievement.achievementIconKey ||
1294
+ mergedIcons.default ||
1295
+ '⭐';
1296
+ return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign({}, getAchievementItemStyles(achievement.isUnlocked)), { position: isBadgeLayout ? 'relative' : 'static' }) },
1297
+ React.createElement("div", { style: getIconContainerStyles(achievement.isUnlocked) }, icon),
1298
+ React.createElement("div", { style: contentStyles },
1299
+ React.createElement("h3", { style: achievementTitleStyles }, achievement.achievementTitle),
1300
+ React.createElement("p", { style: achievementDescriptionStyles }, achievement.achievementDescription)),
1301
+ !achievement.isUnlocked && (React.createElement("div", { style: getLockIconStyles() }, "\uD83D\uDD12"))));
1302
+ })))))));
1303
+ };
1304
+
1305
+ export { AchievementContext, AchievementProvider, AchievementsList, AchievementsModal, AchievementsWidget, BadgesButton, BadgesButtonWithModal, BadgesModal, BuiltInConfetti, BuiltInModal, BuiltInNotification, ConfettiWrapper, AchievementProvider$1 as HeadlessAchievementProvider, LevelProgress, defaultAchievementIcons, defaultStyles, isAsyncStorage, useAchievementEngine, useAchievementState, useAchievements, useSimpleAchievements, useWindowSize };
1306
+ //# sourceMappingURL=web.esm.js.map