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
package/dist/index.esm.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import { AchievementEngine } from 'achievements-engine';
2
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, { useState, useEffect, createContext, useRef, useContext } from 'react';
4
- import Modal from 'react-modal';
5
- import Confetti from 'react-confetti';
3
+ import React, { createContext, useState, useCallback, useEffect, useContext, useMemo, useRef } from 'react';
6
4
 
7
5
  // Type guard to detect async storage
8
6
  function isAsyncStorage(storage) {
@@ -18,6 +16,174 @@ var StorageType;
18
16
  StorageType["RestAPI"] = "restapi"; // Asynchronous REST API storage
19
17
  })(StorageType || (StorageType = {}));
20
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
+
21
187
  /**
22
188
  * Built-in theme presets
23
189
  */
@@ -134,56 +300,6 @@ function getTheme(name) {
134
300
  return builtInThemes[name];
135
301
  }
136
302
 
137
- const getPositionStyles = (position) => {
138
- const base = {
139
- position: 'fixed',
140
- margin: '20px',
141
- zIndex: 1000,
142
- };
143
- switch (position) {
144
- case 'top-left':
145
- return Object.assign(Object.assign({}, base), { top: 0, left: 0 });
146
- case 'top-right':
147
- return Object.assign(Object.assign({}, base), { top: 0, right: 0 });
148
- case 'bottom-left':
149
- return Object.assign(Object.assign({}, base), { bottom: 0, left: 0 });
150
- case 'bottom-right':
151
- return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
152
- }
153
- };
154
- const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
155
- // Get theme configuration for consistent styling
156
- const themeConfig = getTheme(theme) || builtInThemes.modern;
157
- const accentColor = themeConfig.notification.accentColor;
158
- // Different styling for fixed vs inline placement
159
- const baseStyles = placement === 'inline'
160
- ? Object.assign({
161
- // Inline mode: looks like a navigation item
162
- 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({
163
- // Fixed mode: floating button
164
- 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);
165
- return (React.createElement("button", { onClick: onClick, style: baseStyles, onMouseEnter: (e) => {
166
- if (placement === 'inline') {
167
- // Inline mode: subtle background color change
168
- e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
169
- }
170
- else {
171
- // Fixed mode: scale transformation
172
- e.target.style.transform = 'scale(1.05)';
173
- }
174
- }, onMouseLeave: (e) => {
175
- if (placement === 'inline') {
176
- e.target.style.backgroundColor = 'transparent';
177
- }
178
- else {
179
- e.target.style.transform = 'scale(1)';
180
- }
181
- }, "data-placement": placement, "data-testid": "badges-button" },
182
- "\uD83C\uDFC6 Achievements (",
183
- unlockedAchievements.length,
184
- ")"));
185
- };
186
-
187
303
  const defaultAchievementIcons = {
188
304
  // Essential fallback icons for system use
189
305
  default: '⭐', // Fallback when no icon is provided
@@ -195,184 +311,123 @@ const defaultAchievementIcons = {
195
311
  star: '⭐',
196
312
  };
197
313
 
198
- const defaultStyles = {
199
- badgesButton: {
200
- backgroundColor: '#4CAF50',
201
- color: 'white',
202
- padding: '10px 20px',
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',
203
404
  border: 'none',
204
- borderRadius: '20px',
405
+ color: themeStyles.textColor,
406
+ fontSize: '24px',
205
407
  cursor: 'pointer',
206
- display: 'flex',
207
- alignItems: 'center',
208
- gap: '8px',
209
- fontSize: '16px',
210
- boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
211
- transition: 'transform 0.2s ease-in-out',
212
- },
213
- badgesModal: {
214
- overlay: {
215
- backgroundColor: 'rgba(0, 0, 0, 0.75)',
216
- display: 'flex',
217
- alignItems: 'center',
218
- justifyContent: 'center',
219
- zIndex: 1000,
220
- },
221
- content: {
222
- position: 'relative',
223
- background: '#fff',
224
- borderRadius: '8px',
225
- padding: '20px',
226
- maxWidth: '500px',
227
- width: '90%',
228
- maxHeight: '80vh',
229
- overflow: 'auto',
230
- },
231
- header: {
232
- display: 'flex',
233
- justifyContent: 'space-between',
234
- alignItems: 'center',
235
- marginBottom: '20px',
236
- },
237
- closeButton: {
238
- background: 'none',
239
- border: 'none',
240
- fontSize: '24px',
241
- cursor: 'pointer',
242
- padding: '0',
243
- },
244
- achievementList: {
245
- display: 'flex',
246
- flexDirection: 'column',
247
- gap: '16px',
248
- },
249
- achievementItem: {
250
- display: 'flex',
251
- gap: '16px',
252
- padding: '16px',
253
- borderRadius: '8px',
254
- backgroundColor: '#f5f5f5',
255
- alignItems: 'center',
256
- },
257
- achievementTitle: {
258
- margin: '0',
259
- fontSize: '18px',
260
- fontWeight: 'bold',
261
- },
262
- achievementDescription: {
263
- margin: '4px 0 0 0',
264
- color: '#666',
265
- },
266
- achievementIcon: {
267
- fontSize: '32px',
268
- display: 'flex',
269
- alignItems: 'center',
270
- justifyContent: 'center',
271
- },
272
- lockIcon: {
273
- fontSize: '24px',
274
- position: 'absolute',
275
- top: '50%',
276
- right: '16px',
277
- transform: 'translateY(-50%)',
278
- },
279
- lockedAchievementItem: {
280
- display: 'flex',
281
- gap: '16px',
282
- padding: '16px',
283
- borderRadius: '8px',
284
- backgroundColor: '#e0e0e0',
285
- alignItems: 'center',
286
- opacity: 0.5,
287
- },
288
- },
289
- };
290
-
291
- const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, showAllAchievements = false, showUnlockConditions = false, allAchievements, }) => {
292
- // Merge custom icons with default icons, with custom icons taking precedence
293
- const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
294
- return (React.createElement(Modal, { isOpen: isOpen, onRequestClose: onClose, style: {
295
- overlay: Object.assign(Object.assign({}, defaultStyles.badgesModal.overlay), styles === null || styles === void 0 ? void 0 : styles.overlay),
296
- content: Object.assign(Object.assign({}, defaultStyles.badgesModal.content), styles === null || styles === void 0 ? void 0 : styles.content)
297
- }, contentLabel: "Achievements" },
298
- React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.header), styles === null || styles === void 0 ? void 0 : styles.header) },
299
- React.createElement("h2", { style: { margin: 0 } }, "\uD83C\uDFC6 Achievements"),
300
- 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")),
301
- React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementList), styles === null || styles === void 0 ? void 0 : styles.achievementList) }, (() => {
302
- // Determine which achievements to display
303
- const achievementsToDisplay = showAllAchievements && allAchievements
304
- ? allAchievements
305
- : achievements.map(a => (Object.assign(Object.assign({}, a), { isUnlocked: true })));
306
- return (React.createElement(React.Fragment, null,
307
- achievementsToDisplay.map((achievement) => {
308
- const isLocked = !achievement.isUnlocked;
309
- return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign(Object.assign({}, (isLocked
310
- ? 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' }) },
311
- achievement.achievementIconKey && (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 }) }, achievement.achievementIconKey in mergedIcons
312
- ? mergedIcons[achievement.achievementIconKey]
313
- : mergedIcons.default || '⭐')),
314
- React.createElement("div", { style: { flex: 1 } },
315
- 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),
316
- 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' }) },
317
- achievement.achievementDescription,
318
- showUnlockConditions && isLocked && achievement.achievementDescription && (React.createElement("span", { style: {
319
- display: 'block',
320
- fontSize: '12px',
321
- marginTop: '4px',
322
- fontStyle: 'italic',
323
- color: '#888'
324
- } },
325
- "\uD83D\uDD13 ",
326
- achievement.achievementDescription)))),
327
- isLocked && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.lockIcon), styles === null || styles === void 0 ? void 0 : styles.lockIcon) }, "\uD83D\uDD12"))));
328
- }),
329
- achievementsToDisplay.length === 0 && (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, "No achievements configured."))));
330
- })())));
331
- };
332
-
333
- /**
334
- * BadgesButtonWithModal - A convenience component that combines BadgesButton and BadgesModal
335
- *
336
- * This component manages the modal state internally, providing a simplified API
337
- * for the common use case of displaying achievements.
338
- *
339
- * For advanced use cases requiring custom state management or multiple triggers,
340
- * use BadgesButton and BadgesModal separately.
341
- *
342
- * @example
343
- * ```tsx
344
- * // Simple usage
345
- * <BadgesButtonWithModal
346
- * unlockedAchievements={achievements.unlocked}
347
- * />
348
- *
349
- * // Show all achievements (locked + unlocked)
350
- * <BadgesButtonWithModal
351
- * unlockedAchievements={achievements.unlocked}
352
- * showAllAchievements={true}
353
- * allAchievements={getAllAchievements()}
354
- * showUnlockConditions={true}
355
- * />
356
- *
357
- * // Customize position and theme
358
- * <BadgesButtonWithModal
359
- * unlockedAchievements={achievements.unlocked}
360
- * position="top-right"
361
- * theme="gamified"
362
- * />
363
- *
364
- * // Inline mode for navigation
365
- * <BadgesButtonWithModal
366
- * unlockedAchievements={achievements.unlocked}
367
- * placement="inline"
368
- * />
369
- * ```
370
- */
371
- const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right', placement = 'fixed', showAllAchievements = false, allAchievements, showUnlockConditions = false, icons, theme = 'modern', buttonStyles, modalStyles, }) => {
372
- const [isModalOpen, setIsModalOpen] = useState(false);
373
- return (React.createElement(React.Fragment, null,
374
- React.createElement(BadgesButton, { onClick: () => setIsModalOpen(true), unlockedAchievements: unlockedAchievements, position: position, placement: placement, theme: theme, styles: buttonStyles }),
375
- React.createElement(BadgesModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: unlockedAchievements, showAllAchievements: showAllAchievements, allAchievements: allAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles })));
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")));
376
431
  };
377
432
 
378
433
  /**
@@ -415,137 +470,6 @@ function useWindowSize() {
415
470
  return size;
416
471
  }
417
472
 
418
- const ConfettiWrapper = ({ show }) => {
419
- const { width, height } = useWindowSize();
420
- if (!show)
421
- return null;
422
- return (React.createElement(Confetti, { width: width, height: height, numberOfPieces: 200, recycle: false, style: {
423
- position: 'fixed',
424
- top: 0,
425
- left: 0,
426
- zIndex: 1001,
427
- pointerEvents: 'none',
428
- } }));
429
- };
430
-
431
- /**
432
- * Built-in notification component
433
- * Modern, theme-aware achievement notification with smooth animations
434
- */
435
- const BuiltInNotification = ({ achievement, onClose, duration = 5000, position = 'top-center', theme = 'modern', icons = {}, }) => {
436
- var _a, _b, _c;
437
- const [isVisible, setIsVisible] = useState(false);
438
- const [isExiting, setIsExiting] = useState(false);
439
- // Merge custom icons with defaults
440
- const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
441
- // Get theme configuration
442
- const themeConfig = getTheme(theme) || builtInThemes.modern;
443
- const { notification: themeStyles } = themeConfig;
444
- useEffect(() => {
445
- // Slide in animation
446
- const showTimer = setTimeout(() => setIsVisible(true), 10);
447
- // Auto-dismiss
448
- const dismissTimer = setTimeout(() => {
449
- setIsExiting(true);
450
- setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
451
- }, duration);
452
- return () => {
453
- clearTimeout(showTimer);
454
- clearTimeout(dismissTimer);
455
- };
456
- }, [duration, onClose]);
457
- const getPositionStyles = () => {
458
- const base = {
459
- position: 'fixed',
460
- zIndex: 9999,
461
- };
462
- switch (position) {
463
- case 'top-center':
464
- return Object.assign(Object.assign({}, base), { top: 20, left: '50%', transform: 'translateX(-50%)' });
465
- case 'top-left':
466
- return Object.assign(Object.assign({}, base), { top: 20, left: 20 });
467
- case 'top-right':
468
- return Object.assign(Object.assign({}, base), { top: 20, right: 20 });
469
- case 'bottom-center':
470
- return Object.assign(Object.assign({}, base), { bottom: 20, left: '50%', transform: 'translateX(-50%)' });
471
- case 'bottom-left':
472
- return Object.assign(Object.assign({}, base), { bottom: 20, left: 20 });
473
- case 'bottom-right':
474
- return Object.assign(Object.assign({}, base), { bottom: 20, right: 20 });
475
- default:
476
- return Object.assign(Object.assign({}, base), { top: 20, left: '50%', transform: 'translateX(-50%)' });
477
- }
478
- };
479
- 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')
480
- ? `translateY(${isVisible && !isExiting ? '0' : '-20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`
481
- : `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' });
482
- const iconStyles = {
483
- fontSize: '48px',
484
- lineHeight: 1,
485
- flexShrink: 0,
486
- };
487
- const contentStyles = {
488
- flex: 1,
489
- color: themeStyles.textColor,
490
- minWidth: 0,
491
- };
492
- const headerStyles = {
493
- fontSize: ((_a = themeStyles.fontSize) === null || _a === void 0 ? void 0 : _a.header) || '12px',
494
- textTransform: 'uppercase',
495
- letterSpacing: '1px',
496
- opacity: 0.8,
497
- marginBottom: '4px',
498
- color: themeStyles.accentColor,
499
- fontWeight: 600,
500
- };
501
- const titleStyles = {
502
- fontSize: ((_b = themeStyles.fontSize) === null || _b === void 0 ? void 0 : _b.title) || '18px',
503
- fontWeight: 'bold',
504
- marginBottom: '4px',
505
- overflow: 'hidden',
506
- textOverflow: 'ellipsis',
507
- whiteSpace: 'nowrap',
508
- };
509
- const descriptionStyles = {
510
- fontSize: ((_c = themeStyles.fontSize) === null || _c === void 0 ? void 0 : _c.description) || '14px',
511
- opacity: 0.9,
512
- overflow: 'hidden',
513
- textOverflow: 'ellipsis',
514
- display: '-webkit-box',
515
- WebkitLineClamp: 2,
516
- WebkitBoxOrient: 'vertical',
517
- };
518
- const closeButtonStyles = {
519
- background: 'none',
520
- border: 'none',
521
- color: themeStyles.textColor,
522
- fontSize: '24px',
523
- cursor: 'pointer',
524
- opacity: 0.6,
525
- transition: 'opacity 0.2s',
526
- padding: '4px',
527
- flexShrink: 0,
528
- lineHeight: 1,
529
- };
530
- // If achievementIconKey exists but not in mergedIcons, use it directly (might be an emoji)
531
- // Otherwise, look up in mergedIcons or fall back to default
532
- const icon = (achievement.achievementIconKey &&
533
- mergedIcons[achievement.achievementIconKey]) ||
534
- achievement.achievementIconKey ||
535
- mergedIcons.default ||
536
- '⭐';
537
- return (React.createElement("div", { style: containerStyles, "data-testid": "built-in-notification" },
538
- React.createElement("div", { style: iconStyles }, icon),
539
- React.createElement("div", { style: contentStyles },
540
- React.createElement("div", { style: headerStyles }, "Achievement Unlocked!"),
541
- React.createElement("div", { style: titleStyles }, achievement.achievementTitle),
542
- achievement.achievementDescription && (React.createElement("div", { style: descriptionStyles }, achievement.achievementDescription))),
543
- React.createElement("button", { onClick: () => {
544
- setIsExiting(true);
545
- setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
546
- }, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close notification" }, "\u00D7")));
547
- };
548
-
549
473
  /**
550
474
  * Built-in confetti component
551
475
  * Lightweight CSS-based confetti animation
@@ -604,128 +528,565 @@ const BuiltInConfetti = ({ show, duration = 5000, particleCount = 50, colors = [
604
528
  transform: translateY(0) rotate(0deg);
605
529
  opacity: 1;
606
530
  }
607
- 100% {
608
- transform: translateY(${height + 50}px) rotate(720deg);
609
- opacity: 0;
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)';
610
978
  }
611
- }
612
- `),
613
- React.createElement("div", { style: containerStyles, "data-testid": "built-in-confetti" }, particles)));
979
+ }, "data-placement": placement, "data-testid": "badges-button" },
980
+ "\uD83C\uDFC6 Achievements (",
981
+ unlockedAchievements.length,
982
+ ")"));
614
983
  };
615
984
 
616
- /******************************************************************************
617
- Copyright (c) Microsoft Corporation.
618
-
619
- Permission to use, copy, modify, and/or distribute this software for any
620
- purpose with or without fee is hereby granted.
621
-
622
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
623
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
624
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
625
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
626
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
627
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
628
- PERFORMANCE OF THIS SOFTWARE.
629
- ***************************************************************************** */
630
- /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
631
-
632
-
633
- function __awaiter(thisArg, _arguments, P, generator) {
634
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
635
- return new (P || (P = Promise))(function (resolve, reject) {
636
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
637
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
638
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
639
- step((generator = generator.apply(thisArg, _arguments || [])).next());
640
- });
641
- }
642
-
643
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
644
- var e = new Error(message);
645
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
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 }));
646
998
  };
647
999
 
648
1000
  /**
649
- * Legacy UI library detection system
650
- * Attempts to dynamically import external UI libraries
651
- * Shows deprecation warnings when detected
1001
+ * @deprecated Use `AchievementsWidget` for new integrations. This v3
1002
+ * compatibility wrapper will be removed in 4.2.
652
1003
  */
653
- let cachedLibraries = null;
654
- let detectionAttempted = false;
655
- let deprecationWarningShown = false;
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
+
656
1011
  /**
657
- * Attempts to dynamically import legacy UI libraries
658
- * Uses try/catch to gracefully handle missing dependencies
659
- * Caches result to avoid multiple import attempts
660
- *
661
- * @returns Promise resolving to LegacyLibraries object
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.
662
1014
  */
663
- function detectLegacyLibraries() {
664
- return __awaiter(this, void 0, void 0, function* () {
665
- if (detectionAttempted && cachedLibraries !== null) {
666
- return cachedLibraries;
667
- }
668
- detectionAttempted = true;
669
- const libraries = {};
670
- // Try to import react-toastify
671
- try {
672
- const toastifyModule = yield import('react-toastify');
673
- libraries.toast = toastifyModule.toast;
674
- libraries.ToastContainer = toastifyModule.ToastContainer;
675
- }
676
- catch (_a) {
677
- // Not installed, will use built-in notification
678
- }
679
- // Try to import react-modal
680
- try {
681
- const modalModule = yield import('react-modal');
682
- libraries.Modal = modalModule.default;
683
- }
684
- catch (_b) {
685
- // Not installed, will use built-in modal
686
- }
687
- // Try to import react-confetti
688
- try {
689
- const confettiModule = yield import('react-confetti');
690
- libraries.Confetti = confettiModule.default;
691
- }
692
- catch (_c) {
693
- // Not installed, will use built-in confetti
694
- }
695
- // Try to import react-use (only for useWindowSize)
696
- try {
697
- const reactUseModule = yield import('react-use');
698
- libraries.useWindowSize = reactUseModule.useWindowSize;
699
- }
700
- catch (_d) {
701
- // Not installed, will use built-in useWindowSize
702
- }
703
- cachedLibraries = libraries;
704
- // Show deprecation warning if ANY legacy library is found
705
- if (!deprecationWarningShown && Object.keys(libraries).length > 0) {
706
- const foundLibraries = [];
707
- if (libraries.toast)
708
- foundLibraries.push('react-toastify');
709
- if (libraries.Modal)
710
- foundLibraries.push('react-modal');
711
- if (libraries.Confetti)
712
- foundLibraries.push('react-confetti');
713
- if (libraries.useWindowSize)
714
- foundLibraries.push('react-use');
715
- console.warn(`[react-achievements] DEPRECATION WARNING: External UI dependencies (${foundLibraries.join(', ')}) are deprecated and will become fully optional in v4.0.0.\n\n` +
716
- `The library now includes built-in UI components with modern design and theme support.\n\n` +
717
- `To migrate:\n` +
718
- `1. Add "useBuiltInUI={true}" to your AchievementProvider\n` +
719
- `2. Test your application (UI will change to modern theme)\n` +
720
- `3. Optionally customize with theme="minimal" or theme="gamified"\n` +
721
- `4. Remove external dependencies from package.json\n\n` +
722
- `To silence this warning, set useBuiltInUI={true} in AchievementProvider.\n\n` +
723
- `Learn more: https://github.com/dave-b-b/react-achievements#migration-guide`);
724
- deprecationWarningShown = true;
725
- }
726
- return libraries;
727
- });
728
- }
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
+ };
729
1090
 
730
1091
  /**
731
1092
  * Built-in modal component
@@ -941,414 +1302,5 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
941
1302
  })))))));
942
1303
  };
943
1304
 
944
- /**
945
- * Legacy library wrappers for backwards compatibility
946
- * Wraps external UI libraries to match our component interfaces
947
- */
948
- /**
949
- * Wrapper for react-toastify toast notifications
950
- * Falls back to built-in notification if not available
951
- */
952
- const createLegacyToastNotification = (libraries) => {
953
- return ({ achievement, onClose, icons = {} }) => {
954
- const { toast } = libraries;
955
- // Merge custom icons with defaults from react-achievements
956
- const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
957
- const icon = (achievement.achievementIconKey &&
958
- mergedIcons[achievement.achievementIconKey]) ||
959
- achievement.achievementIconKey ||
960
- mergedIcons.default ||
961
- '⭐';
962
- useEffect(() => {
963
- if (!toast)
964
- return;
965
- // Call toast.success with achievement content
966
- toast.success(React.createElement("div", { style: { display: 'flex', alignItems: 'center' } },
967
- React.createElement("span", { style: { fontSize: '2em', marginRight: '10px' } }, icon),
968
- React.createElement("div", null,
969
- React.createElement("div", { style: { fontSize: '12px', opacity: 0.8, marginBottom: '4px' } }, "Achievement Unlocked!"),
970
- React.createElement("div", { style: { fontWeight: 'bold', marginBottom: '4px' } }, achievement.achievementTitle),
971
- achievement.achievementDescription && (React.createElement("div", { style: { fontSize: '13px', opacity: 0.9 } }, achievement.achievementDescription)))), {
972
- position: 'top-right',
973
- autoClose: 5000,
974
- hideProgressBar: false,
975
- closeOnClick: true,
976
- pauseOnHover: true,
977
- draggable: true,
978
- toastId: achievement.achievementId,
979
- onClose,
980
- });
981
- }, [achievement, toast, onClose, icon]);
982
- return null; // Toast handles its own rendering
983
- };
984
- };
985
- /**
986
- * Wrapper for react-confetti Confetti component
987
- * Falls back to built-in confetti if not available
988
- */
989
- const createLegacyConfettiWrapper = (libraries) => {
990
- return ({ show, duration = 5000, particleCount = 200, colors }) => {
991
- const { Confetti, useWindowSize: legacyUseWindowSize } = libraries;
992
- // If Confetti not available, use built-in
993
- if (!Confetti) {
994
- return (React.createElement(BuiltInConfetti, { show: show, duration: duration, particleCount: particleCount, colors: colors }));
995
- }
996
- // Use react-confetti with react-use's useWindowSize if available
997
- // Otherwise fall back to default dimensions
998
- let width = 0;
999
- let height = 0;
1000
- if (legacyUseWindowSize) {
1001
- const size = legacyUseWindowSize();
1002
- width = size.width;
1003
- height = size.height;
1004
- }
1005
- else if (typeof window !== 'undefined') {
1006
- width = window.innerWidth;
1007
- height = window.innerHeight;
1008
- }
1009
- if (!show)
1010
- return null;
1011
- return (React.createElement(Confetti, { width: width, height: height, numberOfPieces: particleCount, recycle: false, colors: colors, style: {
1012
- position: 'fixed',
1013
- top: 0,
1014
- left: 0,
1015
- zIndex: 10001,
1016
- pointerEvents: 'none',
1017
- } }));
1018
- };
1019
- };
1020
-
1021
- const AchievementContext = createContext(undefined);
1022
- const AchievementProvider = ({ achievements: achievementsConfig, storage = 'local', children, icons = {}, onError, restApiConfig, ui = {}, useBuiltInUI = false, engine: externalEngine, eventMapping, }) => {
1023
- // VALIDATION: Prevent mixing patterns
1024
- if (achievementsConfig && externalEngine) {
1025
- throw new Error('Cannot provide both "achievements" and "engine" props to AchievementProvider.\n\n' +
1026
- 'Choose one pattern:\n' +
1027
- '1. OLD metric-based: <AchievementProvider achievements={config}>\n' +
1028
- '2. NEW event-based: <AchievementProvider engine={myEngine}>');
1029
- }
1030
- // Track which pattern is being used
1031
- const isLegacyPattern = Boolean(achievementsConfig);
1032
- // Engine instance (either external or auto-created)
1033
- // Initialize synchronously BEFORE first render to avoid timing issues
1034
- const [engine] = useState(() => {
1035
- if (externalEngine) {
1036
- // NEW PATTERN: Use external injected engine
1037
- return externalEngine;
1038
- }
1039
- // OLD PATTERN: Auto-create engine from achievements
1040
- if (!achievementsConfig) {
1041
- throw new Error('AchievementProvider requires either "achievements" or "engine" prop.\n\n' +
1042
- '1. OLD pattern: <AchievementProvider achievements={config}>\n' +
1043
- '2. NEW pattern: <AchievementProvider engine={myEngine}>');
1044
- }
1045
- return new AchievementEngine({
1046
- achievements: achievementsConfig,
1047
- storage: storage, // Type cast needed for compatibility
1048
- restApiConfig,
1049
- onError: onError,
1050
- eventMapping,
1051
- });
1052
- });
1053
- // React state synced from engine
1054
- // Initialize with current engine state (engine is available immediately)
1055
- const [achievementState, setAchievementState] = useState(() => {
1056
- const unlocked = engine.getUnlocked();
1057
- return {
1058
- unlocked: [...unlocked],
1059
- all: {}, // Will be populated by getAllAchievements
1060
- };
1061
- });
1062
- // Track which achievements have been seen to avoid duplicate notifications
1063
- const seenAchievementsRef = useRef(new Set(engine.getUnlocked()));
1064
- const [showConfetti, setShowConfetti] = useState(false);
1065
- // NEW: UI component resolution state (v3.6.0)
1066
- const [legacyLibraries, setLegacyLibraries] = useState(null);
1067
- const [uiReady, setUiReady] = useState(useBuiltInUI);
1068
- const [currentNotification, setCurrentNotification] = useState(null);
1069
- // Cleanup: Destroy engine on unmount (only if we auto-created it)
1070
- useEffect(() => {
1071
- return () => {
1072
- // Only destroy if we auto-created the engine
1073
- if (!externalEngine) {
1074
- engine.destroy();
1075
- }
1076
- };
1077
- }, [engine, externalEngine]);
1078
- // Subscribe to engine events
1079
- useEffect(() => {
1080
- // Engine is always available (initialized synchronously)
1081
- // Handle achievement unlocked
1082
- const unsubscribeUnlocked = engine.on('achievement:unlocked', (event) => {
1083
- // Update unlocked list
1084
- setAchievementState(prev => (Object.assign(Object.assign({}, prev), { unlocked: [...engine.getUnlocked()] })));
1085
- // Show notification if not seen before
1086
- if (!seenAchievementsRef.current.has(event.achievementId)) {
1087
- seenAchievementsRef.current.add(event.achievementId);
1088
- if (ui.enableNotifications !== false) {
1089
- // Create a full AchievementWithStatus object for the notification
1090
- const unlockedAchievement = {
1091
- achievementId: event.achievementId,
1092
- achievementTitle: event.achievementTitle,
1093
- achievementDescription: event.achievementDescription,
1094
- achievementIconKey: event.achievementIconKey,
1095
- isUnlocked: true,
1096
- };
1097
- setCurrentNotification(unlockedAchievement);
1098
- // Show confetti
1099
- setShowConfetti(true);
1100
- // Hide confetti after 5 seconds
1101
- setTimeout(() => {
1102
- setShowConfetti(false);
1103
- }, 5000);
1104
- }
1105
- }
1106
- });
1107
- // Handle state changes (for syncing unlocked list)
1108
- const unsubscribeStateChanged = engine.on('state:changed', () => {
1109
- setAchievementState(prev => (Object.assign(Object.assign({}, prev), { unlocked: [...engine.getUnlocked()] })));
1110
- });
1111
- return () => {
1112
- unsubscribeUnlocked();
1113
- unsubscribeStateChanged();
1114
- };
1115
- }, [engine, icons, ui.enableNotifications]);
1116
- // Detect legacy UI libraries on mount
1117
- useEffect(() => {
1118
- if (useBuiltInUI) {
1119
- setUiReady(true);
1120
- return;
1121
- }
1122
- detectLegacyLibraries().then((libs) => {
1123
- setLegacyLibraries(libs);
1124
- setUiReady(true);
1125
- });
1126
- }, [useBuiltInUI]);
1127
- // Resolve UI components based on detection and config
1128
- const NotificationComponent = ui.NotificationComponent ||
1129
- (useBuiltInUI ? BuiltInNotification :
1130
- legacyLibraries && Object.keys(legacyLibraries).length > 0 && legacyLibraries.toast
1131
- ? createLegacyToastNotification(legacyLibraries)
1132
- : BuiltInNotification);
1133
- const ConfettiComponentResolved = ui.ConfettiComponent ||
1134
- (useBuiltInUI ? BuiltInConfetti :
1135
- legacyLibraries && Object.keys(legacyLibraries).length > 0 && legacyLibraries.Confetti
1136
- ? createLegacyConfettiWrapper(legacyLibraries)
1137
- : BuiltInConfetti);
1138
- // Context methods - delegate to engine
1139
- const update = (newMetrics) => {
1140
- engine.update(newMetrics);
1141
- };
1142
- const reset = () => {
1143
- engine.reset();
1144
- seenAchievementsRef.current.clear();
1145
- setShowConfetti(false);
1146
- setCurrentNotification(null);
1147
- };
1148
- const getState = () => {
1149
- const metrics = engine.getMetrics();
1150
- const unlocked = engine.getUnlocked();
1151
- // Convert metrics to array format for backward compatibility
1152
- const metricsInArrayFormat = {};
1153
- Object.entries(metrics).forEach(([key, value]) => {
1154
- metricsInArrayFormat[key] = Array.isArray(value) ? value : [value];
1155
- });
1156
- return {
1157
- metrics: metricsInArrayFormat,
1158
- unlocked: [...unlocked],
1159
- };
1160
- };
1161
- const exportData = () => {
1162
- return engine.export();
1163
- };
1164
- const importData = (jsonString, options) => {
1165
- var _a;
1166
- const result = engine.import(jsonString, options);
1167
- // Update seen achievements to prevent duplicate notifications
1168
- if ((result === null || result === void 0 ? void 0 : result.success) && 'mergedUnlocked' in result) {
1169
- (_a = result.mergedUnlocked) === null || _a === void 0 ? void 0 : _a.forEach((id) => {
1170
- seenAchievementsRef.current.add(id);
1171
- });
1172
- }
1173
- return result;
1174
- };
1175
- const getAllAchievements = () => {
1176
- return engine.getAllAchievements();
1177
- };
1178
- return (React.createElement(AchievementContext.Provider, { value: {
1179
- update,
1180
- achievements: achievementState,
1181
- reset,
1182
- getState,
1183
- exportData,
1184
- importData,
1185
- getAllAchievements,
1186
- engine, // Always defined - no undefined fallback needed
1187
- _isLegacyPattern: isLegacyPattern, // NEW: Expose pattern flag to hooks
1188
- } },
1189
- children,
1190
- uiReady && currentNotification && ui.enableNotifications !== false && (React.createElement(NotificationComponent, { achievement: currentNotification, onClose: () => setCurrentNotification(null), duration: 5000, position: ui.notificationPosition || 'top-center', theme: ui.theme || 'modern', icons: icons })),
1191
- uiReady && ui.enableConfetti !== false && (React.createElement(ConfettiComponentResolved, { show: showConfetti, duration: 5000 }))));
1192
- };
1193
-
1194
- const useAchievements = () => {
1195
- const context = useContext(AchievementContext);
1196
- if (!context) {
1197
- throw new Error('useAchievements must be used within an AchievementProvider');
1198
- }
1199
- // STRICT CHECK: Detect if Provider has injected engine (new pattern)
1200
- if (!context._isLegacyPattern) {
1201
- throw new Error('Cannot use useAchievements when AchievementProvider has injected engine.\n\n' +
1202
- 'You are using the NEW event-based pattern.\n' +
1203
- 'useAchievements is for the OLD metric-based pattern only.\n\n' +
1204
- 'Use engine.emit() instead:\n' +
1205
- ' const engine = useAchievementEngine();\n' +
1206
- ' engine.emit("eventName", data);\n\n' +
1207
- 'Or switch to the old pattern:\n' +
1208
- ' <AchievementProvider achievements={config}>\n' +
1209
- ' const { track } = useAchievements();\n' +
1210
- ' track("score", 100);');
1211
- }
1212
- return context;
1213
- };
1214
-
1215
- /**
1216
- * A simplified hook for achievement tracking.
1217
- * Provides an easier API for common use cases while maintaining access to advanced features.
1218
- */
1219
- const useSimpleAchievements = () => {
1220
- const { update, achievements, reset, getState, exportData, importData, getAllAchievements } = useAchievements();
1221
- return {
1222
- /**
1223
- * Track a metric value for achievements
1224
- * @param metric - The metric name (e.g., 'score', 'level')
1225
- * @param value - The metric value
1226
- */
1227
- track: (metric, value) => update({ [metric]: value }),
1228
- /**
1229
- * Increment a numeric metric by a specified amount
1230
- * @param metric - The metric name (e.g., 'buttonClicks', 'score')
1231
- * @param amount - The amount to increment by (defaults to 1)
1232
- */
1233
- increment: (metric, amount = 1) => {
1234
- const currentState = getState();
1235
- const currentMetricArray = currentState.metrics[metric] || [0];
1236
- const currentValue = Array.isArray(currentMetricArray) ? currentMetricArray[0] : currentMetricArray;
1237
- const newValue = (typeof currentValue === 'number' ? currentValue : 0) + amount;
1238
- update({ [metric]: newValue });
1239
- },
1240
- /**
1241
- * Track multiple metrics at once
1242
- * @param metrics - Object with metric names as keys and values
1243
- */
1244
- trackMultiple: (metrics) => update(metrics),
1245
- /**
1246
- * Array of unlocked achievement IDs
1247
- */
1248
- unlocked: achievements.unlocked,
1249
- /**
1250
- * All available achievements
1251
- */
1252
- all: achievements.all,
1253
- /**
1254
- * Number of unlocked achievements
1255
- */
1256
- unlockedCount: achievements.unlocked.length,
1257
- /**
1258
- * Reset all achievement progress
1259
- */
1260
- reset,
1261
- /**
1262
- * Get current state (advanced usage)
1263
- */
1264
- getState,
1265
- /**
1266
- * Export achievement data to JSON string
1267
- * @returns JSON string containing all achievement data
1268
- */
1269
- exportData,
1270
- /**
1271
- * Import achievement data from JSON string
1272
- * @param jsonString - JSON string containing exported achievement data
1273
- * @param options - Import options (merge strategy, validation)
1274
- * @returns Import result with success status and any errors
1275
- */
1276
- importData,
1277
- /**
1278
- * Get all achievements with their unlock status
1279
- * @returns Array of achievements with isUnlocked boolean property
1280
- */
1281
- getAllAchievements,
1282
- };
1283
- };
1284
-
1285
- /**
1286
- * Hook to access the injected AchievementEngine instance (NEW event-based pattern)
1287
- *
1288
- * IMPORTANT: This hook only works when Provider has an externally created engine injected via the `engine` prop.
1289
- * Do NOT use this hook with the old `achievements` prop pattern - use `useAchievements()` instead.
1290
- *
1291
- * @example
1292
- * ```tsx
1293
- * // Create engine outside React
1294
- * import { AchievementEngine } from 'achievements-engine';
1295
- *
1296
- * const myEngine = new AchievementEngine({
1297
- * achievements: config,
1298
- * eventMapping: { 'userScored': 'score', 'levelUp': 'level' },
1299
- * storage: 'local'
1300
- * });
1301
- *
1302
- * // Inject into Provider
1303
- * <AchievementProvider engine={myEngine}>
1304
- * <App />
1305
- * </AchievementProvider>
1306
- *
1307
- * function App() {
1308
- * const engine = useAchievementEngine();
1309
- * engine.emit('userScored', 100);
1310
- * engine.emit('levelUp', 5);
1311
- * }
1312
- * ```
1313
- *
1314
- * @returns AchievementEngine instance
1315
- * @throws Error if used with old achievements prop pattern
1316
- * @throws Error if used outside AchievementProvider
1317
- * @since 3.8.0
1318
- */
1319
- const useAchievementEngine = () => {
1320
- const context = useContext(AchievementContext);
1321
- if (!context) {
1322
- throw new Error('useAchievementEngine must be used within an AchievementProvider.\n\n' +
1323
- 'Wrap your component tree:\n' +
1324
- 'const myEngine = new AchievementEngine({ achievements, eventMapping });\n' +
1325
- '<AchievementProvider engine={myEngine}>\n' +
1326
- ' <YourComponent />\n' +
1327
- '</AchievementProvider>');
1328
- }
1329
- // STRICT CHECK: Detect if Provider was initialized with achievements (old pattern)
1330
- if (context._isLegacyPattern) {
1331
- throw new Error('Cannot use useAchievementEngine when AchievementProvider has achievements prop.\n\n' +
1332
- 'You are using the OLD metric-based pattern.\n' +
1333
- 'useAchievementEngine is for the NEW event-based pattern only.\n\n' +
1334
- 'Choose one:\n\n' +
1335
- '1. OLD PATTERN (keep current code):\n' +
1336
- ' <AchievementProvider achievements={config}>\n' +
1337
- ' const { track } = useAchievements();\n' +
1338
- ' track("score", 100);\n\n' +
1339
- '2. NEW PATTERN (migrate to events):\n' +
1340
- ' const engine = new AchievementEngine({ achievements, eventMapping });\n' +
1341
- ' <AchievementProvider engine={engine}>\n' +
1342
- ' const engine = useAchievementEngine();\n' +
1343
- ' engine.emit("userScored", 100);');
1344
- }
1345
- if (!context.engine) {
1346
- throw new Error('No engine available. AchievementProvider requires either:\n' +
1347
- '1. achievements prop (old pattern)\n' +
1348
- '2. engine prop (new pattern)');
1349
- }
1350
- return context.engine;
1351
- };
1352
-
1353
- export { AchievementContext, AchievementProvider, BadgesButton, BadgesButtonWithModal, BadgesModal, BuiltInConfetti, BuiltInModal, BuiltInNotification, ConfettiWrapper, defaultAchievementIcons, defaultStyles, isAsyncStorage, useAchievementEngine, useAchievements, useSimpleAchievements, useWindowSize };
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 };
1354
1306
  //# sourceMappingURL=index.esm.js.map