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