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