react-achievements 4.1.1 → 4.3.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.
package/dist/index.esm.js CHANGED
@@ -1,6 +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, { createContext, useState, useCallback, useEffect, useContext, useMemo, useRef } from 'react';
3
+ import React, { createContext, useState, useCallback, useEffect, useContext, useRef, useMemo } from 'react';
4
4
 
5
5
  // Type guard to detect async storage
6
6
  function isAsyncStorage(storage) {
@@ -63,15 +63,12 @@ function warnDeprecation(message) {
63
63
  }
64
64
 
65
65
  const AchievementContext = createContext(undefined);
66
- const getAllAchievementRecord = (engine) => {
67
- return Object.fromEntries(engine.getAllAchievements().map((achievement) => [
68
- achievement.achievementId,
69
- achievement,
70
- ]));
66
+ const getAllAchievementRecord = (achievements) => {
67
+ return Object.fromEntries(achievements.map((achievement) => [achievement.achievementId, achievement]));
71
68
  };
72
69
  const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'local', children, onError, useBuiltInUI, restApiConfig, engine: externalEngine, eventMapping, icons = {}, }) => {
73
70
  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.');
71
+ warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 5.0.');
75
72
  }
76
73
  if (achievementsConfig && externalEngine) {
77
74
  throw new Error('Cannot provide both "achievements" and "engine" props to AchievementProvider.\n\n' +
@@ -97,15 +94,9 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
97
94
  eventMapping,
98
95
  });
99
96
  });
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
- });
97
+ const [achievementSnapshot, setAchievementSnapshot] = useState(() => engine.getSnapshot());
98
+ const syncAchievementState = useCallback((snapshot) => {
99
+ setAchievementSnapshot(snapshot || engine.getSnapshot());
109
100
  }, [engine]);
110
101
  useEffect(() => {
111
102
  return () => {
@@ -115,10 +106,17 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
115
106
  };
116
107
  }, [engine, externalEngine]);
117
108
  useEffect(() => {
118
- const unsubscribeUnlocked = engine.on('achievement:unlocked', syncAchievementState);
119
- const unsubscribeStateChanged = engine.on('state:changed', syncAchievementState);
109
+ let isMounted = true;
110
+ const unsubscribeStateChanged = engine.on('state:changed', (event) => {
111
+ syncAchievementState(event);
112
+ });
113
+ engine.ready().then(() => {
114
+ if (isMounted) {
115
+ syncAchievementState();
116
+ }
117
+ });
120
118
  return () => {
121
- unsubscribeUnlocked();
119
+ isMounted = false;
122
120
  unsubscribeStateChanged();
123
121
  };
124
122
  }, [engine, syncAchievementState]);
@@ -127,18 +125,12 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
127
125
  };
128
126
  const reset = () => {
129
127
  engine.reset();
130
- syncAchievementState();
131
128
  };
132
129
  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
- });
130
+ const snapshot = engine.getSnapshot();
139
131
  return {
140
- metrics: metricsInArrayFormat,
141
- unlocked: [...unlocked],
132
+ metrics: snapshot.metrics,
133
+ unlocked: snapshot.unlockedIds,
142
134
  };
143
135
  };
144
136
  const exportData = () => {
@@ -150,11 +142,16 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
150
142
  return result;
151
143
  };
152
144
  const getAllAchievements = () => {
153
- return engine.getAllAchievements();
145
+ return engine.getSnapshot().allAchievements;
146
+ };
147
+ const achievements = {
148
+ unlocked: achievementSnapshot.unlockedIds,
149
+ all: getAllAchievementRecord(achievementSnapshot.allAchievements),
154
150
  };
155
151
  return (React.createElement(AchievementContext.Provider, { value: {
156
152
  update,
157
- achievements: achievementState,
153
+ achievements,
154
+ snapshot: achievementSnapshot,
158
155
  reset,
159
156
  getState,
160
157
  exportData,
@@ -319,24 +316,36 @@ const BuiltInNotification = ({ achievement, onClose, duration = 5000, position =
319
316
  var _a, _b, _c;
320
317
  const [isVisible, setIsVisible] = useState(false);
321
318
  const [isExiting, setIsExiting] = useState(false);
319
+ const onCloseRef = useRef(onClose);
320
+ const exitTimerRef = useRef(null);
322
321
  // Merge custom icons with defaults
323
322
  const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
324
323
  // Get theme configuration
325
324
  const themeConfig = getTheme(theme) || builtInThemes.modern;
326
325
  const { notification: themeStyles } = themeConfig;
326
+ useEffect(() => {
327
+ onCloseRef.current = onClose;
328
+ }, [onClose]);
329
+ const closeAfterExit = useCallback(() => {
330
+ setIsExiting(true);
331
+ if (exitTimerRef.current) {
332
+ clearTimeout(exitTimerRef.current);
333
+ }
334
+ exitTimerRef.current = setTimeout(() => { var _a; return (_a = onCloseRef.current) === null || _a === void 0 ? void 0 : _a.call(onCloseRef); }, 300);
335
+ }, []);
327
336
  useEffect(() => {
328
337
  // Slide in animation
329
338
  const showTimer = setTimeout(() => setIsVisible(true), 10);
330
339
  // Auto-dismiss
331
- const dismissTimer = setTimeout(() => {
332
- setIsExiting(true);
333
- setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
334
- }, duration);
340
+ const dismissTimer = setTimeout(closeAfterExit, duration);
335
341
  return () => {
336
342
  clearTimeout(showTimer);
337
343
  clearTimeout(dismissTimer);
344
+ if (exitTimerRef.current) {
345
+ clearTimeout(exitTimerRef.current);
346
+ }
338
347
  };
339
- }, [duration, onClose]);
348
+ }, [duration, closeAfterExit]);
340
349
  const getPositionStyles = () => {
341
350
  const stackedOffset = 20 + stackIndex * 104;
342
351
  const base = {
@@ -424,10 +433,7 @@ const BuiltInNotification = ({ achievement, onClose, duration = 5000, position =
424
433
  React.createElement("div", { style: headerStyles }, "Achievement Unlocked!"),
425
434
  React.createElement("div", { style: titleStyles }, achievement.achievementTitle),
426
435
  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")));
436
+ React.createElement("button", { onClick: closeAfterExit, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close notification" }, "\u00D7")));
431
437
  };
432
438
 
433
439
  /**
@@ -537,17 +543,20 @@ const BuiltInConfetti = ({ show, duration = 5000, particleCount = 50, colors = [
537
543
  React.createElement("div", { style: containerStyles, "data-testid": "built-in-confetti" }, particles)));
538
544
  };
539
545
 
540
- const NOTIFICATION_DURATION_MS = 5000;
546
+ const DEFAULT_NOTIFICATION_DURATION_MS = 5000;
547
+ const CONFETTI_DURATION_MS = 5000;
541
548
  const AchievementUIContext = createContext({
542
549
  icons: {},
543
550
  ui: {},
544
551
  });
545
552
  const AchievementEffects = ({ icons, ui }) => {
553
+ var _a;
546
554
  const engine = useAchievementEngine();
547
555
  const seenAchievementsRef = useRef(new Set(engine.getUnlocked()));
548
556
  const confettiTimerRef = useRef(null);
549
557
  const [showConfetti, setShowConfetti] = useState(false);
550
558
  const [notifications, setNotifications] = useState([]);
559
+ const notificationDuration = (_a = ui.notificationDuration) !== null && _a !== void 0 ? _a : DEFAULT_NOTIFICATION_DURATION_MS;
551
560
  useEffect(() => {
552
561
  const unsubscribeUnlocked = engine.on('achievement:unlocked', (event) => {
553
562
  if (seenAchievementsRef.current.has(event.achievementId)) {
@@ -577,7 +586,7 @@ const AchievementEffects = ({ icons, ui }) => {
577
586
  confettiTimerRef.current = setTimeout(() => {
578
587
  setShowConfetti(false);
579
588
  confettiTimerRef.current = null;
580
- }, NOTIFICATION_DURATION_MS);
589
+ }, CONFETTI_DURATION_MS);
581
590
  }
582
591
  });
583
592
  const unsubscribeStateChanged = engine.on('state:changed', () => {
@@ -600,13 +609,13 @@ const AchievementEffects = ({ icons, ui }) => {
600
609
  const ConfettiComponentResolved = ui.ConfettiComponent || BuiltInConfetti;
601
610
  return (React.createElement(React.Fragment, null,
602
611
  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 }))));
612
+ notifications.map((notification, index) => (React.createElement(NotificationComponent, { key: notification.achievementId, achievement: notification, onClose: () => setNotifications((currentNotifications) => currentNotifications.filter((currentNotification) => currentNotification.achievementId !== notification.achievementId)), duration: notificationDuration, position: ui.notificationPosition || 'top-center', theme: ui.theme || 'modern', icons: icons, stackIndex: index }))),
613
+ ui.enableConfetti !== false && (React.createElement(ConfettiComponentResolved, { show: showConfetti, duration: CONFETTI_DURATION_MS }))));
605
614
  };
606
615
  const AchievementProvider = (_a) => {
607
616
  var { children, icons = {}, ui = {}, useBuiltInUI } = _a, providerProps = __rest(_a, ["children", "icons", "ui", "useBuiltInUI"]);
608
617
  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.');
618
+ warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 5.0.');
610
619
  }
611
620
  const uiContextValue = useMemo(() => ({ icons, ui }), [icons, ui]);
612
621
  return (React.createElement(AchievementUIContext.Provider, { value: uiContextValue },
@@ -624,18 +633,14 @@ const useAchievements = () => {
624
633
  };
625
634
 
626
635
  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));
636
+ const { snapshot } = useAchievements();
632
637
  return {
633
- unlockedIds,
634
- unlockedAchievements,
635
- allAchievements,
636
- unlockedCount: unlockedIds.length,
637
- totalCount: allAchievements.length,
638
- metrics: getState().metrics,
638
+ unlockedIds: snapshot.unlockedIds,
639
+ unlockedAchievements: snapshot.unlockedAchievements,
640
+ allAchievements: snapshot.allAchievements,
641
+ unlockedCount: snapshot.unlockedCount,
642
+ totalCount: snapshot.totalCount,
643
+ metrics: snapshot.metrics,
639
644
  };
640
645
  };
641
646
 
@@ -776,13 +781,98 @@ const defaultStyles = {
776
781
  },
777
782
  };
778
783
 
784
+ const isNumericString = (value) => /^-?\d+(\.\d+)?$/.test(value);
785
+ const getBackdropBlurFilter = (backdropBlur) => {
786
+ if (backdropBlur === undefined) {
787
+ return undefined;
788
+ }
789
+ if (typeof backdropBlur === 'number') {
790
+ if (backdropBlur <= 0) {
791
+ return undefined;
792
+ }
793
+ return `blur(${backdropBlur}px)`;
794
+ }
795
+ const trimmedBlur = backdropBlur.trim();
796
+ if (!trimmedBlur ||
797
+ trimmedBlur === '0' ||
798
+ trimmedBlur === '0px' ||
799
+ trimmedBlur === 'none') {
800
+ return undefined;
801
+ }
802
+ const blurValue = isNumericString(trimmedBlur) ? `${trimmedBlur}px` : trimmedBlur;
803
+ return blurValue.startsWith('blur(') ? blurValue : `blur(${blurValue})`;
804
+ };
805
+ const getBackdropBlurStyles = (backdropBlur) => {
806
+ const backdropFilter = getBackdropBlurFilter(backdropBlur);
807
+ if (!backdropFilter) {
808
+ return {};
809
+ }
810
+ return {
811
+ backdropFilter,
812
+ WebkitBackdropFilter: backdropFilter,
813
+ };
814
+ };
815
+
816
+ const compactAchievementStyles = {
817
+ achievementList: {
818
+ display: 'grid',
819
+ gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
820
+ gap: '10px',
821
+ },
822
+ achievementItem: {
823
+ display: 'flex',
824
+ flexDirection: 'column',
825
+ alignItems: 'center',
826
+ justifyContent: 'center',
827
+ gap: '6px',
828
+ padding: '12px 10px',
829
+ borderRadius: '8px',
830
+ aspectRatio: '1 / 1',
831
+ minHeight: '120px',
832
+ textAlign: 'center',
833
+ },
834
+ lockedAchievementItem: {
835
+ display: 'flex',
836
+ flexDirection: 'column',
837
+ alignItems: 'center',
838
+ justifyContent: 'center',
839
+ gap: '6px',
840
+ padding: '12px 10px',
841
+ borderRadius: '8px',
842
+ aspectRatio: '1 / 1',
843
+ minHeight: '120px',
844
+ textAlign: 'center',
845
+ },
846
+ achievementIcon: {
847
+ fontSize: '34px',
848
+ lineHeight: 1,
849
+ flexShrink: 0,
850
+ },
851
+ achievementTitle: {
852
+ margin: '0',
853
+ fontSize: '13px',
854
+ lineHeight: 1.2,
855
+ },
856
+ achievementDescription: {
857
+ margin: '0',
858
+ fontSize: '11px',
859
+ lineHeight: 1.25,
860
+ },
861
+ lockIcon: {
862
+ fontSize: '15px',
863
+ top: '8px',
864
+ right: '8px',
865
+ transform: 'none',
866
+ },
867
+ };
868
+ const getDensityStyles = (density) => (density === 'compact' ? compactAchievementStyles : {});
779
869
  const resolveIcon = (achievement, icons) => {
780
870
  return ((achievement.achievementIconKey && icons[achievement.achievementIconKey]) ||
781
871
  achievement.achievementIconKey ||
782
872
  icons.default ||
783
873
  '⭐');
784
874
  };
785
- const AchievementsList = ({ achievements, showLocked = true, showUnlockConditions = false, icons = {}, styles = {}, emptyState, className, renderAchievement, }) => {
875
+ const AchievementsList = ({ achievements, showLocked = true, showUnlockConditions = false, icons = {}, styles = {}, emptyState, className, density = 'comfortable', renderAchievement, }) => {
786
876
  const context = useContext(AchievementContext);
787
877
  const uiContext = useContext(AchievementUIContext);
788
878
  const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
@@ -793,36 +883,37 @@ const AchievementsList = ({ achievements, showLocked = true, showUnlockCondition
793
883
  const achievementsToDisplay = showLocked
794
884
  ? sourceAchievements
795
885
  : sourceAchievements.filter((achievement) => achievement.isUnlocked);
886
+ const densityStyles = getDensityStyles(density);
796
887
  if (achievementsToDisplay.length === 0) {
797
888
  return (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, emptyState || 'No achievements configured.'));
798
889
  }
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) => {
890
+ return (React.createElement("div", { className: className, style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementList), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementList), styles === null || styles === void 0 ? void 0 : styles.achievementList), "data-density": density, "data-testid": "achievements-list" }, achievementsToDisplay.map((achievement, index) => {
800
891
  const isLocked = !achievement.isUnlocked;
801
892
  const icon = resolveIcon(achievement, mergedIcons);
802
893
  if (renderAchievement) {
803
- return (React.createElement(React.Fragment, { key: achievement.achievementId }, renderAchievement({ achievement, isLocked, icon, index })));
894
+ return (React.createElement(React.Fragment, { key: achievement.achievementId }, renderAchievement({ achievement, isLocked, icon, index, density })));
804
895
  }
805
896
  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' }) },
897
+ ? Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.lockedAchievementItem), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.lockedAchievementItem), styles === null || styles === void 0 ? void 0 : styles.lockedAchievementItem) : Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementItem), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementItem))), styles === null || styles === void 0 ? void 0 : styles.achievementItem), { position: 'relative' }), "data-testid": "achievement-list-item", "data-unlocked": achievement.isUnlocked ? 'true' : 'false' },
898
+ React.createElement("div", { style: Object.assign(Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementIcon), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementIcon), styles === null || styles === void 0 ? void 0 : styles.achievementIcon), { opacity: isLocked ? 0.4 : 1 }) }, icon),
899
+ React.createElement("div", { style: density === 'compact' ? { width: '100%', minWidth: 0 } : { flex: 1 } },
900
+ React.createElement("h3", { style: Object.assign(Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementTitle), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementTitle), styles === null || styles === void 0 ? void 0 : styles.achievementTitle), { color: isLocked ? '#999' : undefined }) }, achievement.achievementTitle),
901
+ achievement.achievementDescription && (React.createElement("p", { style: Object.assign(Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementDescription), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementDescription), styles === null || styles === void 0 ? void 0 : styles.achievementDescription), { color: isLocked ? '#aaa' : '#666' }) },
811
902
  achievement.achievementDescription,
812
903
  showUnlockConditions && isLocked && (React.createElement("span", { style: {
813
904
  display: 'block',
814
- fontSize: '12px',
815
- marginTop: '4px',
905
+ fontSize: density === 'compact' ? '11px' : '12px',
906
+ marginTop: density === 'compact' ? '2px' : '4px',
816
907
  fontStyle: 'italic',
817
908
  color: '#888',
818
909
  } },
819
910
  "\uD83D\uDD13 ",
820
911
  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"))));
912
+ isLocked && (React.createElement("div", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.lockIcon), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.lockIcon), styles === null || styles === void 0 ? void 0 : styles.lockIcon) }, "\uD83D\uDD12"))));
822
913
  })));
823
914
  };
824
915
 
825
- const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achievements', styles = {}, icons = {}, showLocked = true, showUnlockConditions = false, emptyState, renderAchievement, theme, }) => {
916
+ const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achievements', styles = {}, icons = {}, showLocked = true, showUnlockConditions = false, emptyState, renderAchievement, theme, hideScrollbar = false, density = 'comfortable', backdropBlur, }) => {
826
917
  const context = useContext(AchievementContext);
827
918
  const uiContext = useContext(AchievementUIContext);
828
919
  useEffect(() => {
@@ -850,19 +941,30 @@ const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achiev
850
941
  ? sourceAchievements
851
942
  : sourceAchievements === null || sourceAchievements === void 0 ? void 0 : sourceAchievements.filter((achievement) => achievement.isUnlocked);
852
943
  const resolvedTheme = theme || uiContext.ui.theme || 'modern';
944
+ const backdropBlurFilter = getBackdropBlurFilter(backdropBlur);
853
945
  const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
854
946
  if (CustomModal) {
855
947
  if (!modalAchievements) {
856
948
  throw new Error('AchievementsModal requires either an achievements prop or an AchievementProvider parent.');
857
949
  }
858
- return (React.createElement(CustomModal, { isOpen: isOpen, onClose: onClose, achievements: modalAchievements, icons: mergedIcons, theme: resolvedTheme }));
950
+ return (React.createElement(CustomModal, { isOpen: isOpen, onClose: onClose, achievements: modalAchievements, icons: mergedIcons, theme: resolvedTheme, hideScrollbar: hideScrollbar, density: density, backdropBlur: backdropBlur }));
859
951
  }
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" },
952
+ return (React.createElement("div", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.overlay), styles === null || styles === void 0 ? void 0 : styles.overlay), getBackdropBlurStyles(backdropBlur)), role: "presentation", onClick: onClose, "data-backdrop-blur": backdropBlurFilter, "data-testid": "achievements-modal-overlay" },
953
+ hideScrollbar && (React.createElement("style", null, `
954
+ [data-react-achievements-modal-content][data-hide-scrollbar="true"] {
955
+ scrollbar-width: none;
956
+ -ms-overflow-style: none;
957
+ }
958
+
959
+ [data-react-achievements-modal-content][data-hide-scrollbar="true"]::-webkit-scrollbar {
960
+ display: none;
961
+ }
962
+ `)),
963
+ 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-hide-scrollbar": hideScrollbar ? 'true' : undefined, "data-density": density, "data-react-achievements-modal-content": true, "data-testid": "achievements-modal" },
862
964
  React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.header), styles === null || styles === void 0 ? void 0 : styles.header) },
863
965
  React.createElement("h2", { id: "achievements-modal-title", style: { margin: 0 } }, title),
864
966
  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 }))));
967
+ React.createElement(AchievementsList, { achievements: modalAchievements, showLocked: showLocked, showUnlockConditions: showUnlockConditions, icons: icons, styles: styles, emptyState: emptyState, renderAchievement: renderAchievement, density: density }))));
866
968
  };
867
969
 
868
970
  const getPositionStyles$1 = (position) => {
@@ -882,7 +984,7 @@ const getPositionStyles$1 = (position) => {
882
984
  return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
883
985
  }
884
986
  };
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, }) => {
987
+ const AchievementsWidget = ({ position = 'bottom-right', placement = 'fixed', showAllAchievements = true, showUnlockConditions = false, showCount = true, icons, theme, density = 'comfortable', label = 'Achievements', icon = '🏆', triggerClassName, renderTrigger, buttonStyles, modalStyles, modalTitle, emptyState, renderAchievement, hideModalScrollbar = false, modalBackdropBlur, }) => {
886
988
  const uiContext = useContext(AchievementUIContext);
887
989
  const [isModalOpen, setIsModalOpen] = useState(false);
888
990
  const { unlockedAchievements, allAchievements, unlockedCount, totalCount } = useAchievementState();
@@ -922,7 +1024,7 @@ const AchievementsWidget = ({ position = 'bottom-right', placement = 'fixed', sh
922
1024
  React.createElement("span", null, icon),
923
1025
  React.createElement("span", { style: { flex: 1 } }, label),
924
1026
  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 })));
1027
+ React.createElement(AchievementsModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: modalAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles, title: modalTitle, emptyState: emptyState, renderAchievement: renderAchievement, theme: resolvedTheme, hideScrollbar: hideModalScrollbar, density: density, backdropBlur: modalBackdropBlur })));
926
1028
  };
927
1029
 
928
1030
  const getPositionStyles = (position) => {
@@ -944,11 +1046,11 @@ const getPositionStyles = (position) => {
944
1046
  };
945
1047
  /**
946
1048
  * @deprecated Use `AchievementsWidget` for new integrations. This v3
947
- * compatibility wrapper will be removed in 4.2.
1049
+ * compatibility wrapper will be removed in 5.0.
948
1050
  */
949
1051
  const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
950
1052
  React.useEffect(() => {
951
- warnDeprecation('`BadgesButton` is deprecated. Use `AchievementsWidget` instead. `BadgesButton` will be removed in 4.2.');
1053
+ warnDeprecation('`BadgesButton` is deprecated. Use `AchievementsWidget` instead. `BadgesButton` will be removed in 5.0.');
952
1054
  }, []);
953
1055
  // Get theme configuration for consistent styling
954
1056
  const themeConfig = getTheme(theme) || builtInThemes.modern;
@@ -985,11 +1087,11 @@ const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed',
985
1087
  /**
986
1088
  * @deprecated Use `AchievementsModal`, `AchievementsWidget`, or
987
1089
  * `AchievementsList` for new integrations. This v3 compatibility wrapper will
988
- * be removed in 4.2.
1090
+ * be removed in 5.0.
989
1091
  */
990
1092
  const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, showAllAchievements = false, showUnlockConditions = false, allAchievements, }) => {
991
1093
  useEffect(() => {
992
- warnDeprecation('`BadgesModal` is deprecated. Use `AchievementsWidget` or `AchievementsList` instead. `BadgesModal` will be removed in 4.2.');
1094
+ warnDeprecation('`BadgesModal` is deprecated. Use `AchievementsWidget` or `AchievementsList` instead. `BadgesModal` will be removed in 5.0.');
993
1095
  }, []);
994
1096
  const achievementsToDisplay = showAllAchievements && allAchievements
995
1097
  ? allAchievements
@@ -999,7 +1101,7 @@ const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, s
999
1101
 
1000
1102
  /**
1001
1103
  * @deprecated Use `AchievementsWidget` for new integrations. This v3
1002
- * compatibility wrapper will be removed in 4.2.
1104
+ * compatibility wrapper will be removed in 5.0.
1003
1105
  */
1004
1106
  const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right', placement = 'fixed', showAllAchievements = false, allAchievements, showUnlockConditions = false, icons, theme = 'modern', buttonStyles, modalStyles, }) => {
1005
1107
  const [isModalOpen, setIsModalOpen] = useState(false);
@@ -1010,11 +1112,11 @@ const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right'
1010
1112
 
1011
1113
  /**
1012
1114
  * @deprecated Use the provider `ui.ConfettiComponent` option or the built-in
1013
- * confetti default. This v3 compatibility wrapper will be removed in 4.2.
1115
+ * confetti default. This v3 compatibility wrapper will be removed in 5.0.
1014
1116
  */
1015
1117
  const ConfettiWrapper = ({ show }) => {
1016
1118
  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.');
1119
+ warnDeprecation('`ConfettiWrapper` is deprecated. Use the provider `ui.ConfettiComponent` option or built-in confetti defaults instead. `ConfettiWrapper` will be removed in 5.0.');
1018
1120
  }, []);
1019
1121
  return React.createElement(BuiltInConfetti, { show: show, particleCount: 200 });
1020
1122
  };
@@ -1049,17 +1151,11 @@ const LevelProgress = ({ level, currentXP, nextLevelXP, label = 'Level', valueLa
1049
1151
  * Provides the v4 happy path for direct metric updates plus explicit state names.
1050
1152
  */
1051
1153
  const useSimpleAchievements = () => {
1052
- const { update, reset, getState, exportData, importData } = useAchievements();
1154
+ const { update, reset, getState, exportData, importData, engine } = useAchievements();
1053
1155
  const achievementState = useAchievementState();
1054
1156
  const track = (metric, value) => update({ [metric]: value });
1055
1157
  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 });
1158
+ return engine.increment(metric, amount);
1063
1159
  };
1064
1160
  const trackMultiple = (metrics) => update(metrics);
1065
1161
  return {
@@ -1078,11 +1174,11 @@ const useSimpleAchievements = () => {
1078
1174
  importData,
1079
1175
  getAllAchievements: () => achievementState.allAchievements,
1080
1176
  /**
1081
- * @deprecated Use `unlockedIds` instead. This alias will be removed in 4.2.
1177
+ * @deprecated Use `unlockedIds` instead. This alias will be removed in 5.0.
1082
1178
  */
1083
1179
  unlocked: achievementState.unlockedIds,
1084
1180
  /**
1085
- * @deprecated Use `allAchievements` instead. This alias will be removed in 4.2.
1181
+ * @deprecated Use `allAchievements` instead. This alias will be removed in 5.0.
1086
1182
  */
1087
1183
  all: achievementState.allAchievements,
1088
1184
  };
@@ -1092,12 +1188,14 @@ const useSimpleAchievements = () => {
1092
1188
  * Built-in modal component
1093
1189
  * Modern, theme-aware achievement modal with smooth animations
1094
1190
  */
1095
- const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', }) => {
1191
+ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', hideScrollbar = false, density = 'comfortable', backdropBlur, }) => {
1096
1192
  // Merge custom icons with defaults
1097
1193
  const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
1098
1194
  // Get theme configuration
1099
1195
  const themeConfig = getTheme(theme) || builtInThemes.modern;
1100
1196
  const { modal: themeStyles } = themeConfig;
1197
+ const isCompact = density === 'compact';
1198
+ const backdropBlurFilter = getBackdropBlurFilter(backdropBlur);
1101
1199
  useEffect(() => {
1102
1200
  if (isOpen) {
1103
1201
  // Lock body scroll when modal is open
@@ -1113,24 +1211,12 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1113
1211
  }, [isOpen]);
1114
1212
  if (!isOpen)
1115
1213
  return null;
1116
- const overlayStyles = {
1117
- position: 'fixed',
1118
- top: 0,
1119
- left: 0,
1120
- right: 0,
1121
- bottom: 0,
1122
- backgroundColor: themeStyles.overlayColor,
1123
- display: 'flex',
1124
- alignItems: 'center',
1125
- justifyContent: 'center',
1126
- zIndex: 10000,
1127
- animation: 'fadeIn 0.3s ease-in-out',
1128
- };
1214
+ const overlayStyles = Object.assign({ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: themeStyles.overlayColor, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000, animation: 'fadeIn 0.3s ease-in-out' }, getBackdropBlurStyles(backdropBlur));
1129
1215
  const modalStyles = {
1130
1216
  background: themeStyles.background,
1131
1217
  borderRadius: themeStyles.borderRadius,
1132
- padding: '32px',
1133
- maxWidth: '600px',
1218
+ padding: isCompact ? '18px' : '32px',
1219
+ maxWidth: isCompact ? '520px' : '600px',
1134
1220
  width: '90%',
1135
1221
  maxHeight: '80vh',
1136
1222
  overflow: 'auto',
@@ -1142,18 +1228,18 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1142
1228
  display: 'flex',
1143
1229
  justifyContent: 'space-between',
1144
1230
  alignItems: 'center',
1145
- marginBottom: '24px',
1231
+ marginBottom: isCompact ? '16px' : '24px',
1146
1232
  };
1147
1233
  const titleStyles = {
1148
1234
  margin: 0,
1149
1235
  color: themeStyles.textColor,
1150
- fontSize: themeStyles.headerFontSize || '28px',
1236
+ fontSize: isCompact ? '22px' : themeStyles.headerFontSize || '28px',
1151
1237
  fontWeight: 'bold',
1152
1238
  };
1153
1239
  const closeButtonStyles = {
1154
1240
  background: 'none',
1155
1241
  border: 'none',
1156
- fontSize: '32px',
1242
+ fontSize: isCompact ? '26px' : '32px',
1157
1243
  cursor: 'pointer',
1158
1244
  color: themeStyles.textColor,
1159
1245
  opacity: 0.6,
@@ -1161,17 +1247,17 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1161
1247
  padding: 0,
1162
1248
  lineHeight: 1,
1163
1249
  };
1164
- const isBadgeLayout = themeStyles.achievementLayout === 'badge';
1250
+ const isBadgeLayout = isCompact || themeStyles.achievementLayout === 'badge';
1165
1251
  const listStyles = isBadgeLayout
1166
1252
  ? {
1167
1253
  display: 'grid',
1168
- gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
1169
- gap: '16px',
1254
+ gridTemplateColumns: `repeat(auto-fill, minmax(${isCompact ? '112px' : '140px'}, 1fr))`,
1255
+ gap: isCompact ? '10px' : '16px',
1170
1256
  }
1171
1257
  : {
1172
1258
  display: 'flex',
1173
1259
  flexDirection: 'column',
1174
- gap: '12px',
1260
+ gap: isCompact ? '8px' : '12px',
1175
1261
  };
1176
1262
  const getAchievementItemStyles = (isUnlocked) => {
1177
1263
  const baseStyles = {
@@ -1185,24 +1271,24 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1185
1271
  };
1186
1272
  if (isBadgeLayout) {
1187
1273
  // Badge layout: vertical, centered, square-ish
1188
- return Object.assign(Object.assign({}, baseStyles), { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: '20px 12px', aspectRatio: '1 / 1.1', minHeight: '160px' });
1274
+ return Object.assign(Object.assign({}, baseStyles), { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: isCompact ? '12px 8px' : '20px 12px', aspectRatio: '1 / 1.1', minHeight: isCompact ? '118px' : '160px' });
1189
1275
  }
1190
1276
  else {
1191
1277
  // Horizontal layout (default)
1192
- return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: '16px', padding: '16px' });
1278
+ return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: isCompact ? '10px' : '16px', padding: isCompact ? '10px 12px' : '16px' });
1193
1279
  }
1194
1280
  };
1195
1281
  const getIconContainerStyles = (isUnlocked) => {
1196
1282
  if (isBadgeLayout) {
1197
1283
  return {
1198
- fontSize: '48px',
1284
+ fontSize: isCompact ? '32px' : '48px',
1199
1285
  lineHeight: 1,
1200
- marginBottom: '8px',
1286
+ marginBottom: isCompact ? '6px' : '8px',
1201
1287
  opacity: isUnlocked ? 1 : 0.3,
1202
1288
  };
1203
1289
  }
1204
1290
  return {
1205
- fontSize: '40px',
1291
+ fontSize: isCompact ? '28px' : '40px',
1206
1292
  flexShrink: 0,
1207
1293
  lineHeight: 1,
1208
1294
  opacity: isUnlocked ? 1 : 0.3,
@@ -1220,14 +1306,14 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1220
1306
  ? {
1221
1307
  margin: '0 0 4px 0',
1222
1308
  color: themeStyles.textColor,
1223
- fontSize: '14px',
1309
+ fontSize: isCompact ? '12px' : '14px',
1224
1310
  fontWeight: 'bold',
1225
1311
  lineHeight: '1.3',
1226
1312
  }
1227
1313
  : {
1228
- margin: '0 0 8px 0',
1314
+ margin: isCompact ? '0 0 4px 0' : '0 0 8px 0',
1229
1315
  color: themeStyles.textColor,
1230
- fontSize: '18px',
1316
+ fontSize: isCompact ? '14px' : '18px',
1231
1317
  fontWeight: 'bold',
1232
1318
  overflow: 'hidden',
1233
1319
  textOverflow: 'ellipsis',
@@ -1238,27 +1324,27 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1238
1324
  margin: 0,
1239
1325
  color: themeStyles.textColor,
1240
1326
  opacity: 0.7,
1241
- fontSize: '11px',
1327
+ fontSize: isCompact ? '10px' : '11px',
1242
1328
  lineHeight: '1.3',
1243
1329
  }
1244
1330
  : {
1245
1331
  margin: 0,
1246
1332
  color: themeStyles.textColor,
1247
1333
  opacity: 0.8,
1248
- fontSize: '14px',
1334
+ fontSize: isCompact ? '12px' : '14px',
1249
1335
  };
1250
1336
  const getLockIconStyles = () => {
1251
1337
  if (isBadgeLayout) {
1252
1338
  return {
1253
1339
  position: 'absolute',
1254
- top: '8px',
1255
- right: '8px',
1256
- fontSize: '18px',
1340
+ top: isCompact ? '6px' : '8px',
1341
+ right: isCompact ? '6px' : '8px',
1342
+ fontSize: isCompact ? '14px' : '18px',
1257
1343
  opacity: 0.6,
1258
1344
  };
1259
1345
  }
1260
1346
  return {
1261
- fontSize: '24px',
1347
+ fontSize: isCompact ? '18px' : '24px',
1262
1348
  flexShrink: 0,
1263
1349
  opacity: 0.5,
1264
1350
  };
@@ -1279,9 +1365,18 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1279
1365
  opacity: 1;
1280
1366
  }
1281
1367
  }
1368
+
1369
+ [data-react-achievements-built-in-modal][data-hide-scrollbar="true"] {
1370
+ scrollbar-width: none;
1371
+ -ms-overflow-style: none;
1372
+ }
1373
+
1374
+ [data-react-achievements-built-in-modal][data-hide-scrollbar="true"]::-webkit-scrollbar {
1375
+ display: none;
1376
+ }
1282
1377
  `),
1283
- React.createElement("div", { style: overlayStyles, onClick: onClose, "data-testid": "built-in-modal-overlay" },
1284
- React.createElement("div", { style: modalStyles, onClick: (e) => e.stopPropagation(), "data-testid": "built-in-modal" },
1378
+ React.createElement("div", { style: overlayStyles, onClick: onClose, "data-backdrop-blur": backdropBlurFilter, "data-testid": "built-in-modal-overlay" },
1379
+ React.createElement("div", { style: modalStyles, onClick: (e) => e.stopPropagation(), "data-hide-scrollbar": hideScrollbar ? 'true' : undefined, "data-density": density, "data-react-achievements-built-in-modal": true, "data-testid": "built-in-modal" },
1285
1380
  React.createElement("div", { style: headerStyles },
1286
1381
  React.createElement("h2", { style: titleStyles }, "\uD83C\uDFC6 Achievements"),
1287
1382
  React.createElement("button", { onClick: onClose, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close modal" }, "\u00D7")),