react-achievements 3.4.2 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +354 -21
  2. package/dist/index.d.ts +210 -2
  3. package/dist/index.js +915 -185
  4. package/dist/index.js.map +1 -1
  5. package/dist/types/core/components/BadgesButton.d.ts +18 -3
  6. package/dist/types/core/components/BadgesModal.d.ts +4 -1
  7. package/dist/types/core/hooks/useWindowSize.d.ts +16 -0
  8. package/dist/types/core/types.d.ts +6 -0
  9. package/dist/types/core/ui/BuiltInConfetti.d.ts +7 -0
  10. package/dist/types/core/ui/BuiltInModal.d.ts +7 -0
  11. package/dist/types/core/ui/BuiltInNotification.d.ts +7 -0
  12. package/dist/types/core/ui/LegacyWrappers.d.ts +21 -0
  13. package/dist/types/core/ui/interfaces.d.ts +131 -0
  14. package/dist/types/core/ui/legacyDetector.d.ts +40 -0
  15. package/dist/types/core/ui/themes.d.ts +14 -0
  16. package/dist/types/hooks/useSimpleAchievements.d.ts +5 -0
  17. package/dist/types/index.d.ts +6 -1
  18. package/dist/types/providers/AchievementProvider.d.ts +15 -2
  19. package/package.json +15 -1
  20. package/dist/assets/defaultIcons.d.ts +0 -81
  21. package/dist/badges.d.ts +0 -8
  22. package/dist/components/Achievement.d.ts +0 -10
  23. package/dist/components/AchievementModal.d.ts +0 -12
  24. package/dist/components/Badge.d.ts +0 -9
  25. package/dist/components/BadgesButton.d.ts +0 -14
  26. package/dist/components/BadgesModal.d.ts +0 -12
  27. package/dist/components/ConfettiWrapper.d.ts +0 -6
  28. package/dist/components/Progress.d.ts +0 -6
  29. package/dist/context/AchievementContext.d.ts +0 -21
  30. package/dist/defaultStyles.d.ts +0 -19
  31. package/dist/hooks/useAchievement.d.ts +0 -8
  32. package/dist/hooks/useAchievementState.d.ts +0 -4
  33. package/dist/index.cjs.js +0 -2428
  34. package/dist/index.esm.js +0 -2403
  35. package/dist/levels.d.ts +0 -7
  36. package/dist/providers/AchievementProvider.d.ts +0 -12
  37. package/dist/redux/achievementSlice.d.ts +0 -30
  38. package/dist/redux/notificationSlice.d.ts +0 -7
  39. package/dist/redux/store.d.ts +0 -15
  40. package/dist/stories/Button.d.ts +0 -28
  41. package/dist/stories/Button.stories.d.ts +0 -23
  42. package/dist/stories/Header.d.ts +0 -13
  43. package/dist/stories/Header.stories.d.ts +0 -18
  44. package/dist/stories/Page.d.ts +0 -3
  45. package/dist/stories/Page.stories.d.ts +0 -12
  46. package/dist/types/core/context/AchievementContext.d.ts +0 -5
  47. package/dist/types/stories/Button.d.ts +0 -16
  48. package/dist/types/stories/Button.stories.d.ts +0 -23
  49. package/dist/types/stories/Header.d.ts +0 -13
  50. package/dist/types/stories/Header.stories.d.ts +0 -18
  51. package/dist/types/stories/Page.d.ts +0 -3
  52. package/dist/types/stories/Page.stories.d.ts +0 -12
  53. package/dist/types.d.ts +0 -37
  54. package/dist/utils/EventEmitter.d.ts +0 -6
package/dist/index.js CHANGED
@@ -1,9 +1,6 @@
1
- import React, { createContext, useState, useRef, useEffect, useContext } from 'react';
1
+ import React, { useState, useEffect, createContext, useRef, useContext } from 'react';
2
2
  import Modal from 'react-modal';
3
3
  import Confetti from 'react-confetti';
4
- import { useWindowSize } from 'react-use';
5
- import { toast, ToastContainer } from 'react-toastify';
6
- import 'react-toastify/dist/ReactToastify.css';
7
4
 
8
5
  const isDate = (value) => {
9
6
  return value instanceof Date;
@@ -861,6 +858,122 @@ class OfflineQueueStorage {
861
858
  }
862
859
  }
863
860
 
861
+ /**
862
+ * Built-in theme presets
863
+ */
864
+ const builtInThemes = {
865
+ /**
866
+ * Modern theme - Dark gradients with vibrant accents
867
+ * Inspired by contemporary achievement systems (Discord, Steam, Xbox)
868
+ */
869
+ modern: {
870
+ name: 'modern',
871
+ notification: {
872
+ background: 'linear-gradient(135deg, rgba(30, 30, 50, 0.98) 0%, rgba(50, 50, 70, 0.98) 100%)',
873
+ textColor: '#ffffff',
874
+ accentColor: '#4CAF50',
875
+ borderRadius: '12px',
876
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
877
+ fontSize: {
878
+ header: '12px',
879
+ title: '18px',
880
+ description: '14px',
881
+ },
882
+ },
883
+ modal: {
884
+ overlayColor: 'rgba(0, 0, 0, 0.85)',
885
+ background: 'linear-gradient(135deg, #1e1e32 0%, #323246 100%)',
886
+ textColor: '#ffffff',
887
+ accentColor: '#4CAF50',
888
+ borderRadius: '16px',
889
+ headerFontSize: '28px',
890
+ },
891
+ confetti: {
892
+ colors: ['#FFD700', '#4CAF50', '#2196F3', '#FF6B6B'],
893
+ particleCount: 50,
894
+ shapes: ['circle', 'square'],
895
+ },
896
+ },
897
+ /**
898
+ * Minimal theme - Clean, light design with subtle accents
899
+ * Perfect for professional or minimalist applications
900
+ */
901
+ minimal: {
902
+ name: 'minimal',
903
+ notification: {
904
+ background: 'rgba(255, 255, 255, 0.98)',
905
+ textColor: '#333333',
906
+ accentColor: '#4CAF50',
907
+ borderRadius: '8px',
908
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
909
+ fontSize: {
910
+ header: '11px',
911
+ title: '16px',
912
+ description: '13px',
913
+ },
914
+ },
915
+ modal: {
916
+ overlayColor: 'rgba(0, 0, 0, 0.5)',
917
+ background: '#ffffff',
918
+ textColor: '#333333',
919
+ accentColor: '#4CAF50',
920
+ borderRadius: '12px',
921
+ headerFontSize: '24px',
922
+ },
923
+ confetti: {
924
+ colors: ['#4CAF50', '#2196F3'],
925
+ particleCount: 30,
926
+ shapes: ['circle'],
927
+ },
928
+ },
929
+ /**
930
+ * Gamified theme - Modern gaming aesthetic with sci-fi colors
931
+ * Dark navy backgrounds with cyan and orange accents (2024 gaming trend)
932
+ * Features square/badge-shaped achievement cards
933
+ */
934
+ gamified: {
935
+ name: 'gamified',
936
+ notification: {
937
+ background: 'linear-gradient(135deg, rgba(5, 8, 22, 0.98) 0%, rgba(15, 23, 42, 0.98) 100%)',
938
+ textColor: '#22d3ee', // Bright cyan
939
+ accentColor: '#f97316', // Bright orange
940
+ borderRadius: '6px',
941
+ boxShadow: '0 8px 32px rgba(34, 211, 238, 0.4), 0 0 20px rgba(249, 115, 22, 0.3)',
942
+ fontSize: {
943
+ header: '13px',
944
+ title: '20px',
945
+ description: '15px',
946
+ },
947
+ },
948
+ modal: {
949
+ overlayColor: 'rgba(5, 8, 22, 0.85)',
950
+ background: 'linear-gradient(135deg, #0f172a 0%, #050816 100%)',
951
+ textColor: '#22d3ee', // Bright cyan
952
+ accentColor: '#f97316', // Bright orange
953
+ borderRadius: '8px',
954
+ headerFontSize: '32px',
955
+ achievementCardBorderRadius: '8px', // Square badge-like cards
956
+ achievementLayout: 'badge', // Use badge/grid layout instead of horizontal list
957
+ },
958
+ confetti: {
959
+ colors: ['#22d3ee', '#f97316', '#a855f7', '#eab308'], // Cyan, orange, purple, yellow
960
+ particleCount: 100,
961
+ shapes: ['circle', 'square'],
962
+ },
963
+ },
964
+ };
965
+ /**
966
+ * Retrieve a theme by name (internal use only)
967
+ * Only checks built-in themes
968
+ *
969
+ * @param name - Theme name (built-in only)
970
+ * @returns Theme configuration or undefined if not found
971
+ * @internal
972
+ */
973
+ function getTheme(name) {
974
+ return builtInThemes[name];
975
+ }
976
+
864
977
  const getPositionStyles = (position) => {
865
978
  const base = {
866
979
  position: 'fixed',
@@ -878,13 +991,34 @@ const getPositionStyles = (position) => {
878
991
  return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
879
992
  }
880
993
  };
881
- const BadgesButton = ({ onClick, position, styles = {}, unlockedAchievements }) => {
882
- const baseStyles = Object.assign(Object.assign({ backgroundColor: '#4CAF50', color: 'white', padding: '10px 20px', border: 'none', borderRadius: '20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '16px', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', transition: 'transform 0.2s ease-in-out' }, getPositionStyles(position)), styles);
994
+ const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
995
+ // Get theme configuration for consistent styling
996
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
997
+ const accentColor = themeConfig.notification.accentColor;
998
+ // Different styling for fixed vs inline placement
999
+ const baseStyles = placement === 'inline'
1000
+ ? Object.assign({
1001
+ // Inline mode: looks like a navigation item
1002
+ backgroundColor: 'transparent', color: themeConfig.notification.textColor, padding: '12px 16px', border: 'none', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', fontSize: '15px', width: '100%', textAlign: 'left', transition: 'background-color 0.2s ease-in-out' }, styles) : Object.assign(Object.assign({
1003
+ // Fixed mode: floating button
1004
+ backgroundColor: accentColor, color: 'white', padding: '10px 20px', border: 'none', borderRadius: '20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '16px', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', transition: 'transform 0.2s ease-in-out' }, getPositionStyles(position)), styles);
883
1005
  return (React.createElement("button", { onClick: onClick, style: baseStyles, onMouseEnter: (e) => {
884
- e.target.style.transform = 'scale(1.05)';
1006
+ if (placement === 'inline') {
1007
+ // Inline mode: subtle background color change
1008
+ e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
1009
+ }
1010
+ else {
1011
+ // Fixed mode: scale transformation
1012
+ e.target.style.transform = 'scale(1.05)';
1013
+ }
885
1014
  }, onMouseLeave: (e) => {
886
- e.target.style.transform = 'scale(1)';
887
- } },
1015
+ if (placement === 'inline') {
1016
+ e.target.style.backgroundColor = 'transparent';
1017
+ }
1018
+ else {
1019
+ e.target.style.transform = 'scale(1)';
1020
+ }
1021
+ }, "data-placement": placement, "data-testid": "badges-button" },
888
1022
  "\uD83C\uDFC6 Achievements (",
889
1023
  unlockedAchievements.length,
890
1024
  ")"));
@@ -901,90 +1035,181 @@ const defaultAchievementIcons = {
901
1035
  star: '⭐',
902
1036
  };
903
1037
 
904
- const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {} }) => {
905
- // Merge custom icons with default icons, with custom icons taking precedence
906
- const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
907
- const defaultOverlayStyle = {
908
- position: 'fixed',
909
- top: 0,
910
- left: 0,
911
- right: 0,
912
- bottom: 0,
913
- backgroundColor: 'rgba(0, 0, 0, 0.75)',
914
- display: 'flex',
915
- alignItems: 'center',
916
- justifyContent: 'center',
917
- zIndex: 1000,
918
- };
919
- const defaultContentStyle = {
920
- position: 'relative',
921
- background: '#fff',
922
- borderRadius: '8px',
923
- padding: '20px',
924
- maxWidth: '500px',
925
- width: '90%',
926
- maxHeight: '80vh',
927
- overflow: 'auto',
928
- };
929
- const defaultHeaderStyle = {
930
- display: 'flex',
931
- justifyContent: 'space-between',
932
- alignItems: 'center',
933
- marginBottom: '20px',
934
- };
935
- const defaultCloseButtonStyle = {
936
- background: 'none',
1038
+ const defaultStyles = {
1039
+ badgesButton: {
1040
+ backgroundColor: '#4CAF50',
1041
+ color: 'white',
1042
+ padding: '10px 20px',
937
1043
  border: 'none',
938
- fontSize: '24px',
1044
+ borderRadius: '20px',
939
1045
  cursor: 'pointer',
940
- padding: '0',
941
- };
942
- const defaultAchievementListStyle = {
943
1046
  display: 'flex',
944
- flexDirection: 'column',
945
- gap: '16px',
946
- };
947
- const defaultAchievementItemStyle = {
948
- display: 'flex',
949
- gap: '16px',
950
- padding: '16px',
951
- borderRadius: '8px',
952
- backgroundColor: '#f5f5f5',
953
1047
  alignItems: 'center',
954
- };
955
- const defaultAchievementTitleStyle = {
956
- margin: '0',
957
- fontSize: '18px',
958
- fontWeight: 'bold',
959
- };
960
- const defaultAchievementDescriptionStyle = {
961
- margin: '4px 0 0 0',
962
- color: '#666',
963
- };
964
- const defaultAchievementIconStyle = {
965
- fontSize: '32px',
966
- display: 'flex',
967
- alignItems: 'center',
968
- justifyContent: 'center',
969
- };
1048
+ gap: '8px',
1049
+ fontSize: '16px',
1050
+ boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
1051
+ transition: 'transform 0.2s ease-in-out',
1052
+ },
1053
+ badgesModal: {
1054
+ overlay: {
1055
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
1056
+ display: 'flex',
1057
+ alignItems: 'center',
1058
+ justifyContent: 'center',
1059
+ zIndex: 1000,
1060
+ },
1061
+ content: {
1062
+ position: 'relative',
1063
+ background: '#fff',
1064
+ borderRadius: '8px',
1065
+ padding: '20px',
1066
+ maxWidth: '500px',
1067
+ width: '90%',
1068
+ maxHeight: '80vh',
1069
+ overflow: 'auto',
1070
+ },
1071
+ header: {
1072
+ display: 'flex',
1073
+ justifyContent: 'space-between',
1074
+ alignItems: 'center',
1075
+ marginBottom: '20px',
1076
+ },
1077
+ closeButton: {
1078
+ background: 'none',
1079
+ border: 'none',
1080
+ fontSize: '24px',
1081
+ cursor: 'pointer',
1082
+ padding: '0',
1083
+ },
1084
+ achievementList: {
1085
+ display: 'flex',
1086
+ flexDirection: 'column',
1087
+ gap: '16px',
1088
+ },
1089
+ achievementItem: {
1090
+ display: 'flex',
1091
+ gap: '16px',
1092
+ padding: '16px',
1093
+ borderRadius: '8px',
1094
+ backgroundColor: '#f5f5f5',
1095
+ alignItems: 'center',
1096
+ },
1097
+ achievementTitle: {
1098
+ margin: '0',
1099
+ fontSize: '18px',
1100
+ fontWeight: 'bold',
1101
+ },
1102
+ achievementDescription: {
1103
+ margin: '4px 0 0 0',
1104
+ color: '#666',
1105
+ },
1106
+ achievementIcon: {
1107
+ fontSize: '32px',
1108
+ display: 'flex',
1109
+ alignItems: 'center',
1110
+ justifyContent: 'center',
1111
+ },
1112
+ lockIcon: {
1113
+ fontSize: '24px',
1114
+ position: 'absolute',
1115
+ top: '50%',
1116
+ right: '16px',
1117
+ transform: 'translateY(-50%)',
1118
+ },
1119
+ lockedAchievementItem: {
1120
+ display: 'flex',
1121
+ gap: '16px',
1122
+ padding: '16px',
1123
+ borderRadius: '8px',
1124
+ backgroundColor: '#e0e0e0',
1125
+ alignItems: 'center',
1126
+ opacity: 0.5,
1127
+ },
1128
+ },
1129
+ };
1130
+
1131
+ const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, showAllAchievements = false, showUnlockConditions = false, allAchievements, }) => {
1132
+ // Merge custom icons with default icons, with custom icons taking precedence
1133
+ const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
970
1134
  return (React.createElement(Modal, { isOpen: isOpen, onRequestClose: onClose, style: {
971
- overlay: Object.assign(Object.assign({}, defaultOverlayStyle), styles === null || styles === void 0 ? void 0 : styles.overlay),
972
- content: Object.assign(Object.assign({}, defaultContentStyle), styles === null || styles === void 0 ? void 0 : styles.content)
1135
+ overlay: Object.assign(Object.assign({}, defaultStyles.badgesModal.overlay), styles === null || styles === void 0 ? void 0 : styles.overlay),
1136
+ content: Object.assign(Object.assign({}, defaultStyles.badgesModal.content), styles === null || styles === void 0 ? void 0 : styles.content)
973
1137
  }, contentLabel: "Achievements" },
974
- React.createElement("div", { style: Object.assign(Object.assign({}, defaultHeaderStyle), styles === null || styles === void 0 ? void 0 : styles.header) },
1138
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.header), styles === null || styles === void 0 ? void 0 : styles.header) },
975
1139
  React.createElement("h2", { style: { margin: 0 } }, "\uD83C\uDFC6 Achievements"),
976
- React.createElement("button", { onClick: onClose, style: Object.assign(Object.assign({}, defaultCloseButtonStyle), styles === null || styles === void 0 ? void 0 : styles.closeButton), "aria-label": "Close" }, "\u00D7")),
977
- React.createElement("div", { style: Object.assign(Object.assign({}, defaultAchievementListStyle), styles === null || styles === void 0 ? void 0 : styles.achievementList) },
978
- achievements.map((achievement) => (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign({}, defaultAchievementItemStyle), styles === null || styles === void 0 ? void 0 : styles.achievementItem) },
979
- achievement.achievementIconKey && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultAchievementIconStyle), styles === null || styles === void 0 ? void 0 : styles.achievementIcon) }, achievement.achievementIconKey in mergedIcons
980
- ? mergedIcons[achievement.achievementIconKey]
981
- : mergedIcons.default || '⭐')),
982
- React.createElement("div", null,
983
- React.createElement("h3", { style: Object.assign(Object.assign({}, defaultAchievementTitleStyle), styles === null || styles === void 0 ? void 0 : styles.achievementTitle) }, achievement.achievementTitle),
984
- React.createElement("p", { style: Object.assign(Object.assign({}, defaultAchievementDescriptionStyle), styles === null || styles === void 0 ? void 0 : styles.achievementDescription) }, achievement.achievementDescription))))),
985
- achievements.length === 0 && (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, "No achievements unlocked yet. Keep going!")))));
1140
+ 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")),
1141
+ React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementList), styles === null || styles === void 0 ? void 0 : styles.achievementList) }, (() => {
1142
+ // Determine which achievements to display
1143
+ const achievementsToDisplay = showAllAchievements && allAchievements
1144
+ ? allAchievements
1145
+ : achievements.map(a => (Object.assign(Object.assign({}, a), { isUnlocked: true })));
1146
+ return (React.createElement(React.Fragment, null,
1147
+ achievementsToDisplay.map((achievement) => {
1148
+ const isLocked = !achievement.isUnlocked;
1149
+ return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign(Object.assign({}, (isLocked
1150
+ ? 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' }) },
1151
+ achievement.achievementIconKey && (React.createElement("div", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementIcon), styles === null || styles === void 0 ? void 0 : styles.achievementIcon), { opacity: isLocked ? 0.4 : 1 }) }, achievement.achievementIconKey in mergedIcons
1152
+ ? mergedIcons[achievement.achievementIconKey]
1153
+ : mergedIcons.default || '⭐')),
1154
+ React.createElement("div", { style: { flex: 1 } },
1155
+ 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),
1156
+ 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' }) },
1157
+ achievement.achievementDescription,
1158
+ showUnlockConditions && isLocked && achievement.achievementDescription && (React.createElement("span", { style: {
1159
+ display: 'block',
1160
+ fontSize: '12px',
1161
+ marginTop: '4px',
1162
+ fontStyle: 'italic',
1163
+ color: '#888'
1164
+ } },
1165
+ "\uD83D\uDD13 ",
1166
+ achievement.achievementDescription)))),
1167
+ isLocked && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.lockIcon), styles === null || styles === void 0 ? void 0 : styles.lockIcon) }, "\uD83D\uDD12"))));
1168
+ }),
1169
+ achievementsToDisplay.length === 0 && (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, "No achievements configured."))));
1170
+ })())));
986
1171
  };
987
1172
 
1173
+ /**
1174
+ * Hook to track window dimensions
1175
+ * Replacement for react-use's useWindowSize
1176
+ *
1177
+ * @returns Object with width and height properties
1178
+ *
1179
+ * @example
1180
+ * ```tsx
1181
+ * const { width, height } = useWindowSize();
1182
+ * console.log(`Window size: ${width}x${height}`);
1183
+ * ```
1184
+ */
1185
+ function useWindowSize() {
1186
+ const [size, setSize] = useState({
1187
+ width: typeof window !== 'undefined' ? window.innerWidth : 0,
1188
+ height: typeof window !== 'undefined' ? window.innerHeight : 0,
1189
+ });
1190
+ useEffect(() => {
1191
+ // Handle SSR - window may not be defined
1192
+ if (typeof window === 'undefined') {
1193
+ return;
1194
+ }
1195
+ const handleResize = () => {
1196
+ setSize({
1197
+ width: window.innerWidth,
1198
+ height: window.innerHeight,
1199
+ });
1200
+ };
1201
+ // Set initial size
1202
+ handleResize();
1203
+ // Add event listener
1204
+ window.addEventListener('resize', handleResize);
1205
+ // Cleanup
1206
+ return () => {
1207
+ window.removeEventListener('resize', handleResize);
1208
+ };
1209
+ }, []);
1210
+ return size;
1211
+ }
1212
+
988
1213
  const ConfettiWrapper = ({ show }) => {
989
1214
  const { width, height } = useWindowSize();
990
1215
  if (!show)
@@ -1299,8 +1524,547 @@ function preserveMetrics(current, imported) {
1299
1524
  return preserved;
1300
1525
  }
1301
1526
 
1527
+ /**
1528
+ * Built-in notification component
1529
+ * Modern, theme-aware achievement notification with smooth animations
1530
+ */
1531
+ const BuiltInNotification = ({ achievement, onClose, duration = 5000, position = 'top-center', theme = 'modern', }) => {
1532
+ var _a, _b, _c;
1533
+ const [isVisible, setIsVisible] = useState(false);
1534
+ const [isExiting, setIsExiting] = useState(false);
1535
+ // Get theme configuration
1536
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
1537
+ const { notification: themeStyles } = themeConfig;
1538
+ useEffect(() => {
1539
+ // Slide in animation
1540
+ const showTimer = setTimeout(() => setIsVisible(true), 10);
1541
+ // Auto-dismiss
1542
+ const dismissTimer = setTimeout(() => {
1543
+ setIsExiting(true);
1544
+ setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
1545
+ }, duration);
1546
+ return () => {
1547
+ clearTimeout(showTimer);
1548
+ clearTimeout(dismissTimer);
1549
+ };
1550
+ }, [duration, onClose]);
1551
+ const getPositionStyles = () => {
1552
+ const base = {
1553
+ position: 'fixed',
1554
+ zIndex: 9999,
1555
+ };
1556
+ switch (position) {
1557
+ case 'top-center':
1558
+ return Object.assign(Object.assign({}, base), { top: 20, left: '50%', transform: 'translateX(-50%)' });
1559
+ case 'top-left':
1560
+ return Object.assign(Object.assign({}, base), { top: 20, left: 20 });
1561
+ case 'top-right':
1562
+ return Object.assign(Object.assign({}, base), { top: 20, right: 20 });
1563
+ case 'bottom-center':
1564
+ return Object.assign(Object.assign({}, base), { bottom: 20, left: '50%', transform: 'translateX(-50%)' });
1565
+ case 'bottom-left':
1566
+ return Object.assign(Object.assign({}, base), { bottom: 20, left: 20 });
1567
+ case 'bottom-right':
1568
+ return Object.assign(Object.assign({}, base), { bottom: 20, right: 20 });
1569
+ default:
1570
+ return Object.assign(Object.assign({}, base), { top: 20, left: '50%', transform: 'translateX(-50%)' });
1571
+ }
1572
+ };
1573
+ const containerStyles = Object.assign(Object.assign({}, getPositionStyles()), { background: themeStyles.background, borderRadius: themeStyles.borderRadius, boxShadow: themeStyles.boxShadow, padding: '16px 24px', minWidth: '320px', maxWidth: '500px', display: 'flex', alignItems: 'center', gap: '16px', opacity: isVisible && !isExiting ? 1 : 0, transform: position.startsWith('top')
1574
+ ? `translateY(${isVisible && !isExiting ? '0' : '-20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`
1575
+ : `translateY(${isVisible && !isExiting ? '0' : '20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`, transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', pointerEvents: isVisible ? 'auto' : 'none' });
1576
+ const iconStyles = {
1577
+ fontSize: '48px',
1578
+ lineHeight: 1,
1579
+ flexShrink: 0,
1580
+ };
1581
+ const contentStyles = {
1582
+ flex: 1,
1583
+ color: themeStyles.textColor,
1584
+ minWidth: 0,
1585
+ };
1586
+ const headerStyles = {
1587
+ fontSize: ((_a = themeStyles.fontSize) === null || _a === void 0 ? void 0 : _a.header) || '12px',
1588
+ textTransform: 'uppercase',
1589
+ letterSpacing: '1px',
1590
+ opacity: 0.8,
1591
+ marginBottom: '4px',
1592
+ color: themeStyles.accentColor,
1593
+ fontWeight: 600,
1594
+ };
1595
+ const titleStyles = {
1596
+ fontSize: ((_b = themeStyles.fontSize) === null || _b === void 0 ? void 0 : _b.title) || '18px',
1597
+ fontWeight: 'bold',
1598
+ marginBottom: '4px',
1599
+ overflow: 'hidden',
1600
+ textOverflow: 'ellipsis',
1601
+ whiteSpace: 'nowrap',
1602
+ };
1603
+ const descriptionStyles = {
1604
+ fontSize: ((_c = themeStyles.fontSize) === null || _c === void 0 ? void 0 : _c.description) || '14px',
1605
+ opacity: 0.9,
1606
+ overflow: 'hidden',
1607
+ textOverflow: 'ellipsis',
1608
+ display: '-webkit-box',
1609
+ WebkitLineClamp: 2,
1610
+ WebkitBoxOrient: 'vertical',
1611
+ };
1612
+ const closeButtonStyles = {
1613
+ background: 'none',
1614
+ border: 'none',
1615
+ color: themeStyles.textColor,
1616
+ fontSize: '24px',
1617
+ cursor: 'pointer',
1618
+ opacity: 0.6,
1619
+ transition: 'opacity 0.2s',
1620
+ padding: '4px',
1621
+ flexShrink: 0,
1622
+ lineHeight: 1,
1623
+ };
1624
+ return (React.createElement("div", { style: containerStyles, "data-testid": "built-in-notification" },
1625
+ React.createElement("div", { style: iconStyles }, achievement.icon),
1626
+ React.createElement("div", { style: contentStyles },
1627
+ React.createElement("div", { style: headerStyles }, "Achievement Unlocked!"),
1628
+ React.createElement("div", { style: titleStyles }, achievement.title),
1629
+ achievement.description && (React.createElement("div", { style: descriptionStyles }, achievement.description))),
1630
+ React.createElement("button", { onClick: () => {
1631
+ setIsExiting(true);
1632
+ setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
1633
+ }, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close notification" }, "\u00D7")));
1634
+ };
1635
+
1636
+ /**
1637
+ * Built-in confetti component
1638
+ * Lightweight CSS-based confetti animation
1639
+ */
1640
+ const BuiltInConfetti = ({ show, duration = 5000, particleCount = 50, colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'], }) => {
1641
+ const [isVisible, setIsVisible] = useState(false);
1642
+ const { width, height } = useWindowSize();
1643
+ useEffect(() => {
1644
+ if (show) {
1645
+ setIsVisible(true);
1646
+ const timer = setTimeout(() => setIsVisible(false), duration);
1647
+ return () => clearTimeout(timer);
1648
+ }
1649
+ else {
1650
+ setIsVisible(false);
1651
+ }
1652
+ }, [show, duration]);
1653
+ if (!isVisible)
1654
+ return null;
1655
+ const containerStyles = {
1656
+ position: 'fixed',
1657
+ top: 0,
1658
+ left: 0,
1659
+ width: '100%',
1660
+ height: '100%',
1661
+ pointerEvents: 'none',
1662
+ zIndex: 10001,
1663
+ overflow: 'hidden',
1664
+ };
1665
+ // Generate particles
1666
+ const particles = Array.from({ length: particleCount }, (_, i) => {
1667
+ const color = colors[Math.floor(Math.random() * colors.length)];
1668
+ const startX = Math.random() * width;
1669
+ const rotation = Math.random() * 360;
1670
+ const fallDuration = 3 + Math.random() * 2; // 3-5 seconds
1671
+ const delay = Math.random() * 0.5; // 0-0.5s delay
1672
+ const shape = Math.random() > 0.5 ? 'circle' : 'square';
1673
+ const particleStyles = {
1674
+ position: 'absolute',
1675
+ top: '-20px',
1676
+ left: `${startX}px`,
1677
+ width: '10px',
1678
+ height: '10px',
1679
+ backgroundColor: color,
1680
+ borderRadius: shape === 'circle' ? '50%' : '0',
1681
+ transform: `rotate(${rotation}deg)`,
1682
+ animation: `confettiFall ${fallDuration}s linear ${delay}s forwards`,
1683
+ opacity: 0.9,
1684
+ };
1685
+ return React.createElement("div", { key: i, style: particleStyles, "data-testid": "confetti-particle" });
1686
+ });
1687
+ return (React.createElement(React.Fragment, null,
1688
+ React.createElement("style", null, `
1689
+ @keyframes confettiFall {
1690
+ 0% {
1691
+ transform: translateY(0) rotate(0deg);
1692
+ opacity: 1;
1693
+ }
1694
+ 100% {
1695
+ transform: translateY(${height + 50}px) rotate(720deg);
1696
+ opacity: 0;
1697
+ }
1698
+ }
1699
+ `),
1700
+ React.createElement("div", { style: containerStyles, "data-testid": "built-in-confetti" }, particles)));
1701
+ };
1702
+
1703
+ /**
1704
+ * Legacy UI library detection system
1705
+ * Attempts to dynamically import external UI libraries
1706
+ * Shows deprecation warnings when detected
1707
+ */
1708
+ let cachedLibraries = null;
1709
+ let detectionAttempted = false;
1710
+ let deprecationWarningShown = false;
1711
+ /**
1712
+ * Attempts to dynamically import legacy UI libraries
1713
+ * Uses try/catch to gracefully handle missing dependencies
1714
+ * Caches result to avoid multiple import attempts
1715
+ *
1716
+ * @returns Promise resolving to LegacyLibraries object
1717
+ */
1718
+ function detectLegacyLibraries() {
1719
+ return __awaiter(this, void 0, void 0, function* () {
1720
+ if (detectionAttempted && cachedLibraries !== null) {
1721
+ return cachedLibraries;
1722
+ }
1723
+ detectionAttempted = true;
1724
+ const libraries = {};
1725
+ // Try to import react-toastify
1726
+ try {
1727
+ const toastifyModule = yield import('react-toastify');
1728
+ libraries.toast = toastifyModule.toast;
1729
+ libraries.ToastContainer = toastifyModule.ToastContainer;
1730
+ }
1731
+ catch (_a) {
1732
+ // Not installed, will use built-in notification
1733
+ }
1734
+ // Try to import react-modal
1735
+ try {
1736
+ const modalModule = yield import('react-modal');
1737
+ libraries.Modal = modalModule.default;
1738
+ }
1739
+ catch (_b) {
1740
+ // Not installed, will use built-in modal
1741
+ }
1742
+ // Try to import react-confetti
1743
+ try {
1744
+ const confettiModule = yield import('react-confetti');
1745
+ libraries.Confetti = confettiModule.default;
1746
+ }
1747
+ catch (_c) {
1748
+ // Not installed, will use built-in confetti
1749
+ }
1750
+ // Try to import react-use (only for useWindowSize)
1751
+ try {
1752
+ const reactUseModule = yield import('react-use');
1753
+ libraries.useWindowSize = reactUseModule.useWindowSize;
1754
+ }
1755
+ catch (_d) {
1756
+ // Not installed, will use built-in useWindowSize
1757
+ }
1758
+ cachedLibraries = libraries;
1759
+ // Show deprecation warning if ANY legacy library is found
1760
+ if (!deprecationWarningShown && Object.keys(libraries).length > 0) {
1761
+ const foundLibraries = [];
1762
+ if (libraries.toast)
1763
+ foundLibraries.push('react-toastify');
1764
+ if (libraries.Modal)
1765
+ foundLibraries.push('react-modal');
1766
+ if (libraries.Confetti)
1767
+ foundLibraries.push('react-confetti');
1768
+ if (libraries.useWindowSize)
1769
+ foundLibraries.push('react-use');
1770
+ console.warn(`[react-achievements] DEPRECATION WARNING: External UI dependencies (${foundLibraries.join(', ')}) are deprecated and will become fully optional in v4.0.0.\n\n` +
1771
+ `The library now includes built-in UI components with modern design and theme support.\n\n` +
1772
+ `To migrate:\n` +
1773
+ `1. Add "useBuiltInUI={true}" to your AchievementProvider\n` +
1774
+ `2. Test your application (UI will change to modern theme)\n` +
1775
+ `3. Optionally customize with theme="minimal" or theme="gamified"\n` +
1776
+ `4. Remove external dependencies from package.json\n\n` +
1777
+ `To silence this warning, set useBuiltInUI={true} in AchievementProvider.\n\n` +
1778
+ `Learn more: https://github.com/dave-b-b/react-achievements#migration-guide`);
1779
+ deprecationWarningShown = true;
1780
+ }
1781
+ return libraries;
1782
+ });
1783
+ }
1784
+
1785
+ /**
1786
+ * Built-in modal component
1787
+ * Modern, theme-aware achievement modal with smooth animations
1788
+ */
1789
+ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', }) => {
1790
+ // Merge custom icons with defaults
1791
+ const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
1792
+ // Get theme configuration
1793
+ const themeConfig = getTheme(theme) || builtInThemes.modern;
1794
+ const { modal: themeStyles } = themeConfig;
1795
+ useEffect(() => {
1796
+ if (isOpen) {
1797
+ // Lock body scroll when modal is open
1798
+ document.body.style.overflow = 'hidden';
1799
+ }
1800
+ else {
1801
+ // Restore body scroll
1802
+ document.body.style.overflow = '';
1803
+ }
1804
+ return () => {
1805
+ document.body.style.overflow = '';
1806
+ };
1807
+ }, [isOpen]);
1808
+ if (!isOpen)
1809
+ return null;
1810
+ const overlayStyles = {
1811
+ position: 'fixed',
1812
+ top: 0,
1813
+ left: 0,
1814
+ right: 0,
1815
+ bottom: 0,
1816
+ backgroundColor: themeStyles.overlayColor,
1817
+ display: 'flex',
1818
+ alignItems: 'center',
1819
+ justifyContent: 'center',
1820
+ zIndex: 10000,
1821
+ animation: 'fadeIn 0.3s ease-in-out',
1822
+ };
1823
+ const modalStyles = {
1824
+ background: themeStyles.background,
1825
+ borderRadius: themeStyles.borderRadius,
1826
+ padding: '32px',
1827
+ maxWidth: '600px',
1828
+ width: '90%',
1829
+ maxHeight: '80vh',
1830
+ overflow: 'auto',
1831
+ boxShadow: '0 20px 60px rgba(0, 0, 0, 0.5)',
1832
+ animation: 'scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
1833
+ position: 'relative',
1834
+ };
1835
+ const headerStyles = {
1836
+ display: 'flex',
1837
+ justifyContent: 'space-between',
1838
+ alignItems: 'center',
1839
+ marginBottom: '24px',
1840
+ };
1841
+ const titleStyles = {
1842
+ margin: 0,
1843
+ color: themeStyles.textColor,
1844
+ fontSize: themeStyles.headerFontSize || '28px',
1845
+ fontWeight: 'bold',
1846
+ };
1847
+ const closeButtonStyles = {
1848
+ background: 'none',
1849
+ border: 'none',
1850
+ fontSize: '32px',
1851
+ cursor: 'pointer',
1852
+ color: themeStyles.textColor,
1853
+ opacity: 0.6,
1854
+ transition: 'opacity 0.2s',
1855
+ padding: 0,
1856
+ lineHeight: 1,
1857
+ };
1858
+ const isBadgeLayout = themeStyles.achievementLayout === 'badge';
1859
+ const listStyles = isBadgeLayout
1860
+ ? {
1861
+ display: 'grid',
1862
+ gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
1863
+ gap: '16px',
1864
+ }
1865
+ : {
1866
+ display: 'flex',
1867
+ flexDirection: 'column',
1868
+ gap: '12px',
1869
+ };
1870
+ const getAchievementItemStyles = (isUnlocked) => {
1871
+ const baseStyles = {
1872
+ borderRadius: themeStyles.achievementCardBorderRadius || '12px',
1873
+ backgroundColor: isUnlocked
1874
+ ? `${themeStyles.accentColor}1A` // 10% opacity
1875
+ : 'rgba(255, 255, 255, 0.05)',
1876
+ border: `2px solid ${isUnlocked ? themeStyles.accentColor : 'rgba(255, 255, 255, 0.1)'}`,
1877
+ opacity: isUnlocked ? 1 : 0.5,
1878
+ transition: 'all 0.2s',
1879
+ };
1880
+ if (isBadgeLayout) {
1881
+ // Badge layout: vertical, centered, square-ish
1882
+ 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' });
1883
+ }
1884
+ else {
1885
+ // Horizontal layout (default)
1886
+ return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: '16px', padding: '16px' });
1887
+ }
1888
+ };
1889
+ const getIconContainerStyles = (isUnlocked) => {
1890
+ if (isBadgeLayout) {
1891
+ return {
1892
+ fontSize: '48px',
1893
+ lineHeight: 1,
1894
+ marginBottom: '8px',
1895
+ opacity: isUnlocked ? 1 : 0.3,
1896
+ };
1897
+ }
1898
+ return {
1899
+ fontSize: '40px',
1900
+ flexShrink: 0,
1901
+ lineHeight: 1,
1902
+ opacity: isUnlocked ? 1 : 0.3,
1903
+ };
1904
+ };
1905
+ const contentStyles = isBadgeLayout
1906
+ ? {
1907
+ width: '100%',
1908
+ }
1909
+ : {
1910
+ flex: 1,
1911
+ minWidth: 0,
1912
+ };
1913
+ const achievementTitleStyles = isBadgeLayout
1914
+ ? {
1915
+ margin: '0 0 4px 0',
1916
+ color: themeStyles.textColor,
1917
+ fontSize: '14px',
1918
+ fontWeight: 'bold',
1919
+ lineHeight: '1.3',
1920
+ }
1921
+ : {
1922
+ margin: '0 0 8px 0',
1923
+ color: themeStyles.textColor,
1924
+ fontSize: '18px',
1925
+ fontWeight: 'bold',
1926
+ overflow: 'hidden',
1927
+ textOverflow: 'ellipsis',
1928
+ whiteSpace: 'nowrap',
1929
+ };
1930
+ const achievementDescriptionStyles = isBadgeLayout
1931
+ ? {
1932
+ margin: 0,
1933
+ color: themeStyles.textColor,
1934
+ opacity: 0.7,
1935
+ fontSize: '11px',
1936
+ lineHeight: '1.3',
1937
+ }
1938
+ : {
1939
+ margin: 0,
1940
+ color: themeStyles.textColor,
1941
+ opacity: 0.8,
1942
+ fontSize: '14px',
1943
+ };
1944
+ const getLockIconStyles = () => {
1945
+ if (isBadgeLayout) {
1946
+ return {
1947
+ position: 'absolute',
1948
+ top: '8px',
1949
+ right: '8px',
1950
+ fontSize: '18px',
1951
+ opacity: 0.6,
1952
+ };
1953
+ }
1954
+ return {
1955
+ fontSize: '24px',
1956
+ flexShrink: 0,
1957
+ opacity: 0.5,
1958
+ };
1959
+ };
1960
+ return (React.createElement(React.Fragment, null,
1961
+ React.createElement("style", null, `
1962
+ @keyframes fadeIn {
1963
+ from { opacity: 0; }
1964
+ to { opacity: 1; }
1965
+ }
1966
+ @keyframes scaleIn {
1967
+ from {
1968
+ transform: scale(0.9);
1969
+ opacity: 0;
1970
+ }
1971
+ to {
1972
+ transform: scale(1);
1973
+ opacity: 1;
1974
+ }
1975
+ }
1976
+ `),
1977
+ React.createElement("div", { style: overlayStyles, onClick: onClose, "data-testid": "built-in-modal-overlay" },
1978
+ React.createElement("div", { style: modalStyles, onClick: (e) => e.stopPropagation(), "data-testid": "built-in-modal" },
1979
+ React.createElement("div", { style: headerStyles },
1980
+ React.createElement("h2", { style: titleStyles }, "\uD83C\uDFC6 Achievements"),
1981
+ 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")),
1982
+ React.createElement("div", { style: listStyles }, achievements.length === 0 ? (React.createElement("div", { style: { textAlign: 'center', padding: '40px 20px', color: themeStyles.textColor, opacity: 0.6 } }, "No achievements yet. Start exploring to unlock them!")) : (achievements.map((achievement) => {
1983
+ const icon = (achievement.achievementIconKey &&
1984
+ mergedIcons[achievement.achievementIconKey]) ||
1985
+ mergedIcons.default ||
1986
+ '⭐';
1987
+ return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign({}, getAchievementItemStyles(achievement.isUnlocked)), { position: isBadgeLayout ? 'relative' : 'static' }) },
1988
+ React.createElement("div", { style: getIconContainerStyles(achievement.isUnlocked) }, icon),
1989
+ React.createElement("div", { style: contentStyles },
1990
+ React.createElement("h3", { style: achievementTitleStyles }, achievement.achievementTitle),
1991
+ React.createElement("p", { style: achievementDescriptionStyles }, achievement.achievementDescription)),
1992
+ !achievement.isUnlocked && (React.createElement("div", { style: getLockIconStyles() }, "\uD83D\uDD12"))));
1993
+ })))))));
1994
+ };
1995
+
1996
+ /**
1997
+ * Legacy library wrappers for backwards compatibility
1998
+ * Wraps external UI libraries to match our component interfaces
1999
+ */
2000
+ /**
2001
+ * Wrapper for react-toastify toast notifications
2002
+ * Falls back to built-in notification if not available
2003
+ */
2004
+ const createLegacyToastNotification = (libraries) => {
2005
+ return ({ achievement, onClose }) => {
2006
+ const { toast } = libraries;
2007
+ useEffect(() => {
2008
+ if (!toast)
2009
+ return;
2010
+ // Call toast.success with achievement content
2011
+ toast.success(React.createElement("div", { style: { display: 'flex', alignItems: 'center' } },
2012
+ React.createElement("span", { style: { fontSize: '2em', marginRight: '10px' } }, achievement.icon),
2013
+ React.createElement("div", null,
2014
+ React.createElement("div", { style: { fontSize: '12px', opacity: 0.8, marginBottom: '4px' } }, "Achievement Unlocked!"),
2015
+ React.createElement("div", { style: { fontWeight: 'bold', marginBottom: '4px' } }, achievement.title),
2016
+ achievement.description && (React.createElement("div", { style: { fontSize: '13px', opacity: 0.9 } }, achievement.description)))), {
2017
+ position: 'top-right',
2018
+ autoClose: 5000,
2019
+ hideProgressBar: false,
2020
+ closeOnClick: true,
2021
+ pauseOnHover: true,
2022
+ draggable: true,
2023
+ toastId: achievement.id,
2024
+ onClose,
2025
+ });
2026
+ }, [achievement, toast, onClose]);
2027
+ return null; // Toast handles its own rendering
2028
+ };
2029
+ };
2030
+ /**
2031
+ * Wrapper for react-confetti Confetti component
2032
+ * Falls back to built-in confetti if not available
2033
+ */
2034
+ const createLegacyConfettiWrapper = (libraries) => {
2035
+ return ({ show, duration = 5000, particleCount = 200, colors }) => {
2036
+ const { Confetti, useWindowSize: legacyUseWindowSize } = libraries;
2037
+ // If Confetti not available, use built-in
2038
+ if (!Confetti) {
2039
+ return (React.createElement(BuiltInConfetti, { show: show, duration: duration, particleCount: particleCount, colors: colors }));
2040
+ }
2041
+ // Use react-confetti with react-use's useWindowSize if available
2042
+ // Otherwise fall back to default dimensions
2043
+ let width = 0;
2044
+ let height = 0;
2045
+ if (legacyUseWindowSize) {
2046
+ const size = legacyUseWindowSize();
2047
+ width = size.width;
2048
+ height = size.height;
2049
+ }
2050
+ else if (typeof window !== 'undefined') {
2051
+ width = window.innerWidth;
2052
+ height = window.innerHeight;
2053
+ }
2054
+ if (!show)
2055
+ return null;
2056
+ return (React.createElement(Confetti, { width: width, height: height, numberOfPieces: particleCount, recycle: false, colors: colors, style: {
2057
+ position: 'fixed',
2058
+ top: 0,
2059
+ left: 0,
2060
+ zIndex: 10001,
2061
+ pointerEvents: 'none',
2062
+ } }));
2063
+ };
2064
+ };
2065
+
1302
2066
  const AchievementContext = createContext(undefined);
1303
- const AchievementProvider = ({ achievements: achievementsConfig, storage = StorageType.Local, children, icons = {}, onError, restApiConfig, }) => {
2067
+ const AchievementProvider = ({ achievements: achievementsConfig, storage = StorageType.Local, children, icons = {}, onError, restApiConfig, ui = {}, useBuiltInUI = false, }) => {
1304
2068
  // Normalize the configuration to the complex format
1305
2069
  const achievements = normalizeAchievements(achievementsConfig);
1306
2070
  const [achievementState, setAchievementState] = useState({
@@ -1314,6 +2078,10 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
1314
2078
  const metricsUpdatedRef = useRef(false);
1315
2079
  const [showConfetti, setShowConfetti] = useState(false);
1316
2080
  const [_currentAchievement, setCurrentAchievement] = useState(null);
2081
+ // NEW: UI component resolution state (v3.6.0)
2082
+ const [legacyLibraries, setLegacyLibraries] = useState(null);
2083
+ const [uiReady, setUiReady] = useState(useBuiltInUI); // Ready immediately if forcing built-in
2084
+ const [currentNotification, setCurrentNotification] = useState(null);
1317
2085
  if (!storageRef.current) {
1318
2086
  if (typeof storage === 'string') {
1319
2087
  // StorageType enum
@@ -1395,6 +2163,30 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
1395
2163
  console.error('Error saving notified achievements', e);
1396
2164
  }
1397
2165
  };
2166
+ // NEW: Detect legacy UI libraries on mount (v3.6.0)
2167
+ useEffect(() => {
2168
+ if (useBuiltInUI) {
2169
+ // User explicitly wants built-in UI, skip detection
2170
+ setUiReady(true);
2171
+ return;
2172
+ }
2173
+ // Attempt to detect legacy libraries
2174
+ detectLegacyLibraries().then((libs) => {
2175
+ setLegacyLibraries(libs);
2176
+ setUiReady(true);
2177
+ });
2178
+ }, [useBuiltInUI]);
2179
+ // NEW: Resolve UI components based on detection and config (v3.6.0)
2180
+ const NotificationComponent = ui.NotificationComponent ||
2181
+ (useBuiltInUI ? BuiltInNotification :
2182
+ legacyLibraries && Object.keys(legacyLibraries).length > 0 && legacyLibraries.toast
2183
+ ? createLegacyToastNotification(legacyLibraries)
2184
+ : BuiltInNotification);
2185
+ const ConfettiComponentResolved = ui.ConfettiComponent ||
2186
+ (useBuiltInUI ? BuiltInConfetti :
2187
+ legacyLibraries && Object.keys(legacyLibraries).length > 0 && legacyLibraries.Confetti
2188
+ ? createLegacyConfettiWrapper(legacyLibraries)
2189
+ : BuiltInConfetti);
1398
2190
  useEffect(() => {
1399
2191
  if (!initialLoadRef.current) {
1400
2192
  const savedUnlocked = storageImpl.getUnlockedAchievements() || [];
@@ -1444,30 +2236,21 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
1444
2236
  const allUnlocked = [...achievementState.unlocked, ...newlyUnlockedAchievements];
1445
2237
  setAchievementState(prev => (Object.assign(Object.assign({}, prev), { unlocked: allUnlocked })));
1446
2238
  storageImpl.setUnlockedAchievements(allUnlocked);
1447
- if (achievementToShow) {
2239
+ if (achievementToShow && (ui.enableNotifications !== false)) {
1448
2240
  const achievement = achievementToShow;
1449
- // Show toast notification
2241
+ // Get icon to display
1450
2242
  let iconToDisplay = '🏆';
1451
2243
  if (achievement.achievementIconKey && achievement.achievementIconKey in icons) {
1452
2244
  iconToDisplay = icons[achievement.achievementIconKey];
1453
2245
  }
1454
- const toastId = achievement.achievementId
1455
- ? `achievement-${achievement.achievementId}`
1456
- : `achievement-${Date.now()}`;
1457
- toast.success(React.createElement("div", { style: { display: 'flex', alignItems: 'center' } },
1458
- React.createElement("span", { style: { fontSize: '2em', marginRight: '10px' } }, iconToDisplay),
1459
- React.createElement("div", null,
1460
- React.createElement("h4", { style: { margin: '0 0 8px 0' } }, "Achievement Unlocked! \uD83C\uDF89"),
1461
- React.createElement("div", { style: { fontWeight: 'bold' } }, achievement.achievementTitle),
1462
- React.createElement("div", { style: { color: '#666' } }, achievement.achievementDescription))), {
1463
- position: "top-right",
1464
- autoClose: 5000,
1465
- hideProgressBar: false,
1466
- closeOnClick: true,
1467
- pauseOnHover: true,
1468
- draggable: true,
1469
- progress: undefined,
1470
- toastId
2246
+ // NEW: Use resolved notification component (v3.6.0)
2247
+ setCurrentNotification({
2248
+ achievement: {
2249
+ id: achievement.achievementId || `achievement-${Date.now()}`,
2250
+ title: achievement.achievementTitle || 'Achievement Unlocked!',
2251
+ description: achievement.achievementDescription || '',
2252
+ icon: iconToDisplay,
2253
+ },
1471
2254
  });
1472
2255
  // Update seen achievements
1473
2256
  if (achievement.achievementId) {
@@ -1486,6 +2269,24 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
1486
2269
  }
1487
2270
  }
1488
2271
  }, [metrics, achievementState.unlocked, achievements, icons]);
2272
+ const getAllAchievements = () => {
2273
+ const result = [];
2274
+ // Iterate through all normalized achievements
2275
+ Object.entries(achievements).forEach(([_metricName, metricAchievements]) => {
2276
+ metricAchievements.forEach((achievement) => {
2277
+ const { achievementDetails } = achievement;
2278
+ const isUnlocked = achievementState.unlocked.includes(achievementDetails.achievementId);
2279
+ result.push({
2280
+ achievementId: achievementDetails.achievementId,
2281
+ achievementTitle: achievementDetails.achievementTitle,
2282
+ achievementDescription: achievementDetails.achievementDescription,
2283
+ achievementIconKey: achievementDetails.achievementIconKey,
2284
+ isUnlocked,
2285
+ });
2286
+ });
2287
+ });
2288
+ return result;
2289
+ };
1489
2290
  const update = (newMetrics) => {
1490
2291
  metricsUpdatedRef.current = true;
1491
2292
  const updatedMetrics = Object.assign({}, metrics);
@@ -1571,10 +2372,11 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
1571
2372
  getState,
1572
2373
  exportData,
1573
2374
  importData,
2375
+ getAllAchievements,
1574
2376
  } },
1575
2377
  children,
1576
- React.createElement(ToastContainer, { position: "top-right", autoClose: 5000, hideProgressBar: false, newestOnTop: true, closeOnClick: true, rtl: false, pauseOnFocusLoss: true, draggable: true, pauseOnHover: true, theme: "light" }),
1577
- React.createElement(ConfettiWrapper, { show: showConfetti })));
2378
+ uiReady && currentNotification && ui.enableNotifications !== false && (React.createElement(NotificationComponent, { achievement: currentNotification.achievement, onClose: () => setCurrentNotification(null), duration: 5000, position: ui.notificationPosition || 'top-center', theme: ui.theme || 'modern' })),
2379
+ uiReady && ui.enableConfetti !== false && (React.createElement(ConfettiComponentResolved, { show: showConfetti, duration: 5000 }))));
1578
2380
  };
1579
2381
 
1580
2382
  const useAchievements = () => {
@@ -1590,7 +2392,7 @@ const useAchievements = () => {
1590
2392
  * Provides an easier API for common use cases while maintaining access to advanced features.
1591
2393
  */
1592
2394
  const useSimpleAchievements = () => {
1593
- const { update, achievements, reset, getState, exportData, importData } = useAchievements();
2395
+ const { update, achievements, reset, getState, exportData, importData, getAllAchievements } = useAchievements();
1594
2396
  return {
1595
2397
  /**
1596
2398
  * Track a metric value for achievements
@@ -1647,86 +2449,14 @@ const useSimpleAchievements = () => {
1647
2449
  * @returns Import result with success status and any errors
1648
2450
  */
1649
2451
  importData,
2452
+ /**
2453
+ * Get all achievements with their unlock status
2454
+ * @returns Array of achievements with isUnlocked boolean property
2455
+ */
2456
+ getAllAchievements,
1650
2457
  };
1651
2458
  };
1652
2459
 
1653
- const defaultStyles = {
1654
- badgesButton: {
1655
- backgroundColor: '#4CAF50',
1656
- color: 'white',
1657
- padding: '10px 20px',
1658
- border: 'none',
1659
- borderRadius: '20px',
1660
- cursor: 'pointer',
1661
- display: 'flex',
1662
- alignItems: 'center',
1663
- gap: '8px',
1664
- fontSize: '16px',
1665
- boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
1666
- transition: 'transform 0.2s ease-in-out',
1667
- },
1668
- badgesModal: {
1669
- overlay: {
1670
- backgroundColor: 'rgba(0, 0, 0, 0.75)',
1671
- display: 'flex',
1672
- alignItems: 'center',
1673
- justifyContent: 'center',
1674
- zIndex: 1000,
1675
- },
1676
- content: {
1677
- position: 'relative',
1678
- background: '#fff',
1679
- borderRadius: '8px',
1680
- padding: '20px',
1681
- maxWidth: '500px',
1682
- width: '90%',
1683
- maxHeight: '80vh',
1684
- overflow: 'auto',
1685
- },
1686
- header: {
1687
- display: 'flex',
1688
- justifyContent: 'space-between',
1689
- alignItems: 'center',
1690
- marginBottom: '20px',
1691
- },
1692
- closeButton: {
1693
- background: 'none',
1694
- border: 'none',
1695
- fontSize: '24px',
1696
- cursor: 'pointer',
1697
- padding: '0',
1698
- },
1699
- achievementList: {
1700
- display: 'flex',
1701
- flexDirection: 'column',
1702
- gap: '16px',
1703
- },
1704
- achievementItem: {
1705
- display: 'flex',
1706
- gap: '16px',
1707
- padding: '16px',
1708
- borderRadius: '8px',
1709
- backgroundColor: '#f5f5f5',
1710
- alignItems: 'center',
1711
- },
1712
- achievementTitle: {
1713
- margin: '0',
1714
- fontSize: '18px',
1715
- fontWeight: 'bold',
1716
- },
1717
- achievementDescription: {
1718
- margin: '4px 0 0 0',
1719
- color: '#666',
1720
- },
1721
- achievementIcon: {
1722
- fontSize: '32px',
1723
- display: 'flex',
1724
- alignItems: 'center',
1725
- justifyContent: 'center',
1726
- },
1727
- },
1728
- };
1729
-
1730
2460
  /**
1731
2461
  * Base class for chainable achievement configuration (Tier 2)
1732
2462
  */
@@ -2013,5 +2743,5 @@ class AchievementBuilder {
2013
2743
  }
2014
2744
  }
2015
2745
 
2016
- export { AchievementBuilder, AchievementContext, AchievementError, AchievementProvider, AsyncStorageAdapter, BadgesButton, BadgesModal, ConfettiWrapper, ConfigurationError, ImportValidationError, IndexedDBStorage, LocalStorage, MemoryStorage, OfflineQueueStorage, RestApiStorage, StorageError, StorageQuotaError, StorageType, SyncError, createConfigHash, defaultAchievementIcons, defaultStyles, exportAchievementData, importAchievementData, isAchievementError, isAsyncStorage, isRecoverableError, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements };
2746
+ export { AchievementBuilder, AchievementContext, AchievementError, AchievementProvider, AsyncStorageAdapter, BadgesButton, BadgesModal, BuiltInConfetti, BuiltInModal, BuiltInNotification, ConfettiWrapper, ConfigurationError, ImportValidationError, IndexedDBStorage, LocalStorage, MemoryStorage, OfflineQueueStorage, RestApiStorage, StorageError, StorageQuotaError, StorageType, SyncError, createConfigHash, defaultAchievementIcons, defaultStyles, exportAchievementData, importAchievementData, isAchievementError, isAsyncStorage, isRecoverableError, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements, useWindowSize };
2017
2747
  //# sourceMappingURL=index.js.map