react-achievements 3.5.0 → 3.6.1

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 (52) hide show
  1. package/README.md +1127 -27
  2. package/dist/index.d.ts +197 -8
  3. package/dist/index.js +764 -32
  4. package/dist/index.js.map +1 -1
  5. package/dist/types/core/components/BadgesButton.d.ts +18 -3
  6. package/dist/types/core/hooks/useWindowSize.d.ts +16 -0
  7. package/dist/types/core/types.d.ts +2 -6
  8. package/dist/types/core/ui/BuiltInConfetti.d.ts +7 -0
  9. package/dist/types/core/ui/BuiltInModal.d.ts +7 -0
  10. package/dist/types/core/ui/BuiltInNotification.d.ts +7 -0
  11. package/dist/types/core/ui/LegacyWrappers.d.ts +21 -0
  12. package/dist/types/core/ui/interfaces.d.ts +131 -0
  13. package/dist/types/core/ui/legacyDetector.d.ts +40 -0
  14. package/dist/types/core/ui/themes.d.ts +14 -0
  15. package/dist/types/index.d.ts +5 -0
  16. package/dist/types/providers/AchievementProvider.d.ts +14 -2
  17. package/package.json +15 -1
  18. package/dist/assets/defaultIcons.d.ts +0 -81
  19. package/dist/badges.d.ts +0 -8
  20. package/dist/components/Achievement.d.ts +0 -10
  21. package/dist/components/AchievementModal.d.ts +0 -12
  22. package/dist/components/Badge.d.ts +0 -9
  23. package/dist/components/BadgesButton.d.ts +0 -14
  24. package/dist/components/BadgesModal.d.ts +0 -12
  25. package/dist/components/ConfettiWrapper.d.ts +0 -6
  26. package/dist/components/Progress.d.ts +0 -6
  27. package/dist/context/AchievementContext.d.ts +0 -21
  28. package/dist/defaultStyles.d.ts +0 -19
  29. package/dist/hooks/useAchievement.d.ts +0 -8
  30. package/dist/hooks/useAchievementState.d.ts +0 -4
  31. package/dist/index.cjs.js +0 -2428
  32. package/dist/index.esm.js +0 -2403
  33. package/dist/levels.d.ts +0 -7
  34. package/dist/providers/AchievementProvider.d.ts +0 -12
  35. package/dist/redux/achievementSlice.d.ts +0 -30
  36. package/dist/redux/notificationSlice.d.ts +0 -7
  37. package/dist/redux/store.d.ts +0 -15
  38. package/dist/stories/Button.d.ts +0 -28
  39. package/dist/stories/Button.stories.d.ts +0 -23
  40. package/dist/stories/Header.d.ts +0 -13
  41. package/dist/stories/Header.stories.d.ts +0 -18
  42. package/dist/stories/Page.d.ts +0 -3
  43. package/dist/stories/Page.stories.d.ts +0 -12
  44. package/dist/types/core/context/AchievementContext.d.ts +0 -5
  45. package/dist/types/stories/Button.d.ts +0 -16
  46. package/dist/types/stories/Button.stories.d.ts +0 -23
  47. package/dist/types/stories/Header.d.ts +0 -13
  48. package/dist/types/stories/Header.stories.d.ts +0 -18
  49. package/dist/types/stories/Page.d.ts +0 -3
  50. package/dist/types/stories/Page.stories.d.ts +0 -12
  51. package/dist/types.d.ts +0 -37
  52. 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
  ")"));
@@ -1036,6 +1170,46 @@ const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, s
1036
1170
  })())));
1037
1171
  };
1038
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
+
1039
1213
  const ConfettiWrapper = ({ show }) => {
1040
1214
  const { width, height } = useWindowSize();
1041
1215
  if (!show)
@@ -1350,8 +1524,547 @@ function preserveMetrics(current, imported) {
1350
1524
  return preserved;
1351
1525
  }
1352
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
+
1353
2066
  const AchievementContext = createContext(undefined);
1354
- 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, }) => {
1355
2068
  // Normalize the configuration to the complex format
1356
2069
  const achievements = normalizeAchievements(achievementsConfig);
1357
2070
  const [achievementState, setAchievementState] = useState({
@@ -1365,6 +2078,10 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
1365
2078
  const metricsUpdatedRef = useRef(false);
1366
2079
  const [showConfetti, setShowConfetti] = useState(false);
1367
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);
1368
2085
  if (!storageRef.current) {
1369
2086
  if (typeof storage === 'string') {
1370
2087
  // StorageType enum
@@ -1446,6 +2163,30 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
1446
2163
  console.error('Error saving notified achievements', e);
1447
2164
  }
1448
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);
1449
2190
  useEffect(() => {
1450
2191
  if (!initialLoadRef.current) {
1451
2192
  const savedUnlocked = storageImpl.getUnlockedAchievements() || [];
@@ -1495,30 +2236,21 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
1495
2236
  const allUnlocked = [...achievementState.unlocked, ...newlyUnlockedAchievements];
1496
2237
  setAchievementState(prev => (Object.assign(Object.assign({}, prev), { unlocked: allUnlocked })));
1497
2238
  storageImpl.setUnlockedAchievements(allUnlocked);
1498
- if (achievementToShow) {
2239
+ if (achievementToShow && (ui.enableNotifications !== false)) {
1499
2240
  const achievement = achievementToShow;
1500
- // Show toast notification
2241
+ // Get icon to display
1501
2242
  let iconToDisplay = '🏆';
1502
2243
  if (achievement.achievementIconKey && achievement.achievementIconKey in icons) {
1503
2244
  iconToDisplay = icons[achievement.achievementIconKey];
1504
2245
  }
1505
- const toastId = achievement.achievementId
1506
- ? `achievement-${achievement.achievementId}`
1507
- : `achievement-${Date.now()}`;
1508
- toast.success(React.createElement("div", { style: { display: 'flex', alignItems: 'center' } },
1509
- React.createElement("span", { style: { fontSize: '2em', marginRight: '10px' } }, iconToDisplay),
1510
- React.createElement("div", null,
1511
- React.createElement("h4", { style: { margin: '0 0 8px 0' } }, "Achievement Unlocked! \uD83C\uDF89"),
1512
- React.createElement("div", { style: { fontWeight: 'bold' } }, achievement.achievementTitle),
1513
- React.createElement("div", { style: { color: '#666' } }, achievement.achievementDescription))), {
1514
- position: "top-right",
1515
- autoClose: 5000,
1516
- hideProgressBar: false,
1517
- closeOnClick: true,
1518
- pauseOnHover: true,
1519
- draggable: true,
1520
- progress: undefined,
1521
- 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
+ },
1522
2254
  });
1523
2255
  // Update seen achievements
1524
2256
  if (achievement.achievementId) {
@@ -1643,8 +2375,8 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
1643
2375
  getAllAchievements,
1644
2376
  } },
1645
2377
  children,
1646
- React.createElement(ToastContainer, { position: "top-right", autoClose: 5000, hideProgressBar: false, newestOnTop: true, closeOnClick: true, rtl: false, pauseOnFocusLoss: true, draggable: true, pauseOnHover: true, theme: "light" }),
1647
- 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 }))));
1648
2380
  };
1649
2381
 
1650
2382
  const useAchievements = () => {
@@ -2011,5 +2743,5 @@ class AchievementBuilder {
2011
2743
  }
2012
2744
  }
2013
2745
 
2014
- 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 };
2015
2747
  //# sourceMappingURL=index.js.map