react-achievements 4.1.0 → 4.2.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
@@ -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,
@@ -606,7 +603,7 @@ const AchievementEffects = ({ icons, ui }) => {
606
603
  const AchievementProvider = (_a) => {
607
604
  var { children, icons = {}, ui = {}, useBuiltInUI } = _a, providerProps = __rest(_a, ["children", "icons", "ui", "useBuiltInUI"]);
608
605
  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.');
606
+ 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
607
  }
611
608
  const uiContextValue = useMemo(() => ({ icons, ui }), [icons, ui]);
612
609
  return (React.createElement(AchievementUIContext.Provider, { value: uiContextValue },
@@ -624,18 +621,14 @@ const useAchievements = () => {
624
621
  };
625
622
 
626
623
  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));
624
+ const { snapshot } = useAchievements();
632
625
  return {
633
- unlockedIds,
634
- unlockedAchievements,
635
- allAchievements,
636
- unlockedCount: unlockedIds.length,
637
- totalCount: allAchievements.length,
638
- metrics: getState().metrics,
626
+ unlockedIds: snapshot.unlockedIds,
627
+ unlockedAchievements: snapshot.unlockedAchievements,
628
+ allAchievements: snapshot.allAchievements,
629
+ unlockedCount: snapshot.unlockedCount,
630
+ totalCount: snapshot.totalCount,
631
+ metrics: snapshot.metrics,
639
632
  };
640
633
  };
641
634
 
@@ -776,13 +769,98 @@ const defaultStyles = {
776
769
  },
777
770
  };
778
771
 
772
+ const isNumericString = (value) => /^-?\d+(\.\d+)?$/.test(value);
773
+ const getBackdropBlurFilter = (backdropBlur) => {
774
+ if (backdropBlur === undefined) {
775
+ return undefined;
776
+ }
777
+ if (typeof backdropBlur === 'number') {
778
+ if (backdropBlur <= 0) {
779
+ return undefined;
780
+ }
781
+ return `blur(${backdropBlur}px)`;
782
+ }
783
+ const trimmedBlur = backdropBlur.trim();
784
+ if (!trimmedBlur ||
785
+ trimmedBlur === '0' ||
786
+ trimmedBlur === '0px' ||
787
+ trimmedBlur === 'none') {
788
+ return undefined;
789
+ }
790
+ const blurValue = isNumericString(trimmedBlur) ? `${trimmedBlur}px` : trimmedBlur;
791
+ return blurValue.startsWith('blur(') ? blurValue : `blur(${blurValue})`;
792
+ };
793
+ const getBackdropBlurStyles = (backdropBlur) => {
794
+ const backdropFilter = getBackdropBlurFilter(backdropBlur);
795
+ if (!backdropFilter) {
796
+ return {};
797
+ }
798
+ return {
799
+ backdropFilter,
800
+ WebkitBackdropFilter: backdropFilter,
801
+ };
802
+ };
803
+
804
+ const compactAchievementStyles = {
805
+ achievementList: {
806
+ display: 'grid',
807
+ gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
808
+ gap: '10px',
809
+ },
810
+ achievementItem: {
811
+ display: 'flex',
812
+ flexDirection: 'column',
813
+ alignItems: 'center',
814
+ justifyContent: 'center',
815
+ gap: '6px',
816
+ padding: '12px 10px',
817
+ borderRadius: '8px',
818
+ aspectRatio: '1 / 1',
819
+ minHeight: '120px',
820
+ textAlign: 'center',
821
+ },
822
+ lockedAchievementItem: {
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
+ achievementIcon: {
835
+ fontSize: '34px',
836
+ lineHeight: 1,
837
+ flexShrink: 0,
838
+ },
839
+ achievementTitle: {
840
+ margin: '0',
841
+ fontSize: '13px',
842
+ lineHeight: 1.2,
843
+ },
844
+ achievementDescription: {
845
+ margin: '0',
846
+ fontSize: '11px',
847
+ lineHeight: 1.25,
848
+ },
849
+ lockIcon: {
850
+ fontSize: '15px',
851
+ top: '8px',
852
+ right: '8px',
853
+ transform: 'none',
854
+ },
855
+ };
856
+ const getDensityStyles = (density) => (density === 'compact' ? compactAchievementStyles : {});
779
857
  const resolveIcon = (achievement, icons) => {
780
858
  return ((achievement.achievementIconKey && icons[achievement.achievementIconKey]) ||
781
859
  achievement.achievementIconKey ||
782
860
  icons.default ||
783
861
  '⭐');
784
862
  };
785
- const AchievementsList = ({ achievements, showLocked = true, showUnlockConditions = false, icons = {}, styles = {}, emptyState, className, renderAchievement, }) => {
863
+ const AchievementsList = ({ achievements, showLocked = true, showUnlockConditions = false, icons = {}, styles = {}, emptyState, className, density = 'comfortable', renderAchievement, }) => {
786
864
  const context = useContext(AchievementContext);
787
865
  const uiContext = useContext(AchievementUIContext);
788
866
  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 +871,37 @@ const AchievementsList = ({ achievements, showLocked = true, showUnlockCondition
793
871
  const achievementsToDisplay = showLocked
794
872
  ? sourceAchievements
795
873
  : sourceAchievements.filter((achievement) => achievement.isUnlocked);
874
+ const densityStyles = getDensityStyles(density);
796
875
  if (achievementsToDisplay.length === 0) {
797
876
  return (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, emptyState || 'No achievements configured.'));
798
877
  }
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) => {
878
+ 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
879
  const isLocked = !achievement.isUnlocked;
801
880
  const icon = resolveIcon(achievement, mergedIcons);
802
881
  if (renderAchievement) {
803
- return (React.createElement(React.Fragment, { key: achievement.achievementId }, renderAchievement({ achievement, isLocked, icon, index })));
882
+ return (React.createElement(React.Fragment, { key: achievement.achievementId }, renderAchievement({ achievement, isLocked, icon, index, density })));
804
883
  }
805
884
  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' }) },
885
+ ? 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' },
886
+ 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),
887
+ React.createElement("div", { style: density === 'compact' ? { width: '100%', minWidth: 0 } : { flex: 1 } },
888
+ 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),
889
+ 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
890
  achievement.achievementDescription,
812
891
  showUnlockConditions && isLocked && (React.createElement("span", { style: {
813
892
  display: 'block',
814
- fontSize: '12px',
815
- marginTop: '4px',
893
+ fontSize: density === 'compact' ? '11px' : '12px',
894
+ marginTop: density === 'compact' ? '2px' : '4px',
816
895
  fontStyle: 'italic',
817
896
  color: '#888',
818
897
  } },
819
898
  "\uD83D\uDD13 ",
820
899
  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"))));
900
+ 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
901
  })));
823
902
  };
824
903
 
825
- const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achievements', styles = {}, icons = {}, showLocked = true, showUnlockConditions = false, emptyState, renderAchievement, theme, }) => {
904
+ const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achievements', styles = {}, icons = {}, showLocked = true, showUnlockConditions = false, emptyState, renderAchievement, theme, hideScrollbar = false, density = 'comfortable', backdropBlur, }) => {
826
905
  const context = useContext(AchievementContext);
827
906
  const uiContext = useContext(AchievementUIContext);
828
907
  useEffect(() => {
@@ -850,19 +929,30 @@ const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achiev
850
929
  ? sourceAchievements
851
930
  : sourceAchievements === null || sourceAchievements === void 0 ? void 0 : sourceAchievements.filter((achievement) => achievement.isUnlocked);
852
931
  const resolvedTheme = theme || uiContext.ui.theme || 'modern';
932
+ const backdropBlurFilter = getBackdropBlurFilter(backdropBlur);
853
933
  const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
854
934
  if (CustomModal) {
855
935
  if (!modalAchievements) {
856
936
  throw new Error('AchievementsModal requires either an achievements prop or an AchievementProvider parent.');
857
937
  }
858
- return (React.createElement(CustomModal, { isOpen: isOpen, onClose: onClose, achievements: modalAchievements, icons: mergedIcons, theme: resolvedTheme }));
938
+ return (React.createElement(CustomModal, { isOpen: isOpen, onClose: onClose, achievements: modalAchievements, icons: mergedIcons, theme: resolvedTheme, hideScrollbar: hideScrollbar, density: density, backdropBlur: backdropBlur }));
859
939
  }
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" },
940
+ 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" },
941
+ hideScrollbar && (React.createElement("style", null, `
942
+ [data-react-achievements-modal-content][data-hide-scrollbar="true"] {
943
+ scrollbar-width: none;
944
+ -ms-overflow-style: none;
945
+ }
946
+
947
+ [data-react-achievements-modal-content][data-hide-scrollbar="true"]::-webkit-scrollbar {
948
+ display: none;
949
+ }
950
+ `)),
951
+ 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
952
  React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.header), styles === null || styles === void 0 ? void 0 : styles.header) },
863
953
  React.createElement("h2", { id: "achievements-modal-title", style: { margin: 0 } }, title),
864
954
  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 }))));
955
+ React.createElement(AchievementsList, { achievements: modalAchievements, showLocked: showLocked, showUnlockConditions: showUnlockConditions, icons: icons, styles: styles, emptyState: emptyState, renderAchievement: renderAchievement, density: density }))));
866
956
  };
867
957
 
868
958
  const getPositionStyles$1 = (position) => {
@@ -882,7 +972,7 @@ const getPositionStyles$1 = (position) => {
882
972
  return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
883
973
  }
884
974
  };
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, }) => {
975
+ 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
976
  const uiContext = useContext(AchievementUIContext);
887
977
  const [isModalOpen, setIsModalOpen] = useState(false);
888
978
  const { unlockedAchievements, allAchievements, unlockedCount, totalCount } = useAchievementState();
@@ -922,7 +1012,7 @@ const AchievementsWidget = ({ position = 'bottom-right', placement = 'fixed', sh
922
1012
  React.createElement("span", null, icon),
923
1013
  React.createElement("span", { style: { flex: 1 } }, label),
924
1014
  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 })));
1015
+ 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
1016
  };
927
1017
 
928
1018
  const getPositionStyles = (position) => {
@@ -944,11 +1034,11 @@ const getPositionStyles = (position) => {
944
1034
  };
945
1035
  /**
946
1036
  * @deprecated Use `AchievementsWidget` for new integrations. This v3
947
- * compatibility wrapper will be removed in 4.2.
1037
+ * compatibility wrapper will be removed in 5.0.
948
1038
  */
949
1039
  const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
950
1040
  React.useEffect(() => {
951
- warnDeprecation('`BadgesButton` is deprecated. Use `AchievementsWidget` instead. `BadgesButton` will be removed in 4.2.');
1041
+ warnDeprecation('`BadgesButton` is deprecated. Use `AchievementsWidget` instead. `BadgesButton` will be removed in 5.0.');
952
1042
  }, []);
953
1043
  // Get theme configuration for consistent styling
954
1044
  const themeConfig = getTheme(theme) || builtInThemes.modern;
@@ -985,11 +1075,11 @@ const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed',
985
1075
  /**
986
1076
  * @deprecated Use `AchievementsModal`, `AchievementsWidget`, or
987
1077
  * `AchievementsList` for new integrations. This v3 compatibility wrapper will
988
- * be removed in 4.2.
1078
+ * be removed in 5.0.
989
1079
  */
990
1080
  const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, showAllAchievements = false, showUnlockConditions = false, allAchievements, }) => {
991
1081
  useEffect(() => {
992
- warnDeprecation('`BadgesModal` is deprecated. Use `AchievementsWidget` or `AchievementsList` instead. `BadgesModal` will be removed in 4.2.');
1082
+ warnDeprecation('`BadgesModal` is deprecated. Use `AchievementsWidget` or `AchievementsList` instead. `BadgesModal` will be removed in 5.0.');
993
1083
  }, []);
994
1084
  const achievementsToDisplay = showAllAchievements && allAchievements
995
1085
  ? allAchievements
@@ -999,7 +1089,7 @@ const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, s
999
1089
 
1000
1090
  /**
1001
1091
  * @deprecated Use `AchievementsWidget` for new integrations. This v3
1002
- * compatibility wrapper will be removed in 4.2.
1092
+ * compatibility wrapper will be removed in 5.0.
1003
1093
  */
1004
1094
  const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right', placement = 'fixed', showAllAchievements = false, allAchievements, showUnlockConditions = false, icons, theme = 'modern', buttonStyles, modalStyles, }) => {
1005
1095
  const [isModalOpen, setIsModalOpen] = useState(false);
@@ -1010,11 +1100,11 @@ const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right'
1010
1100
 
1011
1101
  /**
1012
1102
  * @deprecated Use the provider `ui.ConfettiComponent` option or the built-in
1013
- * confetti default. This v3 compatibility wrapper will be removed in 4.2.
1103
+ * confetti default. This v3 compatibility wrapper will be removed in 5.0.
1014
1104
  */
1015
1105
  const ConfettiWrapper = ({ show }) => {
1016
1106
  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.');
1107
+ warnDeprecation('`ConfettiWrapper` is deprecated. Use the provider `ui.ConfettiComponent` option or built-in confetti defaults instead. `ConfettiWrapper` will be removed in 5.0.');
1018
1108
  }, []);
1019
1109
  return React.createElement(BuiltInConfetti, { show: show, particleCount: 200 });
1020
1110
  };
@@ -1049,17 +1139,11 @@ const LevelProgress = ({ level, currentXP, nextLevelXP, label = 'Level', valueLa
1049
1139
  * Provides the v4 happy path for direct metric updates plus explicit state names.
1050
1140
  */
1051
1141
  const useSimpleAchievements = () => {
1052
- const { update, reset, getState, exportData, importData } = useAchievements();
1142
+ const { update, reset, getState, exportData, importData, engine } = useAchievements();
1053
1143
  const achievementState = useAchievementState();
1054
1144
  const track = (metric, value) => update({ [metric]: value });
1055
1145
  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 });
1146
+ return engine.increment(metric, amount);
1063
1147
  };
1064
1148
  const trackMultiple = (metrics) => update(metrics);
1065
1149
  return {
@@ -1078,11 +1162,11 @@ const useSimpleAchievements = () => {
1078
1162
  importData,
1079
1163
  getAllAchievements: () => achievementState.allAchievements,
1080
1164
  /**
1081
- * @deprecated Use `unlockedIds` instead. This alias will be removed in 4.2.
1165
+ * @deprecated Use `unlockedIds` instead. This alias will be removed in 5.0.
1082
1166
  */
1083
1167
  unlocked: achievementState.unlockedIds,
1084
1168
  /**
1085
- * @deprecated Use `allAchievements` instead. This alias will be removed in 4.2.
1169
+ * @deprecated Use `allAchievements` instead. This alias will be removed in 5.0.
1086
1170
  */
1087
1171
  all: achievementState.allAchievements,
1088
1172
  };
@@ -1092,12 +1176,14 @@ const useSimpleAchievements = () => {
1092
1176
  * Built-in modal component
1093
1177
  * Modern, theme-aware achievement modal with smooth animations
1094
1178
  */
1095
- const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', }) => {
1179
+ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', hideScrollbar = false, density = 'comfortable', backdropBlur, }) => {
1096
1180
  // Merge custom icons with defaults
1097
1181
  const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
1098
1182
  // Get theme configuration
1099
1183
  const themeConfig = getTheme(theme) || builtInThemes.modern;
1100
1184
  const { modal: themeStyles } = themeConfig;
1185
+ const isCompact = density === 'compact';
1186
+ const backdropBlurFilter = getBackdropBlurFilter(backdropBlur);
1101
1187
  useEffect(() => {
1102
1188
  if (isOpen) {
1103
1189
  // Lock body scroll when modal is open
@@ -1113,24 +1199,12 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1113
1199
  }, [isOpen]);
1114
1200
  if (!isOpen)
1115
1201
  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
- };
1202
+ 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
1203
  const modalStyles = {
1130
1204
  background: themeStyles.background,
1131
1205
  borderRadius: themeStyles.borderRadius,
1132
- padding: '32px',
1133
- maxWidth: '600px',
1206
+ padding: isCompact ? '18px' : '32px',
1207
+ maxWidth: isCompact ? '520px' : '600px',
1134
1208
  width: '90%',
1135
1209
  maxHeight: '80vh',
1136
1210
  overflow: 'auto',
@@ -1142,18 +1216,18 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1142
1216
  display: 'flex',
1143
1217
  justifyContent: 'space-between',
1144
1218
  alignItems: 'center',
1145
- marginBottom: '24px',
1219
+ marginBottom: isCompact ? '16px' : '24px',
1146
1220
  };
1147
1221
  const titleStyles = {
1148
1222
  margin: 0,
1149
1223
  color: themeStyles.textColor,
1150
- fontSize: themeStyles.headerFontSize || '28px',
1224
+ fontSize: isCompact ? '22px' : themeStyles.headerFontSize || '28px',
1151
1225
  fontWeight: 'bold',
1152
1226
  };
1153
1227
  const closeButtonStyles = {
1154
1228
  background: 'none',
1155
1229
  border: 'none',
1156
- fontSize: '32px',
1230
+ fontSize: isCompact ? '26px' : '32px',
1157
1231
  cursor: 'pointer',
1158
1232
  color: themeStyles.textColor,
1159
1233
  opacity: 0.6,
@@ -1161,17 +1235,17 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1161
1235
  padding: 0,
1162
1236
  lineHeight: 1,
1163
1237
  };
1164
- const isBadgeLayout = themeStyles.achievementLayout === 'badge';
1238
+ const isBadgeLayout = isCompact || themeStyles.achievementLayout === 'badge';
1165
1239
  const listStyles = isBadgeLayout
1166
1240
  ? {
1167
1241
  display: 'grid',
1168
- gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
1169
- gap: '16px',
1242
+ gridTemplateColumns: `repeat(auto-fill, minmax(${isCompact ? '112px' : '140px'}, 1fr))`,
1243
+ gap: isCompact ? '10px' : '16px',
1170
1244
  }
1171
1245
  : {
1172
1246
  display: 'flex',
1173
1247
  flexDirection: 'column',
1174
- gap: '12px',
1248
+ gap: isCompact ? '8px' : '12px',
1175
1249
  };
1176
1250
  const getAchievementItemStyles = (isUnlocked) => {
1177
1251
  const baseStyles = {
@@ -1185,24 +1259,24 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1185
1259
  };
1186
1260
  if (isBadgeLayout) {
1187
1261
  // 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' });
1262
+ 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
1263
  }
1190
1264
  else {
1191
1265
  // Horizontal layout (default)
1192
- return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: '16px', padding: '16px' });
1266
+ return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: isCompact ? '10px' : '16px', padding: isCompact ? '10px 12px' : '16px' });
1193
1267
  }
1194
1268
  };
1195
1269
  const getIconContainerStyles = (isUnlocked) => {
1196
1270
  if (isBadgeLayout) {
1197
1271
  return {
1198
- fontSize: '48px',
1272
+ fontSize: isCompact ? '32px' : '48px',
1199
1273
  lineHeight: 1,
1200
- marginBottom: '8px',
1274
+ marginBottom: isCompact ? '6px' : '8px',
1201
1275
  opacity: isUnlocked ? 1 : 0.3,
1202
1276
  };
1203
1277
  }
1204
1278
  return {
1205
- fontSize: '40px',
1279
+ fontSize: isCompact ? '28px' : '40px',
1206
1280
  flexShrink: 0,
1207
1281
  lineHeight: 1,
1208
1282
  opacity: isUnlocked ? 1 : 0.3,
@@ -1220,14 +1294,14 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1220
1294
  ? {
1221
1295
  margin: '0 0 4px 0',
1222
1296
  color: themeStyles.textColor,
1223
- fontSize: '14px',
1297
+ fontSize: isCompact ? '12px' : '14px',
1224
1298
  fontWeight: 'bold',
1225
1299
  lineHeight: '1.3',
1226
1300
  }
1227
1301
  : {
1228
- margin: '0 0 8px 0',
1302
+ margin: isCompact ? '0 0 4px 0' : '0 0 8px 0',
1229
1303
  color: themeStyles.textColor,
1230
- fontSize: '18px',
1304
+ fontSize: isCompact ? '14px' : '18px',
1231
1305
  fontWeight: 'bold',
1232
1306
  overflow: 'hidden',
1233
1307
  textOverflow: 'ellipsis',
@@ -1238,27 +1312,27 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1238
1312
  margin: 0,
1239
1313
  color: themeStyles.textColor,
1240
1314
  opacity: 0.7,
1241
- fontSize: '11px',
1315
+ fontSize: isCompact ? '10px' : '11px',
1242
1316
  lineHeight: '1.3',
1243
1317
  }
1244
1318
  : {
1245
1319
  margin: 0,
1246
1320
  color: themeStyles.textColor,
1247
1321
  opacity: 0.8,
1248
- fontSize: '14px',
1322
+ fontSize: isCompact ? '12px' : '14px',
1249
1323
  };
1250
1324
  const getLockIconStyles = () => {
1251
1325
  if (isBadgeLayout) {
1252
1326
  return {
1253
1327
  position: 'absolute',
1254
- top: '8px',
1255
- right: '8px',
1256
- fontSize: '18px',
1328
+ top: isCompact ? '6px' : '8px',
1329
+ right: isCompact ? '6px' : '8px',
1330
+ fontSize: isCompact ? '14px' : '18px',
1257
1331
  opacity: 0.6,
1258
1332
  };
1259
1333
  }
1260
1334
  return {
1261
- fontSize: '24px',
1335
+ fontSize: isCompact ? '18px' : '24px',
1262
1336
  flexShrink: 0,
1263
1337
  opacity: 0.5,
1264
1338
  };
@@ -1279,9 +1353,18 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
1279
1353
  opacity: 1;
1280
1354
  }
1281
1355
  }
1356
+
1357
+ [data-react-achievements-built-in-modal][data-hide-scrollbar="true"] {
1358
+ scrollbar-width: none;
1359
+ -ms-overflow-style: none;
1360
+ }
1361
+
1362
+ [data-react-achievements-built-in-modal][data-hide-scrollbar="true"]::-webkit-scrollbar {
1363
+ display: none;
1364
+ }
1282
1365
  `),
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" },
1366
+ React.createElement("div", { style: overlayStyles, onClick: onClose, "data-backdrop-blur": backdropBlurFilter, "data-testid": "built-in-modal-overlay" },
1367
+ 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
1368
  React.createElement("div", { style: headerStyles },
1286
1369
  React.createElement("h2", { style: titleStyles }, "\uD83C\uDFC6 Achievements"),
1287
1370
  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")),