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.cjs CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  var achievementsEngine = require('achievements-engine');
4
4
  var React = require('react');
5
- var Modal = require('react-modal');
6
- var Confetti = require('react-confetti');
7
5
 
8
6
  // Type guard to detect async storage
9
7
  function isAsyncStorage(storage) {
@@ -19,6 +17,174 @@ var StorageType;
19
17
  StorageType["RestAPI"] = "restapi"; // Asynchronous REST API storage
20
18
  })(StorageType || (StorageType = {}));
21
19
 
20
+ /******************************************************************************
21
+ Copyright (c) Microsoft Corporation.
22
+
23
+ Permission to use, copy, modify, and/or distribute this software for any
24
+ purpose with or without fee is hereby granted.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
27
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
28
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
29
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
30
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
31
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
32
+ PERFORMANCE OF THIS SOFTWARE.
33
+ ***************************************************************************** */
34
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
35
+
36
+
37
+ function __rest(s, e) {
38
+ var t = {};
39
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
40
+ t[p] = s[p];
41
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
42
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
43
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
44
+ t[p[i]] = s[p[i]];
45
+ }
46
+ return t;
47
+ }
48
+
49
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ };
53
+
54
+ const warnedMessages = new Set();
55
+ function warnDeprecation(message) {
56
+ var _a, _b;
57
+ const isProduction = typeof globalThis !== 'undefined' &&
58
+ ((_b = (_a = globalThis.process) === null || _a === void 0 ? void 0 : _a.env) === null || _b === void 0 ? void 0 : _b.NODE_ENV) === 'production';
59
+ if (isProduction || warnedMessages.has(message)) {
60
+ return;
61
+ }
62
+ warnedMessages.add(message);
63
+ console.warn(`[react-achievements] ${message}`);
64
+ }
65
+
66
+ const AchievementContext = React.createContext(undefined);
67
+ const getAllAchievementRecord = (engine) => {
68
+ return Object.fromEntries(engine.getAllAchievements().map((achievement) => [
69
+ achievement.achievementId,
70
+ achievement,
71
+ ]));
72
+ };
73
+ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'local', children, onError, useBuiltInUI, restApiConfig, engine: externalEngine, eventMapping, icons = {}, }) => {
74
+ if (useBuiltInUI !== undefined) {
75
+ warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 4.2.');
76
+ }
77
+ if (achievementsConfig && externalEngine) {
78
+ throw new Error('Cannot provide both "achievements" and "engine" props to AchievementProvider.\n\n' +
79
+ 'Choose one pattern:\n' +
80
+ '1. Direct metric tracking: <AchievementProvider achievements={config}>\n' +
81
+ '2. Event-based tracking: <AchievementProvider engine={myEngine}>');
82
+ }
83
+ const isProviderCreatedEngine = Boolean(achievementsConfig);
84
+ const [engine] = React.useState(() => {
85
+ if (externalEngine) {
86
+ return externalEngine;
87
+ }
88
+ if (!achievementsConfig) {
89
+ throw new Error('AchievementProvider requires either "achievements" or "engine" prop.\n\n' +
90
+ '1. Direct metric tracking: <AchievementProvider achievements={config}>\n' +
91
+ '2. Event-based tracking: <AchievementProvider engine={myEngine}>');
92
+ }
93
+ return new achievementsEngine.AchievementEngine({
94
+ achievements: achievementsConfig,
95
+ storage: storage,
96
+ restApiConfig,
97
+ onError: onError,
98
+ eventMapping,
99
+ });
100
+ });
101
+ const [achievementState, setAchievementState] = React.useState(() => ({
102
+ unlocked: [...engine.getUnlocked()],
103
+ all: getAllAchievementRecord(engine),
104
+ }));
105
+ const syncAchievementState = React.useCallback(() => {
106
+ setAchievementState({
107
+ unlocked: [...engine.getUnlocked()],
108
+ all: getAllAchievementRecord(engine),
109
+ });
110
+ }, [engine]);
111
+ React.useEffect(() => {
112
+ return () => {
113
+ if (!externalEngine) {
114
+ engine.destroy();
115
+ }
116
+ };
117
+ }, [engine, externalEngine]);
118
+ React.useEffect(() => {
119
+ const unsubscribeUnlocked = engine.on('achievement:unlocked', syncAchievementState);
120
+ const unsubscribeStateChanged = engine.on('state:changed', syncAchievementState);
121
+ return () => {
122
+ unsubscribeUnlocked();
123
+ unsubscribeStateChanged();
124
+ };
125
+ }, [engine, syncAchievementState]);
126
+ const update = (newMetrics) => {
127
+ engine.update(newMetrics);
128
+ };
129
+ const reset = () => {
130
+ engine.reset();
131
+ syncAchievementState();
132
+ };
133
+ const getState = () => {
134
+ const metrics = engine.getMetrics();
135
+ const unlocked = engine.getUnlocked();
136
+ const metricsInArrayFormat = {};
137
+ Object.entries(metrics).forEach(([key, value]) => {
138
+ metricsInArrayFormat[key] = Array.isArray(value) ? value : [value];
139
+ });
140
+ return {
141
+ metrics: metricsInArrayFormat,
142
+ unlocked: [...unlocked],
143
+ };
144
+ };
145
+ const exportData = () => {
146
+ return engine.export();
147
+ };
148
+ const importData = (jsonString, options) => {
149
+ const result = engine.import(jsonString, options);
150
+ syncAchievementState();
151
+ return result;
152
+ };
153
+ const getAllAchievements = () => {
154
+ return engine.getAllAchievements();
155
+ };
156
+ return (React.createElement(AchievementContext.Provider, { value: {
157
+ update,
158
+ achievements: achievementState,
159
+ reset,
160
+ getState,
161
+ exportData,
162
+ importData,
163
+ getAllAchievements,
164
+ engine,
165
+ icons,
166
+ _isLegacyPattern: isProviderCreatedEngine,
167
+ } }, children));
168
+ };
169
+
170
+ /**
171
+ * Access the active AchievementEngine instance.
172
+ *
173
+ * In v4 this works with both provider-created engines (`achievements` prop) and
174
+ * injected engines (`engine` prop).
175
+ */
176
+ const useAchievementEngine = () => {
177
+ const context = React.useContext(AchievementContext);
178
+ if (!context) {
179
+ throw new Error('useAchievementEngine must be used within an AchievementProvider.\n\n' +
180
+ 'Wrap your component tree:\n' +
181
+ '<AchievementProvider achievements={achievements}>\n' +
182
+ ' <YourComponent />\n' +
183
+ '</AchievementProvider>');
184
+ }
185
+ return context.engine;
186
+ };
187
+
22
188
  /**
23
189
  * Built-in theme presets
24
190
  */
@@ -135,56 +301,6 @@ function getTheme(name) {
135
301
  return builtInThemes[name];
136
302
  }
137
303
 
138
- const getPositionStyles = (position) => {
139
- const base = {
140
- position: 'fixed',
141
- margin: '20px',
142
- zIndex: 1000,
143
- };
144
- switch (position) {
145
- case 'top-left':
146
- return Object.assign(Object.assign({}, base), { top: 0, left: 0 });
147
- case 'top-right':
148
- return Object.assign(Object.assign({}, base), { top: 0, right: 0 });
149
- case 'bottom-left':
150
- return Object.assign(Object.assign({}, base), { bottom: 0, left: 0 });
151
- case 'bottom-right':
152
- return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
153
- }
154
- };
155
- const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
156
- // Get theme configuration for consistent styling
157
- const themeConfig = getTheme(theme) || builtInThemes.modern;
158
- const accentColor = themeConfig.notification.accentColor;
159
- // Different styling for fixed vs inline placement
160
- const baseStyles = placement === 'inline'
161
- ? Object.assign({
162
- // Inline mode: looks like a navigation item
163
- 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({
164
- // Fixed mode: floating button
165
- 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);
166
- return (React.createElement("button", { onClick: onClick, style: baseStyles, onMouseEnter: (e) => {
167
- if (placement === 'inline') {
168
- // Inline mode: subtle background color change
169
- e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
170
- }
171
- else {
172
- // Fixed mode: scale transformation
173
- e.target.style.transform = 'scale(1.05)';
174
- }
175
- }, onMouseLeave: (e) => {
176
- if (placement === 'inline') {
177
- e.target.style.backgroundColor = 'transparent';
178
- }
179
- else {
180
- e.target.style.transform = 'scale(1)';
181
- }
182
- }, "data-placement": placement, "data-testid": "badges-button" },
183
- "\uD83C\uDFC6 Achievements (",
184
- unlockedAchievements.length,
185
- ")"));
186
- };
187
-
188
304
  const defaultAchievementIcons = {
189
305
  // Essential fallback icons for system use
190
306
  default: '⭐', // Fallback when no icon is provided
@@ -196,184 +312,123 @@ const defaultAchievementIcons = {
196
312
  star: '⭐',
197
313
  };
198
314
 
199
- const defaultStyles = {
200
- badgesButton: {
201
- backgroundColor: '#4CAF50',
202
- color: 'white',
203
- padding: '10px 20px',
315
+ /**
316
+ * Built-in notification component
317
+ * Modern, theme-aware achievement notification with smooth animations
318
+ */
319
+ const BuiltInNotification = ({ achievement, onClose, duration = 5000, position = 'top-center', theme = 'modern', icons = {}, stackIndex = 0, }) => {
320
+ var _a, _b, _c;
321
+ const [isVisible, setIsVisible] = React.useState(false);
322
+ const [isExiting, setIsExiting] = React.useState(false);
323
+ // Merge custom icons with defaults
324
+ const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
325
+ // Get theme configuration
326
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
327
+ const { notification: themeStyles } = themeConfig;
328
+ React.useEffect(() => {
329
+ // Slide in animation
330
+ const showTimer = setTimeout(() => setIsVisible(true), 10);
331
+ // Auto-dismiss
332
+ const dismissTimer = setTimeout(() => {
333
+ setIsExiting(true);
334
+ setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
335
+ }, duration);
336
+ return () => {
337
+ clearTimeout(showTimer);
338
+ clearTimeout(dismissTimer);
339
+ };
340
+ }, [duration, onClose]);
341
+ const getPositionStyles = () => {
342
+ const stackedOffset = 20 + stackIndex * 104;
343
+ const base = {
344
+ position: 'fixed',
345
+ zIndex: 9999,
346
+ };
347
+ switch (position) {
348
+ case 'top-center':
349
+ return Object.assign(Object.assign({}, base), { top: stackedOffset, left: '50%', transform: 'translateX(-50%)' });
350
+ case 'top-left':
351
+ return Object.assign(Object.assign({}, base), { top: stackedOffset, left: 20 });
352
+ case 'top-right':
353
+ return Object.assign(Object.assign({}, base), { top: stackedOffset, right: 20 });
354
+ case 'bottom-center':
355
+ return Object.assign(Object.assign({}, base), { bottom: stackedOffset, left: '50%', transform: 'translateX(-50%)' });
356
+ case 'bottom-left':
357
+ return Object.assign(Object.assign({}, base), { bottom: stackedOffset, left: 20 });
358
+ case 'bottom-right':
359
+ return Object.assign(Object.assign({}, base), { bottom: stackedOffset, right: 20 });
360
+ default:
361
+ return Object.assign(Object.assign({}, base), { top: stackedOffset, left: '50%', transform: 'translateX(-50%)' });
362
+ }
363
+ };
364
+ 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')
365
+ ? `translateY(${isVisible && !isExiting ? '0' : '-20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`
366
+ : `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' });
367
+ const iconStyles = {
368
+ fontSize: '48px',
369
+ lineHeight: 1,
370
+ flexShrink: 0,
371
+ };
372
+ const contentStyles = {
373
+ flex: 1,
374
+ color: themeStyles.textColor,
375
+ minWidth: 0,
376
+ };
377
+ const headerStyles = {
378
+ fontSize: ((_a = themeStyles.fontSize) === null || _a === void 0 ? void 0 : _a.header) || '12px',
379
+ textTransform: 'uppercase',
380
+ letterSpacing: '1px',
381
+ opacity: 0.8,
382
+ marginBottom: '4px',
383
+ color: themeStyles.accentColor,
384
+ fontWeight: 600,
385
+ };
386
+ const titleStyles = {
387
+ fontSize: ((_b = themeStyles.fontSize) === null || _b === void 0 ? void 0 : _b.title) || '18px',
388
+ fontWeight: 'bold',
389
+ marginBottom: '4px',
390
+ overflow: 'hidden',
391
+ textOverflow: 'ellipsis',
392
+ whiteSpace: 'nowrap',
393
+ };
394
+ const descriptionStyles = {
395
+ fontSize: ((_c = themeStyles.fontSize) === null || _c === void 0 ? void 0 : _c.description) || '14px',
396
+ opacity: 0.9,
397
+ overflow: 'hidden',
398
+ textOverflow: 'ellipsis',
399
+ display: '-webkit-box',
400
+ WebkitLineClamp: 2,
401
+ WebkitBoxOrient: 'vertical',
402
+ };
403
+ const closeButtonStyles = {
404
+ background: 'none',
204
405
  border: 'none',
205
- borderRadius: '20px',
406
+ color: themeStyles.textColor,
407
+ fontSize: '24px',
206
408
  cursor: 'pointer',
207
- display: 'flex',
208
- alignItems: 'center',
209
- gap: '8px',
210
- fontSize: '16px',
211
- boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
212
- transition: 'transform 0.2s ease-in-out',
213
- },
214
- badgesModal: {
215
- overlay: {
216
- backgroundColor: 'rgba(0, 0, 0, 0.75)',
217
- display: 'flex',
218
- alignItems: 'center',
219
- justifyContent: 'center',
220
- zIndex: 1000,
221
- },
222
- content: {
223
- position: 'relative',
224
- background: '#fff',
225
- borderRadius: '8px',
226
- padding: '20px',
227
- maxWidth: '500px',
228
- width: '90%',
229
- maxHeight: '80vh',
230
- overflow: 'auto',
231
- },
232
- header: {
233
- display: 'flex',
234
- justifyContent: 'space-between',
235
- alignItems: 'center',
236
- marginBottom: '20px',
237
- },
238
- closeButton: {
239
- background: 'none',
240
- border: 'none',
241
- fontSize: '24px',
242
- cursor: 'pointer',
243
- padding: '0',
244
- },
245
- achievementList: {
246
- display: 'flex',
247
- flexDirection: 'column',
248
- gap: '16px',
249
- },
250
- achievementItem: {
251
- display: 'flex',
252
- gap: '16px',
253
- padding: '16px',
254
- borderRadius: '8px',
255
- backgroundColor: '#f5f5f5',
256
- alignItems: 'center',
257
- },
258
- achievementTitle: {
259
- margin: '0',
260
- fontSize: '18px',
261
- fontWeight: 'bold',
262
- },
263
- achievementDescription: {
264
- margin: '4px 0 0 0',
265
- color: '#666',
266
- },
267
- achievementIcon: {
268
- fontSize: '32px',
269
- display: 'flex',
270
- alignItems: 'center',
271
- justifyContent: 'center',
272
- },
273
- lockIcon: {
274
- fontSize: '24px',
275
- position: 'absolute',
276
- top: '50%',
277
- right: '16px',
278
- transform: 'translateY(-50%)',
279
- },
280
- lockedAchievementItem: {
281
- display: 'flex',
282
- gap: '16px',
283
- padding: '16px',
284
- borderRadius: '8px',
285
- backgroundColor: '#e0e0e0',
286
- alignItems: 'center',
287
- opacity: 0.5,
288
- },
289
- },
290
- };
291
-
292
- const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, showAllAchievements = false, showUnlockConditions = false, allAchievements, }) => {
293
- // Merge custom icons with default icons, with custom icons taking precedence
294
- const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
295
- return (React.createElement(Modal, { isOpen: isOpen, onRequestClose: onClose, style: {
296
- overlay: Object.assign(Object.assign({}, defaultStyles.badgesModal.overlay), styles === null || styles === void 0 ? void 0 : styles.overlay),
297
- content: Object.assign(Object.assign({}, defaultStyles.badgesModal.content), styles === null || styles === void 0 ? void 0 : styles.content)
298
- }, contentLabel: "Achievements" },
299
- React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.header), styles === null || styles === void 0 ? void 0 : styles.header) },
300
- React.createElement("h2", { style: { margin: 0 } }, "\uD83C\uDFC6 Achievements"),
301
- 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")),
302
- React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementList), styles === null || styles === void 0 ? void 0 : styles.achievementList) }, (() => {
303
- // Determine which achievements to display
304
- const achievementsToDisplay = showAllAchievements && allAchievements
305
- ? allAchievements
306
- : achievements.map(a => (Object.assign(Object.assign({}, a), { isUnlocked: true })));
307
- return (React.createElement(React.Fragment, null,
308
- achievementsToDisplay.map((achievement) => {
309
- const isLocked = !achievement.isUnlocked;
310
- return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign(Object.assign({}, (isLocked
311
- ? 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' }) },
312
- 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
313
- ? mergedIcons[achievement.achievementIconKey]
314
- : mergedIcons.default || '⭐')),
315
- React.createElement("div", { style: { flex: 1 } },
316
- 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),
317
- 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' }) },
318
- achievement.achievementDescription,
319
- showUnlockConditions && isLocked && achievement.achievementDescription && (React.createElement("span", { style: {
320
- display: 'block',
321
- fontSize: '12px',
322
- marginTop: '4px',
323
- fontStyle: 'italic',
324
- color: '#888'
325
- } },
326
- "\uD83D\uDD13 ",
327
- achievement.achievementDescription)))),
328
- isLocked && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.lockIcon), styles === null || styles === void 0 ? void 0 : styles.lockIcon) }, "\uD83D\uDD12"))));
329
- }),
330
- achievementsToDisplay.length === 0 && (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, "No achievements configured."))));
331
- })())));
332
- };
333
-
334
- /**
335
- * BadgesButtonWithModal - A convenience component that combines BadgesButton and BadgesModal
336
- *
337
- * This component manages the modal state internally, providing a simplified API
338
- * for the common use case of displaying achievements.
339
- *
340
- * For advanced use cases requiring custom state management or multiple triggers,
341
- * use BadgesButton and BadgesModal separately.
342
- *
343
- * @example
344
- * ```tsx
345
- * // Simple usage
346
- * <BadgesButtonWithModal
347
- * unlockedAchievements={achievements.unlocked}
348
- * />
349
- *
350
- * // Show all achievements (locked + unlocked)
351
- * <BadgesButtonWithModal
352
- * unlockedAchievements={achievements.unlocked}
353
- * showAllAchievements={true}
354
- * allAchievements={getAllAchievements()}
355
- * showUnlockConditions={true}
356
- * />
357
- *
358
- * // Customize position and theme
359
- * <BadgesButtonWithModal
360
- * unlockedAchievements={achievements.unlocked}
361
- * position="top-right"
362
- * theme="gamified"
363
- * />
364
- *
365
- * // Inline mode for navigation
366
- * <BadgesButtonWithModal
367
- * unlockedAchievements={achievements.unlocked}
368
- * placement="inline"
369
- * />
370
- * ```
371
- */
372
- const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right', placement = 'fixed', showAllAchievements = false, allAchievements, showUnlockConditions = false, icons, theme = 'modern', buttonStyles, modalStyles, }) => {
373
- const [isModalOpen, setIsModalOpen] = React.useState(false);
374
- return (React.createElement(React.Fragment, null,
375
- React.createElement(BadgesButton, { onClick: () => setIsModalOpen(true), unlockedAchievements: unlockedAchievements, position: position, placement: placement, theme: theme, styles: buttonStyles }),
376
- React.createElement(BadgesModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: unlockedAchievements, showAllAchievements: showAllAchievements, allAchievements: allAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles })));
409
+ opacity: 0.6,
410
+ transition: 'opacity 0.2s',
411
+ padding: '4px',
412
+ flexShrink: 0,
413
+ lineHeight: 1,
414
+ };
415
+ // If achievementIconKey exists but not in mergedIcons, use it directly (might be an emoji)
416
+ // Otherwise, look up in mergedIcons or fall back to default
417
+ const icon = (achievement.achievementIconKey &&
418
+ mergedIcons[achievement.achievementIconKey]) ||
419
+ achievement.achievementIconKey ||
420
+ mergedIcons.default ||
421
+ '';
422
+ return (React.createElement("div", { style: containerStyles, "data-testid": "built-in-notification" },
423
+ React.createElement("div", { style: iconStyles }, icon),
424
+ React.createElement("div", { style: contentStyles },
425
+ React.createElement("div", { style: headerStyles }, "Achievement Unlocked!"),
426
+ React.createElement("div", { style: titleStyles }, achievement.achievementTitle),
427
+ achievement.achievementDescription && (React.createElement("div", { style: descriptionStyles }, achievement.achievementDescription))),
428
+ React.createElement("button", { onClick: () => {
429
+ setIsExiting(true);
430
+ setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
431
+ }, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close notification" }, "\u00D7")));
377
432
  };
378
433
 
379
434
  /**
@@ -416,137 +471,6 @@ function useWindowSize() {
416
471
  return size;
417
472
  }
418
473
 
419
- const ConfettiWrapper = ({ show }) => {
420
- const { width, height } = useWindowSize();
421
- if (!show)
422
- return null;
423
- return (React.createElement(Confetti, { width: width, height: height, numberOfPieces: 200, recycle: false, style: {
424
- position: 'fixed',
425
- top: 0,
426
- left: 0,
427
- zIndex: 1001,
428
- pointerEvents: 'none',
429
- } }));
430
- };
431
-
432
- /**
433
- * Built-in notification component
434
- * Modern, theme-aware achievement notification with smooth animations
435
- */
436
- const BuiltInNotification = ({ achievement, onClose, duration = 5000, position = 'top-center', theme = 'modern', icons = {}, }) => {
437
- var _a, _b, _c;
438
- const [isVisible, setIsVisible] = React.useState(false);
439
- const [isExiting, setIsExiting] = React.useState(false);
440
- // Merge custom icons with defaults
441
- const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
442
- // Get theme configuration
443
- const themeConfig = getTheme(theme) || builtInThemes.modern;
444
- const { notification: themeStyles } = themeConfig;
445
- React.useEffect(() => {
446
- // Slide in animation
447
- const showTimer = setTimeout(() => setIsVisible(true), 10);
448
- // Auto-dismiss
449
- const dismissTimer = setTimeout(() => {
450
- setIsExiting(true);
451
- setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
452
- }, duration);
453
- return () => {
454
- clearTimeout(showTimer);
455
- clearTimeout(dismissTimer);
456
- };
457
- }, [duration, onClose]);
458
- const getPositionStyles = () => {
459
- const base = {
460
- position: 'fixed',
461
- zIndex: 9999,
462
- };
463
- switch (position) {
464
- case 'top-center':
465
- return Object.assign(Object.assign({}, base), { top: 20, left: '50%', transform: 'translateX(-50%)' });
466
- case 'top-left':
467
- return Object.assign(Object.assign({}, base), { top: 20, left: 20 });
468
- case 'top-right':
469
- return Object.assign(Object.assign({}, base), { top: 20, right: 20 });
470
- case 'bottom-center':
471
- return Object.assign(Object.assign({}, base), { bottom: 20, left: '50%', transform: 'translateX(-50%)' });
472
- case 'bottom-left':
473
- return Object.assign(Object.assign({}, base), { bottom: 20, left: 20 });
474
- case 'bottom-right':
475
- return Object.assign(Object.assign({}, base), { bottom: 20, right: 20 });
476
- default:
477
- return Object.assign(Object.assign({}, base), { top: 20, left: '50%', transform: 'translateX(-50%)' });
478
- }
479
- };
480
- 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')
481
- ? `translateY(${isVisible && !isExiting ? '0' : '-20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`
482
- : `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' });
483
- const iconStyles = {
484
- fontSize: '48px',
485
- lineHeight: 1,
486
- flexShrink: 0,
487
- };
488
- const contentStyles = {
489
- flex: 1,
490
- color: themeStyles.textColor,
491
- minWidth: 0,
492
- };
493
- const headerStyles = {
494
- fontSize: ((_a = themeStyles.fontSize) === null || _a === void 0 ? void 0 : _a.header) || '12px',
495
- textTransform: 'uppercase',
496
- letterSpacing: '1px',
497
- opacity: 0.8,
498
- marginBottom: '4px',
499
- color: themeStyles.accentColor,
500
- fontWeight: 600,
501
- };
502
- const titleStyles = {
503
- fontSize: ((_b = themeStyles.fontSize) === null || _b === void 0 ? void 0 : _b.title) || '18px',
504
- fontWeight: 'bold',
505
- marginBottom: '4px',
506
- overflow: 'hidden',
507
- textOverflow: 'ellipsis',
508
- whiteSpace: 'nowrap',
509
- };
510
- const descriptionStyles = {
511
- fontSize: ((_c = themeStyles.fontSize) === null || _c === void 0 ? void 0 : _c.description) || '14px',
512
- opacity: 0.9,
513
- overflow: 'hidden',
514
- textOverflow: 'ellipsis',
515
- display: '-webkit-box',
516
- WebkitLineClamp: 2,
517
- WebkitBoxOrient: 'vertical',
518
- };
519
- const closeButtonStyles = {
520
- background: 'none',
521
- border: 'none',
522
- color: themeStyles.textColor,
523
- fontSize: '24px',
524
- cursor: 'pointer',
525
- opacity: 0.6,
526
- transition: 'opacity 0.2s',
527
- padding: '4px',
528
- flexShrink: 0,
529
- lineHeight: 1,
530
- };
531
- // If achievementIconKey exists but not in mergedIcons, use it directly (might be an emoji)
532
- // Otherwise, look up in mergedIcons or fall back to default
533
- const icon = (achievement.achievementIconKey &&
534
- mergedIcons[achievement.achievementIconKey]) ||
535
- achievement.achievementIconKey ||
536
- mergedIcons.default ||
537
- '⭐';
538
- return (React.createElement("div", { style: containerStyles, "data-testid": "built-in-notification" },
539
- React.createElement("div", { style: iconStyles }, icon),
540
- React.createElement("div", { style: contentStyles },
541
- React.createElement("div", { style: headerStyles }, "Achievement Unlocked!"),
542
- React.createElement("div", { style: titleStyles }, achievement.achievementTitle),
543
- achievement.achievementDescription && (React.createElement("div", { style: descriptionStyles }, achievement.achievementDescription))),
544
- React.createElement("button", { onClick: () => {
545
- setIsExiting(true);
546
- setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
547
- }, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close notification" }, "\u00D7")));
548
- };
549
-
550
474
  /**
551
475
  * Built-in confetti component
552
476
  * Lightweight CSS-based confetti animation
@@ -605,128 +529,565 @@ const BuiltInConfetti = ({ show, duration = 5000, particleCount = 50, colors = [
605
529
  transform: translateY(0) rotate(0deg);
606
530
  opacity: 1;
607
531
  }
608
- 100% {
609
- transform: translateY(${height + 50}px) rotate(720deg);
610
- opacity: 0;
532
+ 100% {
533
+ transform: translateY(${height + 50}px) rotate(720deg);
534
+ opacity: 0;
535
+ }
536
+ }
537
+ `),
538
+ React.createElement("div", { style: containerStyles, "data-testid": "built-in-confetti" }, particles)));
539
+ };
540
+
541
+ const NOTIFICATION_DURATION_MS = 5000;
542
+ const AchievementUIContext = React.createContext({
543
+ icons: {},
544
+ ui: {},
545
+ });
546
+ const AchievementEffects = ({ icons, ui }) => {
547
+ const engine = useAchievementEngine();
548
+ const seenAchievementsRef = React.useRef(new Set(engine.getUnlocked()));
549
+ const confettiTimerRef = React.useRef(null);
550
+ const [showConfetti, setShowConfetti] = React.useState(false);
551
+ const [notifications, setNotifications] = React.useState([]);
552
+ React.useEffect(() => {
553
+ const unsubscribeUnlocked = engine.on('achievement:unlocked', (event) => {
554
+ if (seenAchievementsRef.current.has(event.achievementId)) {
555
+ return;
556
+ }
557
+ seenAchievementsRef.current.add(event.achievementId);
558
+ const unlockedAchievement = {
559
+ achievementId: event.achievementId,
560
+ achievementTitle: event.achievementTitle,
561
+ achievementDescription: event.achievementDescription,
562
+ achievementIconKey: event.achievementIconKey,
563
+ isUnlocked: true,
564
+ };
565
+ if (ui.enableNotifications !== false) {
566
+ setNotifications((currentNotifications) => {
567
+ if (currentNotifications.some((notification) => notification.achievementId === unlockedAchievement.achievementId)) {
568
+ return currentNotifications;
569
+ }
570
+ return [...currentNotifications, unlockedAchievement];
571
+ });
572
+ }
573
+ if (ui.enableConfetti !== false) {
574
+ if (confettiTimerRef.current) {
575
+ clearTimeout(confettiTimerRef.current);
576
+ }
577
+ setShowConfetti(true);
578
+ confettiTimerRef.current = setTimeout(() => {
579
+ setShowConfetti(false);
580
+ confettiTimerRef.current = null;
581
+ }, NOTIFICATION_DURATION_MS);
582
+ }
583
+ });
584
+ const unsubscribeStateChanged = engine.on('state:changed', () => {
585
+ const unlocked = new Set(engine.getUnlocked());
586
+ seenAchievementsRef.current.forEach((achievementId) => {
587
+ if (!unlocked.has(achievementId)) {
588
+ seenAchievementsRef.current.delete(achievementId);
589
+ }
590
+ });
591
+ });
592
+ return () => {
593
+ unsubscribeUnlocked();
594
+ unsubscribeStateChanged();
595
+ if (confettiTimerRef.current) {
596
+ clearTimeout(confettiTimerRef.current);
597
+ }
598
+ };
599
+ }, [engine, ui.enableConfetti, ui.enableNotifications]);
600
+ const NotificationComponent = ui.NotificationComponent || BuiltInNotification;
601
+ const ConfettiComponentResolved = ui.ConfettiComponent || BuiltInConfetti;
602
+ return (React.createElement(React.Fragment, null,
603
+ ui.enableNotifications !== false &&
604
+ 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 }))),
605
+ ui.enableConfetti !== false && (React.createElement(ConfettiComponentResolved, { show: showConfetti, duration: NOTIFICATION_DURATION_MS }))));
606
+ };
607
+ const AchievementProvider = (_a) => {
608
+ var { children, icons = {}, ui = {}, useBuiltInUI } = _a, providerProps = __rest(_a, ["children", "icons", "ui", "useBuiltInUI"]);
609
+ if (useBuiltInUI !== undefined) {
610
+ warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 4.2.');
611
+ }
612
+ const uiContextValue = React.useMemo(() => ({ icons, ui }), [icons, ui]);
613
+ return (React.createElement(AchievementUIContext.Provider, { value: uiContextValue },
614
+ React.createElement(AchievementProvider$1, Object.assign({}, providerProps, { icons: icons }),
615
+ children,
616
+ React.createElement(AchievementEffects, { icons: icons, ui: ui }))));
617
+ };
618
+
619
+ const useAchievements = () => {
620
+ const context = React.useContext(AchievementContext);
621
+ if (!context) {
622
+ throw new Error('useAchievements must be used within an AchievementProvider');
623
+ }
624
+ return context;
625
+ };
626
+
627
+ const useAchievementState = () => {
628
+ const { achievements, getAllAchievements, getState } = useAchievements();
629
+ const allAchievements = getAllAchievements();
630
+ const unlockedIds = achievements.unlocked;
631
+ const unlockedAchievementSet = new Set(unlockedIds);
632
+ const unlockedAchievements = allAchievements.filter((achievement) => unlockedAchievementSet.has(achievement.achievementId));
633
+ return {
634
+ unlockedIds,
635
+ unlockedAchievements,
636
+ allAchievements,
637
+ unlockedCount: unlockedIds.length,
638
+ totalCount: allAchievements.length,
639
+ metrics: getState().metrics,
640
+ };
641
+ };
642
+
643
+ const defaultStyles = {
644
+ badgesButton: {
645
+ backgroundColor: '#4CAF50',
646
+ color: 'white',
647
+ padding: '10px 20px',
648
+ border: 'none',
649
+ borderRadius: '20px',
650
+ cursor: 'pointer',
651
+ display: 'flex',
652
+ alignItems: 'center',
653
+ gap: '8px',
654
+ fontSize: '16px',
655
+ boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
656
+ transition: 'transform 0.2s ease-in-out',
657
+ },
658
+ badgesModal: {
659
+ overlay: {
660
+ position: 'fixed',
661
+ top: 0,
662
+ right: 0,
663
+ bottom: 0,
664
+ left: 0,
665
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
666
+ display: 'flex',
667
+ alignItems: 'center',
668
+ justifyContent: 'center',
669
+ zIndex: 10000,
670
+ },
671
+ content: {
672
+ position: 'relative',
673
+ background: '#fff',
674
+ borderRadius: '8px',
675
+ padding: '20px',
676
+ maxWidth: '500px',
677
+ width: '90%',
678
+ maxHeight: '80vh',
679
+ overflow: 'auto',
680
+ },
681
+ header: {
682
+ display: 'flex',
683
+ justifyContent: 'space-between',
684
+ alignItems: 'center',
685
+ marginBottom: '20px',
686
+ },
687
+ closeButton: {
688
+ background: 'none',
689
+ border: 'none',
690
+ fontSize: '24px',
691
+ cursor: 'pointer',
692
+ padding: '0',
693
+ },
694
+ achievementList: {
695
+ display: 'flex',
696
+ flexDirection: 'column',
697
+ gap: '16px',
698
+ },
699
+ achievementItem: {
700
+ display: 'flex',
701
+ gap: '16px',
702
+ padding: '16px',
703
+ borderRadius: '8px',
704
+ backgroundColor: '#f5f5f5',
705
+ alignItems: 'center',
706
+ },
707
+ achievementTitle: {
708
+ margin: '0',
709
+ fontSize: '18px',
710
+ fontWeight: 'bold',
711
+ },
712
+ achievementDescription: {
713
+ margin: '4px 0 0 0',
714
+ color: '#666',
715
+ },
716
+ achievementIcon: {
717
+ fontSize: '32px',
718
+ display: 'flex',
719
+ alignItems: 'center',
720
+ justifyContent: 'center',
721
+ },
722
+ lockIcon: {
723
+ fontSize: '24px',
724
+ position: 'absolute',
725
+ top: '50%',
726
+ right: '16px',
727
+ transform: 'translateY(-50%)',
728
+ },
729
+ lockedAchievementItem: {
730
+ display: 'flex',
731
+ gap: '16px',
732
+ padding: '16px',
733
+ borderRadius: '8px',
734
+ backgroundColor: '#e0e0e0',
735
+ alignItems: 'center',
736
+ opacity: 0.5,
737
+ },
738
+ },
739
+ levelProgress: {
740
+ container: {
741
+ display: 'flex',
742
+ flexDirection: 'column',
743
+ gap: '8px',
744
+ padding: '12px 16px',
745
+ },
746
+ header: {
747
+ display: 'flex',
748
+ alignItems: 'center',
749
+ justifyContent: 'space-between',
750
+ gap: '12px',
751
+ fontWeight: 600,
752
+ },
753
+ levelLabel: {
754
+ fontSize: '14px',
755
+ },
756
+ valueText: {
757
+ fontSize: '12px',
758
+ opacity: 0.8,
759
+ },
760
+ progressTrack: {
761
+ position: 'relative',
762
+ height: '10px',
763
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
764
+ borderRadius: '999px',
765
+ overflow: 'hidden',
766
+ },
767
+ progressBar: {
768
+ height: '100%',
769
+ borderRadius: '999px',
770
+ transition: 'width 0.2s ease-in-out',
771
+ },
772
+ progressText: {
773
+ fontSize: '12px',
774
+ opacity: 0.8,
775
+ textAlign: 'right',
776
+ },
777
+ },
778
+ };
779
+
780
+ const resolveIcon = (achievement, icons) => {
781
+ return ((achievement.achievementIconKey && icons[achievement.achievementIconKey]) ||
782
+ achievement.achievementIconKey ||
783
+ icons.default ||
784
+ '⭐');
785
+ };
786
+ const AchievementsList = ({ achievements, showLocked = true, showUnlockConditions = false, icons = {}, styles = {}, emptyState, className, renderAchievement, }) => {
787
+ const context = React.useContext(AchievementContext);
788
+ const uiContext = React.useContext(AchievementUIContext);
789
+ const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
790
+ const sourceAchievements = achievements || (context === null || context === void 0 ? void 0 : context.getAllAchievements());
791
+ if (!sourceAchievements) {
792
+ throw new Error('AchievementsList requires either an achievements prop or an AchievementProvider parent.');
793
+ }
794
+ const achievementsToDisplay = showLocked
795
+ ? sourceAchievements
796
+ : sourceAchievements.filter((achievement) => achievement.isUnlocked);
797
+ if (achievementsToDisplay.length === 0) {
798
+ return (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, emptyState || 'No achievements configured.'));
799
+ }
800
+ 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) => {
801
+ const isLocked = !achievement.isUnlocked;
802
+ const icon = resolveIcon(achievement, mergedIcons);
803
+ if (renderAchievement) {
804
+ return (React.createElement(React.Fragment, { key: achievement.achievementId }, renderAchievement({ achievement, isLocked, icon, index })));
805
+ }
806
+ return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign(Object.assign({}, (isLocked
807
+ ? 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' },
808
+ 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),
809
+ React.createElement("div", { style: { flex: 1 } },
810
+ 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),
811
+ 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' }) },
812
+ achievement.achievementDescription,
813
+ showUnlockConditions && isLocked && (React.createElement("span", { style: {
814
+ display: 'block',
815
+ fontSize: '12px',
816
+ marginTop: '4px',
817
+ fontStyle: 'italic',
818
+ color: '#888',
819
+ } },
820
+ "\uD83D\uDD13 ",
821
+ achievement.achievementDescription))))),
822
+ isLocked && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.lockIcon), styles === null || styles === void 0 ? void 0 : styles.lockIcon) }, "\uD83D\uDD12"))));
823
+ })));
824
+ };
825
+
826
+ const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achievements', styles = {}, icons = {}, showLocked = true, showUnlockConditions = false, emptyState, renderAchievement, theme, }) => {
827
+ const context = React.useContext(AchievementContext);
828
+ const uiContext = React.useContext(AchievementUIContext);
829
+ React.useEffect(() => {
830
+ if (!isOpen || typeof document === 'undefined') {
831
+ return;
832
+ }
833
+ const previousOverflow = document.body.style.overflow;
834
+ document.body.style.overflow = 'hidden';
835
+ const handleKeyDown = (event) => {
836
+ if (event.key === 'Escape') {
837
+ onClose();
838
+ }
839
+ };
840
+ document.addEventListener('keydown', handleKeyDown);
841
+ return () => {
842
+ document.body.style.overflow = previousOverflow;
843
+ document.removeEventListener('keydown', handleKeyDown);
844
+ };
845
+ }, [isOpen, onClose]);
846
+ if (!isOpen)
847
+ return null;
848
+ const CustomModal = uiContext.ui.ModalComponent;
849
+ const sourceAchievements = achievements || (context === null || context === void 0 ? void 0 : context.getAllAchievements());
850
+ const modalAchievements = showLocked
851
+ ? sourceAchievements
852
+ : sourceAchievements === null || sourceAchievements === void 0 ? void 0 : sourceAchievements.filter((achievement) => achievement.isUnlocked);
853
+ const resolvedTheme = theme || uiContext.ui.theme || 'modern';
854
+ const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
855
+ if (CustomModal) {
856
+ if (!modalAchievements) {
857
+ throw new Error('AchievementsModal requires either an achievements prop or an AchievementProvider parent.');
858
+ }
859
+ return (React.createElement(CustomModal, { isOpen: isOpen, onClose: onClose, achievements: modalAchievements, icons: mergedIcons, theme: resolvedTheme }));
860
+ }
861
+ 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" },
862
+ 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" },
863
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.header), styles === null || styles === void 0 ? void 0 : styles.header) },
864
+ React.createElement("h2", { id: "achievements-modal-title", style: { margin: 0 } }, title),
865
+ 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")),
866
+ React.createElement(AchievementsList, { achievements: modalAchievements, showLocked: showLocked, showUnlockConditions: showUnlockConditions, icons: icons, styles: styles, emptyState: emptyState, renderAchievement: renderAchievement }))));
867
+ };
868
+
869
+ const getPositionStyles$1 = (position) => {
870
+ const base = {
871
+ position: 'fixed',
872
+ margin: '20px',
873
+ zIndex: 1000,
874
+ };
875
+ switch (position) {
876
+ case 'top-left':
877
+ return Object.assign(Object.assign({}, base), { top: 0, left: 0 });
878
+ case 'top-right':
879
+ return Object.assign(Object.assign({}, base), { top: 0, right: 0 });
880
+ case 'bottom-left':
881
+ return Object.assign(Object.assign({}, base), { bottom: 0, left: 0 });
882
+ case 'bottom-right':
883
+ return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
884
+ }
885
+ };
886
+ 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, }) => {
887
+ const uiContext = React.useContext(AchievementUIContext);
888
+ const [isModalOpen, setIsModalOpen] = React.useState(false);
889
+ const { unlockedAchievements, allAchievements, unlockedCount, totalCount } = useAchievementState();
890
+ const resolvedTheme = theme || uiContext.ui.theme || 'modern';
891
+ const themeConfig = getTheme(resolvedTheme) || builtInThemes.modern;
892
+ const modalAchievements = showAllAchievements ? allAchievements : unlockedAchievements;
893
+ const openModal = () => setIsModalOpen(true);
894
+ const buttonBaseStyles = placement === 'inline'
895
+ ? 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);
896
+ const buttonProps = {
897
+ type: 'button',
898
+ onClick: openModal,
899
+ style: buttonBaseStyles,
900
+ className: triggerClassName,
901
+ 'data-placement': placement,
902
+ 'data-testid': 'achievements-widget-button',
903
+ 'aria-label': `${label}: ${unlockedCount} of ${totalCount} achievements unlocked`,
904
+ };
905
+ return (React.createElement(React.Fragment, null,
906
+ renderTrigger ? (renderTrigger({
907
+ open: openModal,
908
+ label,
909
+ unlockedCount,
910
+ totalCount,
911
+ unlockedAchievements,
912
+ allAchievements,
913
+ buttonProps,
914
+ })) : (React.createElement("button", Object.assign({}, buttonProps, { onMouseEnter: (event) => {
915
+ if (placement !== 'inline') {
916
+ event.currentTarget.style.transform = 'scale(1.05)';
917
+ }
918
+ }, onMouseLeave: (event) => {
919
+ if (placement !== 'inline') {
920
+ event.currentTarget.style.transform = 'scale(1)';
921
+ }
922
+ } }),
923
+ React.createElement("span", null, icon),
924
+ React.createElement("span", { style: { flex: 1 } }, label),
925
+ showCount && React.createElement("span", null, unlockedCount))),
926
+ React.createElement(AchievementsModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: modalAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles, title: modalTitle, emptyState: emptyState, renderAchievement: renderAchievement, theme: resolvedTheme })));
927
+ };
928
+
929
+ const getPositionStyles = (position) => {
930
+ const base = {
931
+ position: 'fixed',
932
+ margin: '20px',
933
+ zIndex: 1000,
934
+ };
935
+ switch (position) {
936
+ case 'top-left':
937
+ return Object.assign(Object.assign({}, base), { top: 0, left: 0 });
938
+ case 'top-right':
939
+ return Object.assign(Object.assign({}, base), { top: 0, right: 0 });
940
+ case 'bottom-left':
941
+ return Object.assign(Object.assign({}, base), { bottom: 0, left: 0 });
942
+ case 'bottom-right':
943
+ return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
944
+ }
945
+ };
946
+ /**
947
+ * @deprecated Use `AchievementsWidget` for new integrations. This v3
948
+ * compatibility wrapper will be removed in 4.2.
949
+ */
950
+ const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
951
+ React.useEffect(() => {
952
+ warnDeprecation('`BadgesButton` is deprecated. Use `AchievementsWidget` instead. `BadgesButton` will be removed in 4.2.');
953
+ }, []);
954
+ // Get theme configuration for consistent styling
955
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
956
+ const accentColor = themeConfig.notification.accentColor;
957
+ // Different styling for fixed vs inline placement
958
+ const baseStyles = placement === 'inline'
959
+ ? Object.assign({
960
+ // Inline mode: looks like a navigation item
961
+ 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({
962
+ // Fixed mode: floating button
963
+ 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);
964
+ return (React.createElement("button", { onClick: onClick, style: baseStyles, onMouseEnter: (e) => {
965
+ if (placement === 'inline') {
966
+ // Inline mode: subtle background color change
967
+ e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
968
+ }
969
+ else {
970
+ // Fixed mode: scale transformation
971
+ e.target.style.transform = 'scale(1.05)';
972
+ }
973
+ }, onMouseLeave: (e) => {
974
+ if (placement === 'inline') {
975
+ e.target.style.backgroundColor = 'transparent';
976
+ }
977
+ else {
978
+ e.target.style.transform = 'scale(1)';
611
979
  }
612
- }
613
- `),
614
- React.createElement("div", { style: containerStyles, "data-testid": "built-in-confetti" }, particles)));
980
+ }, "data-placement": placement, "data-testid": "badges-button" },
981
+ "\uD83C\uDFC6 Achievements (",
982
+ unlockedAchievements.length,
983
+ ")"));
615
984
  };
616
985
 
617
- /******************************************************************************
618
- Copyright (c) Microsoft Corporation.
619
-
620
- Permission to use, copy, modify, and/or distribute this software for any
621
- purpose with or without fee is hereby granted.
622
-
623
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
624
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
625
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
626
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
627
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
628
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
629
- PERFORMANCE OF THIS SOFTWARE.
630
- ***************************************************************************** */
631
- /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
632
-
633
-
634
- function __awaiter(thisArg, _arguments, P, generator) {
635
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
636
- return new (P || (P = Promise))(function (resolve, reject) {
637
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
638
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
639
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
640
- step((generator = generator.apply(thisArg, _arguments || [])).next());
641
- });
642
- }
643
-
644
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
645
- var e = new Error(message);
646
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
986
+ /**
987
+ * @deprecated Use `AchievementsModal`, `AchievementsWidget`, or
988
+ * `AchievementsList` for new integrations. This v3 compatibility wrapper will
989
+ * be removed in 4.2.
990
+ */
991
+ const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, showAllAchievements = false, showUnlockConditions = false, allAchievements, }) => {
992
+ React.useEffect(() => {
993
+ warnDeprecation('`BadgesModal` is deprecated. Use `AchievementsWidget` or `AchievementsList` instead. `BadgesModal` will be removed in 4.2.');
994
+ }, []);
995
+ const achievementsToDisplay = showAllAchievements && allAchievements
996
+ ? allAchievements
997
+ : achievements.map((achievement) => (Object.assign(Object.assign({}, achievement), { isUnlocked: true })));
998
+ return (React.createElement(AchievementsModal, { isOpen: isOpen, onClose: onClose, achievements: achievementsToDisplay, styles: styles, icons: icons, showUnlockConditions: showUnlockConditions }));
647
999
  };
648
1000
 
649
1001
  /**
650
- * Legacy UI library detection system
651
- * Attempts to dynamically import external UI libraries
652
- * Shows deprecation warnings when detected
1002
+ * @deprecated Use `AchievementsWidget` for new integrations. This v3
1003
+ * compatibility wrapper will be removed in 4.2.
653
1004
  */
654
- let cachedLibraries = null;
655
- let detectionAttempted = false;
656
- let deprecationWarningShown = false;
1005
+ const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right', placement = 'fixed', showAllAchievements = false, allAchievements, showUnlockConditions = false, icons, theme = 'modern', buttonStyles, modalStyles, }) => {
1006
+ const [isModalOpen, setIsModalOpen] = React.useState(false);
1007
+ return (React.createElement(React.Fragment, null,
1008
+ React.createElement(BadgesButton, { onClick: () => setIsModalOpen(true), unlockedAchievements: unlockedAchievements, position: position, placement: placement, theme: theme, styles: buttonStyles }),
1009
+ React.createElement(BadgesModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: unlockedAchievements, showAllAchievements: showAllAchievements, allAchievements: allAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles })));
1010
+ };
1011
+
657
1012
  /**
658
- * Attempts to dynamically import legacy UI libraries
659
- * Uses try/catch to gracefully handle missing dependencies
660
- * Caches result to avoid multiple import attempts
661
- *
662
- * @returns Promise resolving to LegacyLibraries object
1013
+ * @deprecated Use the provider `ui.ConfettiComponent` option or the built-in
1014
+ * confetti default. This v3 compatibility wrapper will be removed in 4.2.
663
1015
  */
664
- function detectLegacyLibraries() {
665
- return __awaiter(this, void 0, void 0, function* () {
666
- if (detectionAttempted && cachedLibraries !== null) {
667
- return cachedLibraries;
668
- }
669
- detectionAttempted = true;
670
- const libraries = {};
671
- // Try to import react-toastify
672
- try {
673
- const toastifyModule = yield import('react-toastify');
674
- libraries.toast = toastifyModule.toast;
675
- libraries.ToastContainer = toastifyModule.ToastContainer;
676
- }
677
- catch (_a) {
678
- // Not installed, will use built-in notification
679
- }
680
- // Try to import react-modal
681
- try {
682
- const modalModule = yield import('react-modal');
683
- libraries.Modal = modalModule.default;
684
- }
685
- catch (_b) {
686
- // Not installed, will use built-in modal
687
- }
688
- // Try to import react-confetti
689
- try {
690
- const confettiModule = yield import('react-confetti');
691
- libraries.Confetti = confettiModule.default;
692
- }
693
- catch (_c) {
694
- // Not installed, will use built-in confetti
695
- }
696
- // Try to import react-use (only for useWindowSize)
697
- try {
698
- const reactUseModule = yield import('react-use');
699
- libraries.useWindowSize = reactUseModule.useWindowSize;
700
- }
701
- catch (_d) {
702
- // Not installed, will use built-in useWindowSize
703
- }
704
- cachedLibraries = libraries;
705
- // Show deprecation warning if ANY legacy library is found
706
- if (!deprecationWarningShown && Object.keys(libraries).length > 0) {
707
- const foundLibraries = [];
708
- if (libraries.toast)
709
- foundLibraries.push('react-toastify');
710
- if (libraries.Modal)
711
- foundLibraries.push('react-modal');
712
- if (libraries.Confetti)
713
- foundLibraries.push('react-confetti');
714
- if (libraries.useWindowSize)
715
- foundLibraries.push('react-use');
716
- console.warn(`[react-achievements] DEPRECATION WARNING: External UI dependencies (${foundLibraries.join(', ')}) are deprecated and will become fully optional in v4.0.0.\n\n` +
717
- `The library now includes built-in UI components with modern design and theme support.\n\n` +
718
- `To migrate:\n` +
719
- `1. Add "useBuiltInUI={true}" to your AchievementProvider\n` +
720
- `2. Test your application (UI will change to modern theme)\n` +
721
- `3. Optionally customize with theme="minimal" or theme="gamified"\n` +
722
- `4. Remove external dependencies from package.json\n\n` +
723
- `To silence this warning, set useBuiltInUI={true} in AchievementProvider.\n\n` +
724
- `Learn more: https://github.com/dave-b-b/react-achievements#migration-guide`);
725
- deprecationWarningShown = true;
726
- }
727
- return libraries;
728
- });
729
- }
1016
+ const ConfettiWrapper = ({ show }) => {
1017
+ React.useEffect(() => {
1018
+ warnDeprecation('`ConfettiWrapper` is deprecated. Use the provider `ui.ConfettiComponent` option or built-in confetti defaults instead. `ConfettiWrapper` will be removed in 4.2.');
1019
+ }, []);
1020
+ return React.createElement(BuiltInConfetti, { show: show, particleCount: 200 });
1021
+ };
1022
+
1023
+ const clamp = (value, min, max) => {
1024
+ return Math.min(Math.max(value, min), max);
1025
+ };
1026
+ const LevelProgress = ({ level, currentXP, nextLevelXP, label = 'Level', valueLabel, showValues = true, showPercent = true, theme = 'modern', styles = {}, className, }) => {
1027
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
1028
+ const safeMax = nextLevelXP > 0 ? nextLevelXP : 1;
1029
+ const clampedCurrent = clamp(currentXP, 0, safeMax);
1030
+ const progress = Math.round((clampedCurrent / safeMax) * 100);
1031
+ const computedValueLabel = valueLabel !== null && valueLabel !== void 0 ? valueLabel : `${clampedCurrent} / ${safeMax} XP`;
1032
+ 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);
1033
+ 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);
1034
+ return (React.createElement("div", { className: className, style: containerStyles, "data-testid": "level-progress" },
1035
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.header), styles === null || styles === void 0 ? void 0 : styles.header) },
1036
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.levelLabel), styles === null || styles === void 0 ? void 0 : styles.levelLabel) },
1037
+ label,
1038
+ " ",
1039
+ level),
1040
+ showValues && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.valueText), styles === null || styles === void 0 ? void 0 : styles.valueText) }, computedValueLabel))),
1041
+ 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" },
1042
+ React.createElement("div", { style: progressBarStyles })),
1043
+ showPercent && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.levelProgress.progressText), styles === null || styles === void 0 ? void 0 : styles.progressText) },
1044
+ progress,
1045
+ "%"))));
1046
+ };
1047
+
1048
+ /**
1049
+ * A simplified hook for achievement tracking.
1050
+ * Provides the v4 happy path for direct metric updates plus explicit state names.
1051
+ */
1052
+ const useSimpleAchievements = () => {
1053
+ const { update, reset, getState, exportData, importData } = useAchievements();
1054
+ const achievementState = useAchievementState();
1055
+ const track = (metric, value) => update({ [metric]: value });
1056
+ const increment = (metric, amount = 1) => {
1057
+ const currentState = getState();
1058
+ const currentMetricArray = currentState.metrics[metric] || [0];
1059
+ const currentValue = Array.isArray(currentMetricArray)
1060
+ ? currentMetricArray[0]
1061
+ : currentMetricArray;
1062
+ const newValue = (typeof currentValue === 'number' ? currentValue : 0) + amount;
1063
+ update({ [metric]: newValue });
1064
+ };
1065
+ const trackMultiple = (metrics) => update(metrics);
1066
+ return {
1067
+ track,
1068
+ increment,
1069
+ trackMultiple,
1070
+ unlockedIds: achievementState.unlockedIds,
1071
+ unlockedAchievements: achievementState.unlockedAchievements,
1072
+ allAchievements: achievementState.allAchievements,
1073
+ unlockedCount: achievementState.unlockedCount,
1074
+ totalCount: achievementState.totalCount,
1075
+ metrics: achievementState.metrics,
1076
+ reset,
1077
+ getState,
1078
+ exportData,
1079
+ importData,
1080
+ getAllAchievements: () => achievementState.allAchievements,
1081
+ /**
1082
+ * @deprecated Use `unlockedIds` instead. This alias will be removed in 4.2.
1083
+ */
1084
+ unlocked: achievementState.unlockedIds,
1085
+ /**
1086
+ * @deprecated Use `allAchievements` instead. This alias will be removed in 4.2.
1087
+ */
1088
+ all: achievementState.allAchievements,
1089
+ };
1090
+ };
730
1091
 
731
1092
  /**
732
1093
  * Built-in modal component
@@ -942,415 +1303,6 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
942
1303
  })))))));
943
1304
  };
944
1305
 
945
- /**
946
- * Legacy library wrappers for backwards compatibility
947
- * Wraps external UI libraries to match our component interfaces
948
- */
949
- /**
950
- * Wrapper for react-toastify toast notifications
951
- * Falls back to built-in notification if not available
952
- */
953
- const createLegacyToastNotification = (libraries) => {
954
- return ({ achievement, onClose, icons = {} }) => {
955
- const { toast } = libraries;
956
- // Merge custom icons with defaults from react-achievements
957
- const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
958
- const icon = (achievement.achievementIconKey &&
959
- mergedIcons[achievement.achievementIconKey]) ||
960
- achievement.achievementIconKey ||
961
- mergedIcons.default ||
962
- '⭐';
963
- React.useEffect(() => {
964
- if (!toast)
965
- return;
966
- // Call toast.success with achievement content
967
- toast.success(React.createElement("div", { style: { display: 'flex', alignItems: 'center' } },
968
- React.createElement("span", { style: { fontSize: '2em', marginRight: '10px' } }, icon),
969
- React.createElement("div", null,
970
- React.createElement("div", { style: { fontSize: '12px', opacity: 0.8, marginBottom: '4px' } }, "Achievement Unlocked!"),
971
- React.createElement("div", { style: { fontWeight: 'bold', marginBottom: '4px' } }, achievement.achievementTitle),
972
- achievement.achievementDescription && (React.createElement("div", { style: { fontSize: '13px', opacity: 0.9 } }, achievement.achievementDescription)))), {
973
- position: 'top-right',
974
- autoClose: 5000,
975
- hideProgressBar: false,
976
- closeOnClick: true,
977
- pauseOnHover: true,
978
- draggable: true,
979
- toastId: achievement.achievementId,
980
- onClose,
981
- });
982
- }, [achievement, toast, onClose, icon]);
983
- return null; // Toast handles its own rendering
984
- };
985
- };
986
- /**
987
- * Wrapper for react-confetti Confetti component
988
- * Falls back to built-in confetti if not available
989
- */
990
- const createLegacyConfettiWrapper = (libraries) => {
991
- return ({ show, duration = 5000, particleCount = 200, colors }) => {
992
- const { Confetti, useWindowSize: legacyUseWindowSize } = libraries;
993
- // If Confetti not available, use built-in
994
- if (!Confetti) {
995
- return (React.createElement(BuiltInConfetti, { show: show, duration: duration, particleCount: particleCount, colors: colors }));
996
- }
997
- // Use react-confetti with react-use's useWindowSize if available
998
- // Otherwise fall back to default dimensions
999
- let width = 0;
1000
- let height = 0;
1001
- if (legacyUseWindowSize) {
1002
- const size = legacyUseWindowSize();
1003
- width = size.width;
1004
- height = size.height;
1005
- }
1006
- else if (typeof window !== 'undefined') {
1007
- width = window.innerWidth;
1008
- height = window.innerHeight;
1009
- }
1010
- if (!show)
1011
- return null;
1012
- return (React.createElement(Confetti, { width: width, height: height, numberOfPieces: particleCount, recycle: false, colors: colors, style: {
1013
- position: 'fixed',
1014
- top: 0,
1015
- left: 0,
1016
- zIndex: 10001,
1017
- pointerEvents: 'none',
1018
- } }));
1019
- };
1020
- };
1021
-
1022
- const AchievementContext = React.createContext(undefined);
1023
- const AchievementProvider = ({ achievements: achievementsConfig, storage = 'local', children, icons = {}, onError, restApiConfig, ui = {}, useBuiltInUI = false, engine: externalEngine, eventMapping, }) => {
1024
- // VALIDATION: Prevent mixing patterns
1025
- if (achievementsConfig && externalEngine) {
1026
- throw new Error('Cannot provide both "achievements" and "engine" props to AchievementProvider.\n\n' +
1027
- 'Choose one pattern:\n' +
1028
- '1. OLD metric-based: <AchievementProvider achievements={config}>\n' +
1029
- '2. NEW event-based: <AchievementProvider engine={myEngine}>');
1030
- }
1031
- // Track which pattern is being used
1032
- const isLegacyPattern = Boolean(achievementsConfig);
1033
- // Engine instance (either external or auto-created)
1034
- // Initialize synchronously BEFORE first render to avoid timing issues
1035
- const [engine] = React.useState(() => {
1036
- if (externalEngine) {
1037
- // NEW PATTERN: Use external injected engine
1038
- return externalEngine;
1039
- }
1040
- // OLD PATTERN: Auto-create engine from achievements
1041
- if (!achievementsConfig) {
1042
- throw new Error('AchievementProvider requires either "achievements" or "engine" prop.\n\n' +
1043
- '1. OLD pattern: <AchievementProvider achievements={config}>\n' +
1044
- '2. NEW pattern: <AchievementProvider engine={myEngine}>');
1045
- }
1046
- return new achievementsEngine.AchievementEngine({
1047
- achievements: achievementsConfig,
1048
- storage: storage, // Type cast needed for compatibility
1049
- restApiConfig,
1050
- onError: onError,
1051
- eventMapping,
1052
- });
1053
- });
1054
- // React state synced from engine
1055
- // Initialize with current engine state (engine is available immediately)
1056
- const [achievementState, setAchievementState] = React.useState(() => {
1057
- const unlocked = engine.getUnlocked();
1058
- return {
1059
- unlocked: [...unlocked],
1060
- all: {}, // Will be populated by getAllAchievements
1061
- };
1062
- });
1063
- // Track which achievements have been seen to avoid duplicate notifications
1064
- const seenAchievementsRef = React.useRef(new Set(engine.getUnlocked()));
1065
- const [showConfetti, setShowConfetti] = React.useState(false);
1066
- // NEW: UI component resolution state (v3.6.0)
1067
- const [legacyLibraries, setLegacyLibraries] = React.useState(null);
1068
- const [uiReady, setUiReady] = React.useState(useBuiltInUI);
1069
- const [currentNotification, setCurrentNotification] = React.useState(null);
1070
- // Cleanup: Destroy engine on unmount (only if we auto-created it)
1071
- React.useEffect(() => {
1072
- return () => {
1073
- // Only destroy if we auto-created the engine
1074
- if (!externalEngine) {
1075
- engine.destroy();
1076
- }
1077
- };
1078
- }, [engine, externalEngine]);
1079
- // Subscribe to engine events
1080
- React.useEffect(() => {
1081
- // Engine is always available (initialized synchronously)
1082
- // Handle achievement unlocked
1083
- const unsubscribeUnlocked = engine.on('achievement:unlocked', (event) => {
1084
- // Update unlocked list
1085
- setAchievementState(prev => (Object.assign(Object.assign({}, prev), { unlocked: [...engine.getUnlocked()] })));
1086
- // Show notification if not seen before
1087
- if (!seenAchievementsRef.current.has(event.achievementId)) {
1088
- seenAchievementsRef.current.add(event.achievementId);
1089
- if (ui.enableNotifications !== false) {
1090
- // Create a full AchievementWithStatus object for the notification
1091
- const unlockedAchievement = {
1092
- achievementId: event.achievementId,
1093
- achievementTitle: event.achievementTitle,
1094
- achievementDescription: event.achievementDescription,
1095
- achievementIconKey: event.achievementIconKey,
1096
- isUnlocked: true,
1097
- };
1098
- setCurrentNotification(unlockedAchievement);
1099
- // Show confetti
1100
- setShowConfetti(true);
1101
- // Hide confetti after 5 seconds
1102
- setTimeout(() => {
1103
- setShowConfetti(false);
1104
- }, 5000);
1105
- }
1106
- }
1107
- });
1108
- // Handle state changes (for syncing unlocked list)
1109
- const unsubscribeStateChanged = engine.on('state:changed', () => {
1110
- setAchievementState(prev => (Object.assign(Object.assign({}, prev), { unlocked: [...engine.getUnlocked()] })));
1111
- });
1112
- return () => {
1113
- unsubscribeUnlocked();
1114
- unsubscribeStateChanged();
1115
- };
1116
- }, [engine, icons, ui.enableNotifications]);
1117
- // Detect legacy UI libraries on mount
1118
- React.useEffect(() => {
1119
- if (useBuiltInUI) {
1120
- setUiReady(true);
1121
- return;
1122
- }
1123
- detectLegacyLibraries().then((libs) => {
1124
- setLegacyLibraries(libs);
1125
- setUiReady(true);
1126
- });
1127
- }, [useBuiltInUI]);
1128
- // Resolve UI components based on detection and config
1129
- const NotificationComponent = ui.NotificationComponent ||
1130
- (useBuiltInUI ? BuiltInNotification :
1131
- legacyLibraries && Object.keys(legacyLibraries).length > 0 && legacyLibraries.toast
1132
- ? createLegacyToastNotification(legacyLibraries)
1133
- : BuiltInNotification);
1134
- const ConfettiComponentResolved = ui.ConfettiComponent ||
1135
- (useBuiltInUI ? BuiltInConfetti :
1136
- legacyLibraries && Object.keys(legacyLibraries).length > 0 && legacyLibraries.Confetti
1137
- ? createLegacyConfettiWrapper(legacyLibraries)
1138
- : BuiltInConfetti);
1139
- // Context methods - delegate to engine
1140
- const update = (newMetrics) => {
1141
- engine.update(newMetrics);
1142
- };
1143
- const reset = () => {
1144
- engine.reset();
1145
- seenAchievementsRef.current.clear();
1146
- setShowConfetti(false);
1147
- setCurrentNotification(null);
1148
- };
1149
- const getState = () => {
1150
- const metrics = engine.getMetrics();
1151
- const unlocked = engine.getUnlocked();
1152
- // Convert metrics to array format for backward compatibility
1153
- const metricsInArrayFormat = {};
1154
- Object.entries(metrics).forEach(([key, value]) => {
1155
- metricsInArrayFormat[key] = Array.isArray(value) ? value : [value];
1156
- });
1157
- return {
1158
- metrics: metricsInArrayFormat,
1159
- unlocked: [...unlocked],
1160
- };
1161
- };
1162
- const exportData = () => {
1163
- return engine.export();
1164
- };
1165
- const importData = (jsonString, options) => {
1166
- var _a;
1167
- const result = engine.import(jsonString, options);
1168
- // Update seen achievements to prevent duplicate notifications
1169
- if ((result === null || result === void 0 ? void 0 : result.success) && 'mergedUnlocked' in result) {
1170
- (_a = result.mergedUnlocked) === null || _a === void 0 ? void 0 : _a.forEach((id) => {
1171
- seenAchievementsRef.current.add(id);
1172
- });
1173
- }
1174
- return result;
1175
- };
1176
- const getAllAchievements = () => {
1177
- return engine.getAllAchievements();
1178
- };
1179
- return (React.createElement(AchievementContext.Provider, { value: {
1180
- update,
1181
- achievements: achievementState,
1182
- reset,
1183
- getState,
1184
- exportData,
1185
- importData,
1186
- getAllAchievements,
1187
- engine, // Always defined - no undefined fallback needed
1188
- _isLegacyPattern: isLegacyPattern, // NEW: Expose pattern flag to hooks
1189
- } },
1190
- children,
1191
- 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 })),
1192
- uiReady && ui.enableConfetti !== false && (React.createElement(ConfettiComponentResolved, { show: showConfetti, duration: 5000 }))));
1193
- };
1194
-
1195
- const useAchievements = () => {
1196
- const context = React.useContext(AchievementContext);
1197
- if (!context) {
1198
- throw new Error('useAchievements must be used within an AchievementProvider');
1199
- }
1200
- // STRICT CHECK: Detect if Provider has injected engine (new pattern)
1201
- if (!context._isLegacyPattern) {
1202
- throw new Error('Cannot use useAchievements when AchievementProvider has injected engine.\n\n' +
1203
- 'You are using the NEW event-based pattern.\n' +
1204
- 'useAchievements is for the OLD metric-based pattern only.\n\n' +
1205
- 'Use engine.emit() instead:\n' +
1206
- ' const engine = useAchievementEngine();\n' +
1207
- ' engine.emit("eventName", data);\n\n' +
1208
- 'Or switch to the old pattern:\n' +
1209
- ' <AchievementProvider achievements={config}>\n' +
1210
- ' const { track } = useAchievements();\n' +
1211
- ' track("score", 100);');
1212
- }
1213
- return context;
1214
- };
1215
-
1216
- /**
1217
- * A simplified hook for achievement tracking.
1218
- * Provides an easier API for common use cases while maintaining access to advanced features.
1219
- */
1220
- const useSimpleAchievements = () => {
1221
- const { update, achievements, reset, getState, exportData, importData, getAllAchievements } = useAchievements();
1222
- return {
1223
- /**
1224
- * Track a metric value for achievements
1225
- * @param metric - The metric name (e.g., 'score', 'level')
1226
- * @param value - The metric value
1227
- */
1228
- track: (metric, value) => update({ [metric]: value }),
1229
- /**
1230
- * Increment a numeric metric by a specified amount
1231
- * @param metric - The metric name (e.g., 'buttonClicks', 'score')
1232
- * @param amount - The amount to increment by (defaults to 1)
1233
- */
1234
- increment: (metric, amount = 1) => {
1235
- const currentState = getState();
1236
- const currentMetricArray = currentState.metrics[metric] || [0];
1237
- const currentValue = Array.isArray(currentMetricArray) ? currentMetricArray[0] : currentMetricArray;
1238
- const newValue = (typeof currentValue === 'number' ? currentValue : 0) + amount;
1239
- update({ [metric]: newValue });
1240
- },
1241
- /**
1242
- * Track multiple metrics at once
1243
- * @param metrics - Object with metric names as keys and values
1244
- */
1245
- trackMultiple: (metrics) => update(metrics),
1246
- /**
1247
- * Array of unlocked achievement IDs
1248
- */
1249
- unlocked: achievements.unlocked,
1250
- /**
1251
- * All available achievements
1252
- */
1253
- all: achievements.all,
1254
- /**
1255
- * Number of unlocked achievements
1256
- */
1257
- unlockedCount: achievements.unlocked.length,
1258
- /**
1259
- * Reset all achievement progress
1260
- */
1261
- reset,
1262
- /**
1263
- * Get current state (advanced usage)
1264
- */
1265
- getState,
1266
- /**
1267
- * Export achievement data to JSON string
1268
- * @returns JSON string containing all achievement data
1269
- */
1270
- exportData,
1271
- /**
1272
- * Import achievement data from JSON string
1273
- * @param jsonString - JSON string containing exported achievement data
1274
- * @param options - Import options (merge strategy, validation)
1275
- * @returns Import result with success status and any errors
1276
- */
1277
- importData,
1278
- /**
1279
- * Get all achievements with their unlock status
1280
- * @returns Array of achievements with isUnlocked boolean property
1281
- */
1282
- getAllAchievements,
1283
- };
1284
- };
1285
-
1286
- /**
1287
- * Hook to access the injected AchievementEngine instance (NEW event-based pattern)
1288
- *
1289
- * IMPORTANT: This hook only works when Provider has an externally created engine injected via the `engine` prop.
1290
- * Do NOT use this hook with the old `achievements` prop pattern - use `useAchievements()` instead.
1291
- *
1292
- * @example
1293
- * ```tsx
1294
- * // Create engine outside React
1295
- * import { AchievementEngine } from 'achievements-engine';
1296
- *
1297
- * const myEngine = new AchievementEngine({
1298
- * achievements: config,
1299
- * eventMapping: { 'userScored': 'score', 'levelUp': 'level' },
1300
- * storage: 'local'
1301
- * });
1302
- *
1303
- * // Inject into Provider
1304
- * <AchievementProvider engine={myEngine}>
1305
- * <App />
1306
- * </AchievementProvider>
1307
- *
1308
- * function App() {
1309
- * const engine = useAchievementEngine();
1310
- * engine.emit('userScored', 100);
1311
- * engine.emit('levelUp', 5);
1312
- * }
1313
- * ```
1314
- *
1315
- * @returns AchievementEngine instance
1316
- * @throws Error if used with old achievements prop pattern
1317
- * @throws Error if used outside AchievementProvider
1318
- * @since 3.8.0
1319
- */
1320
- const useAchievementEngine = () => {
1321
- const context = React.useContext(AchievementContext);
1322
- if (!context) {
1323
- throw new Error('useAchievementEngine must be used within an AchievementProvider.\n\n' +
1324
- 'Wrap your component tree:\n' +
1325
- 'const myEngine = new AchievementEngine({ achievements, eventMapping });\n' +
1326
- '<AchievementProvider engine={myEngine}>\n' +
1327
- ' <YourComponent />\n' +
1328
- '</AchievementProvider>');
1329
- }
1330
- // STRICT CHECK: Detect if Provider was initialized with achievements (old pattern)
1331
- if (context._isLegacyPattern) {
1332
- throw new Error('Cannot use useAchievementEngine when AchievementProvider has achievements prop.\n\n' +
1333
- 'You are using the OLD metric-based pattern.\n' +
1334
- 'useAchievementEngine is for the NEW event-based pattern only.\n\n' +
1335
- 'Choose one:\n\n' +
1336
- '1. OLD PATTERN (keep current code):\n' +
1337
- ' <AchievementProvider achievements={config}>\n' +
1338
- ' const { track } = useAchievements();\n' +
1339
- ' track("score", 100);\n\n' +
1340
- '2. NEW PATTERN (migrate to events):\n' +
1341
- ' const engine = new AchievementEngine({ achievements, eventMapping });\n' +
1342
- ' <AchievementProvider engine={engine}>\n' +
1343
- ' const engine = useAchievementEngine();\n' +
1344
- ' engine.emit("userScored", 100);');
1345
- }
1346
- if (!context.engine) {
1347
- throw new Error('No engine available. AchievementProvider requires either:\n' +
1348
- '1. achievements prop (old pattern)\n' +
1349
- '2. engine prop (new pattern)');
1350
- }
1351
- return context.engine;
1352
- };
1353
-
1354
1306
  Object.defineProperty(exports, 'AchievementBuilder', {
1355
1307
  enumerable: true,
1356
1308
  get: function () { return achievementsEngine.AchievementBuilder; }
@@ -1441,6 +1393,9 @@ Object.defineProperty(exports, 'normalizeAchievements', {
1441
1393
  });
1442
1394
  exports.AchievementContext = AchievementContext;
1443
1395
  exports.AchievementProvider = AchievementProvider;
1396
+ exports.AchievementsList = AchievementsList;
1397
+ exports.AchievementsModal = AchievementsModal;
1398
+ exports.AchievementsWidget = AchievementsWidget;
1444
1399
  exports.BadgesButton = BadgesButton;
1445
1400
  exports.BadgesButtonWithModal = BadgesButtonWithModal;
1446
1401
  exports.BadgesModal = BadgesModal;
@@ -1448,10 +1403,13 @@ exports.BuiltInConfetti = BuiltInConfetti;
1448
1403
  exports.BuiltInModal = BuiltInModal;
1449
1404
  exports.BuiltInNotification = BuiltInNotification;
1450
1405
  exports.ConfettiWrapper = ConfettiWrapper;
1406
+ exports.HeadlessAchievementProvider = AchievementProvider$1;
1407
+ exports.LevelProgress = LevelProgress;
1451
1408
  exports.defaultAchievementIcons = defaultAchievementIcons;
1452
1409
  exports.defaultStyles = defaultStyles;
1453
1410
  exports.isAsyncStorage = isAsyncStorage;
1454
1411
  exports.useAchievementEngine = useAchievementEngine;
1412
+ exports.useAchievementState = useAchievementState;
1455
1413
  exports.useAchievements = useAchievements;
1456
1414
  exports.useSimpleAchievements = useSimpleAchievements;
1457
1415
  exports.useWindowSize = useWindowSize;