react-achievements 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/headless.cjs +37 -50
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.ts +11 -10
- package/dist/headless.esm.js +37 -50
- package/dist/headless.esm.js.map +1 -1
- package/dist/index.cjs +198 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +30 -16
- package/dist/index.esm.js +198 -115
- package/dist/index.esm.js.map +1 -1
- package/dist/web.cjs +198 -115
- package/dist/web.cjs.map +1 -1
- package/dist/web.d.ts +30 -16
- package/dist/web.esm.js +198 -115
- package/dist/web.esm.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -64,15 +64,12 @@ function warnDeprecation(message) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const AchievementContext = React.createContext(undefined);
|
|
67
|
-
const getAllAchievementRecord = (
|
|
68
|
-
return Object.fromEntries(
|
|
69
|
-
achievement.achievementId,
|
|
70
|
-
achievement,
|
|
71
|
-
]));
|
|
67
|
+
const getAllAchievementRecord = (achievements) => {
|
|
68
|
+
return Object.fromEntries(achievements.map((achievement) => [achievement.achievementId, achievement]));
|
|
72
69
|
};
|
|
73
70
|
const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'local', children, onError, useBuiltInUI, restApiConfig, engine: externalEngine, eventMapping, icons = {}, }) => {
|
|
74
71
|
if (useBuiltInUI !== undefined) {
|
|
75
|
-
warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in
|
|
72
|
+
warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 5.0.');
|
|
76
73
|
}
|
|
77
74
|
if (achievementsConfig && externalEngine) {
|
|
78
75
|
throw new Error('Cannot provide both "achievements" and "engine" props to AchievementProvider.\n\n' +
|
|
@@ -98,15 +95,9 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
|
|
|
98
95
|
eventMapping,
|
|
99
96
|
});
|
|
100
97
|
});
|
|
101
|
-
const [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}));
|
|
105
|
-
const syncAchievementState = React.useCallback(() => {
|
|
106
|
-
setAchievementState({
|
|
107
|
-
unlocked: [...engine.getUnlocked()],
|
|
108
|
-
all: getAllAchievementRecord(engine),
|
|
109
|
-
});
|
|
98
|
+
const [achievementSnapshot, setAchievementSnapshot] = React.useState(() => engine.getSnapshot());
|
|
99
|
+
const syncAchievementState = React.useCallback((snapshot) => {
|
|
100
|
+
setAchievementSnapshot(snapshot || engine.getSnapshot());
|
|
110
101
|
}, [engine]);
|
|
111
102
|
React.useEffect(() => {
|
|
112
103
|
return () => {
|
|
@@ -116,10 +107,17 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
|
|
|
116
107
|
};
|
|
117
108
|
}, [engine, externalEngine]);
|
|
118
109
|
React.useEffect(() => {
|
|
119
|
-
|
|
120
|
-
const unsubscribeStateChanged = engine.on('state:changed',
|
|
110
|
+
let isMounted = true;
|
|
111
|
+
const unsubscribeStateChanged = engine.on('state:changed', (event) => {
|
|
112
|
+
syncAchievementState(event);
|
|
113
|
+
});
|
|
114
|
+
engine.ready().then(() => {
|
|
115
|
+
if (isMounted) {
|
|
116
|
+
syncAchievementState();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
121
119
|
return () => {
|
|
122
|
-
|
|
120
|
+
isMounted = false;
|
|
123
121
|
unsubscribeStateChanged();
|
|
124
122
|
};
|
|
125
123
|
}, [engine, syncAchievementState]);
|
|
@@ -128,18 +126,12 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
|
|
|
128
126
|
};
|
|
129
127
|
const reset = () => {
|
|
130
128
|
engine.reset();
|
|
131
|
-
syncAchievementState();
|
|
132
129
|
};
|
|
133
130
|
const getState = () => {
|
|
134
|
-
const
|
|
135
|
-
const unlocked = engine.getUnlocked();
|
|
136
|
-
const metricsInArrayFormat = {};
|
|
137
|
-
Object.entries(metrics).forEach(([key, value]) => {
|
|
138
|
-
metricsInArrayFormat[key] = Array.isArray(value) ? value : [value];
|
|
139
|
-
});
|
|
131
|
+
const snapshot = engine.getSnapshot();
|
|
140
132
|
return {
|
|
141
|
-
metrics:
|
|
142
|
-
unlocked:
|
|
133
|
+
metrics: snapshot.metrics,
|
|
134
|
+
unlocked: snapshot.unlockedIds,
|
|
143
135
|
};
|
|
144
136
|
};
|
|
145
137
|
const exportData = () => {
|
|
@@ -151,11 +143,16 @@ const AchievementProvider$1 = ({ achievements: achievementsConfig, storage = 'lo
|
|
|
151
143
|
return result;
|
|
152
144
|
};
|
|
153
145
|
const getAllAchievements = () => {
|
|
154
|
-
return engine.
|
|
146
|
+
return engine.getSnapshot().allAchievements;
|
|
147
|
+
};
|
|
148
|
+
const achievements = {
|
|
149
|
+
unlocked: achievementSnapshot.unlockedIds,
|
|
150
|
+
all: getAllAchievementRecord(achievementSnapshot.allAchievements),
|
|
155
151
|
};
|
|
156
152
|
return (React.createElement(AchievementContext.Provider, { value: {
|
|
157
153
|
update,
|
|
158
|
-
achievements
|
|
154
|
+
achievements,
|
|
155
|
+
snapshot: achievementSnapshot,
|
|
159
156
|
reset,
|
|
160
157
|
getState,
|
|
161
158
|
exportData,
|
|
@@ -607,7 +604,7 @@ const AchievementEffects = ({ icons, ui }) => {
|
|
|
607
604
|
const AchievementProvider = (_a) => {
|
|
608
605
|
var { children, icons = {}, ui = {}, useBuiltInUI } = _a, providerProps = __rest(_a, ["children", "icons", "ui", "useBuiltInUI"]);
|
|
609
606
|
if (useBuiltInUI !== undefined) {
|
|
610
|
-
warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in
|
|
607
|
+
warnDeprecation('`useBuiltInUI` is deprecated and is now a no-op because built-in UI is the default. It will be removed in 5.0.');
|
|
611
608
|
}
|
|
612
609
|
const uiContextValue = React.useMemo(() => ({ icons, ui }), [icons, ui]);
|
|
613
610
|
return (React.createElement(AchievementUIContext.Provider, { value: uiContextValue },
|
|
@@ -625,18 +622,14 @@ const useAchievements = () => {
|
|
|
625
622
|
};
|
|
626
623
|
|
|
627
624
|
const useAchievementState = () => {
|
|
628
|
-
const {
|
|
629
|
-
const allAchievements = getAllAchievements();
|
|
630
|
-
const unlockedIds = achievements.unlocked;
|
|
631
|
-
const unlockedAchievementSet = new Set(unlockedIds);
|
|
632
|
-
const unlockedAchievements = allAchievements.filter((achievement) => unlockedAchievementSet.has(achievement.achievementId));
|
|
625
|
+
const { snapshot } = useAchievements();
|
|
633
626
|
return {
|
|
634
|
-
unlockedIds,
|
|
635
|
-
unlockedAchievements,
|
|
636
|
-
allAchievements,
|
|
637
|
-
unlockedCount:
|
|
638
|
-
totalCount:
|
|
639
|
-
metrics:
|
|
627
|
+
unlockedIds: snapshot.unlockedIds,
|
|
628
|
+
unlockedAchievements: snapshot.unlockedAchievements,
|
|
629
|
+
allAchievements: snapshot.allAchievements,
|
|
630
|
+
unlockedCount: snapshot.unlockedCount,
|
|
631
|
+
totalCount: snapshot.totalCount,
|
|
632
|
+
metrics: snapshot.metrics,
|
|
640
633
|
};
|
|
641
634
|
};
|
|
642
635
|
|
|
@@ -777,13 +770,98 @@ const defaultStyles = {
|
|
|
777
770
|
},
|
|
778
771
|
};
|
|
779
772
|
|
|
773
|
+
const isNumericString = (value) => /^-?\d+(\.\d+)?$/.test(value);
|
|
774
|
+
const getBackdropBlurFilter = (backdropBlur) => {
|
|
775
|
+
if (backdropBlur === undefined) {
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
if (typeof backdropBlur === 'number') {
|
|
779
|
+
if (backdropBlur <= 0) {
|
|
780
|
+
return undefined;
|
|
781
|
+
}
|
|
782
|
+
return `blur(${backdropBlur}px)`;
|
|
783
|
+
}
|
|
784
|
+
const trimmedBlur = backdropBlur.trim();
|
|
785
|
+
if (!trimmedBlur ||
|
|
786
|
+
trimmedBlur === '0' ||
|
|
787
|
+
trimmedBlur === '0px' ||
|
|
788
|
+
trimmedBlur === 'none') {
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
const blurValue = isNumericString(trimmedBlur) ? `${trimmedBlur}px` : trimmedBlur;
|
|
792
|
+
return blurValue.startsWith('blur(') ? blurValue : `blur(${blurValue})`;
|
|
793
|
+
};
|
|
794
|
+
const getBackdropBlurStyles = (backdropBlur) => {
|
|
795
|
+
const backdropFilter = getBackdropBlurFilter(backdropBlur);
|
|
796
|
+
if (!backdropFilter) {
|
|
797
|
+
return {};
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
backdropFilter,
|
|
801
|
+
WebkitBackdropFilter: backdropFilter,
|
|
802
|
+
};
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
const compactAchievementStyles = {
|
|
806
|
+
achievementList: {
|
|
807
|
+
display: 'grid',
|
|
808
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
|
|
809
|
+
gap: '10px',
|
|
810
|
+
},
|
|
811
|
+
achievementItem: {
|
|
812
|
+
display: 'flex',
|
|
813
|
+
flexDirection: 'column',
|
|
814
|
+
alignItems: 'center',
|
|
815
|
+
justifyContent: 'center',
|
|
816
|
+
gap: '6px',
|
|
817
|
+
padding: '12px 10px',
|
|
818
|
+
borderRadius: '8px',
|
|
819
|
+
aspectRatio: '1 / 1',
|
|
820
|
+
minHeight: '120px',
|
|
821
|
+
textAlign: 'center',
|
|
822
|
+
},
|
|
823
|
+
lockedAchievementItem: {
|
|
824
|
+
display: 'flex',
|
|
825
|
+
flexDirection: 'column',
|
|
826
|
+
alignItems: 'center',
|
|
827
|
+
justifyContent: 'center',
|
|
828
|
+
gap: '6px',
|
|
829
|
+
padding: '12px 10px',
|
|
830
|
+
borderRadius: '8px',
|
|
831
|
+
aspectRatio: '1 / 1',
|
|
832
|
+
minHeight: '120px',
|
|
833
|
+
textAlign: 'center',
|
|
834
|
+
},
|
|
835
|
+
achievementIcon: {
|
|
836
|
+
fontSize: '34px',
|
|
837
|
+
lineHeight: 1,
|
|
838
|
+
flexShrink: 0,
|
|
839
|
+
},
|
|
840
|
+
achievementTitle: {
|
|
841
|
+
margin: '0',
|
|
842
|
+
fontSize: '13px',
|
|
843
|
+
lineHeight: 1.2,
|
|
844
|
+
},
|
|
845
|
+
achievementDescription: {
|
|
846
|
+
margin: '0',
|
|
847
|
+
fontSize: '11px',
|
|
848
|
+
lineHeight: 1.25,
|
|
849
|
+
},
|
|
850
|
+
lockIcon: {
|
|
851
|
+
fontSize: '15px',
|
|
852
|
+
top: '8px',
|
|
853
|
+
right: '8px',
|
|
854
|
+
transform: 'none',
|
|
855
|
+
},
|
|
856
|
+
};
|
|
857
|
+
const getDensityStyles = (density) => (density === 'compact' ? compactAchievementStyles : {});
|
|
780
858
|
const resolveIcon = (achievement, icons) => {
|
|
781
859
|
return ((achievement.achievementIconKey && icons[achievement.achievementIconKey]) ||
|
|
782
860
|
achievement.achievementIconKey ||
|
|
783
861
|
icons.default ||
|
|
784
862
|
'⭐');
|
|
785
863
|
};
|
|
786
|
-
const AchievementsList = ({ achievements, showLocked = true, showUnlockConditions = false, icons = {}, styles = {}, emptyState, className, renderAchievement, }) => {
|
|
864
|
+
const AchievementsList = ({ achievements, showLocked = true, showUnlockConditions = false, icons = {}, styles = {}, emptyState, className, density = 'comfortable', renderAchievement, }) => {
|
|
787
865
|
const context = React.useContext(AchievementContext);
|
|
788
866
|
const uiContext = React.useContext(AchievementUIContext);
|
|
789
867
|
const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
|
|
@@ -794,36 +872,37 @@ const AchievementsList = ({ achievements, showLocked = true, showUnlockCondition
|
|
|
794
872
|
const achievementsToDisplay = showLocked
|
|
795
873
|
? sourceAchievements
|
|
796
874
|
: sourceAchievements.filter((achievement) => achievement.isUnlocked);
|
|
875
|
+
const densityStyles = getDensityStyles(density);
|
|
797
876
|
if (achievementsToDisplay.length === 0) {
|
|
798
877
|
return (React.createElement("p", { style: { textAlign: 'center', color: '#666' } }, emptyState || 'No achievements configured.'));
|
|
799
878
|
}
|
|
800
|
-
return (React.createElement("div", { className: className, style: Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementList), styles === null || styles === void 0 ? void 0 : styles.achievementList), "data-testid": "achievements-list" }, achievementsToDisplay.map((achievement, index) => {
|
|
879
|
+
return (React.createElement("div", { className: className, style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementList), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementList), styles === null || styles === void 0 ? void 0 : styles.achievementList), "data-density": density, "data-testid": "achievements-list" }, achievementsToDisplay.map((achievement, index) => {
|
|
801
880
|
const isLocked = !achievement.isUnlocked;
|
|
802
881
|
const icon = resolveIcon(achievement, mergedIcons);
|
|
803
882
|
if (renderAchievement) {
|
|
804
|
-
return (React.createElement(React.Fragment, { key: achievement.achievementId }, renderAchievement({ achievement, isLocked, icon, index })));
|
|
883
|
+
return (React.createElement(React.Fragment, { key: achievement.achievementId }, renderAchievement({ achievement, isLocked, icon, index, density })));
|
|
805
884
|
}
|
|
806
885
|
return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign(Object.assign({}, (isLocked
|
|
807
|
-
? Object.assign(Object.assign({}, defaultStyles.badgesModal.lockedAchievementItem), styles === null || styles === void 0 ? void 0 : styles.lockedAchievementItem) : defaultStyles.badgesModal.achievementItem)), styles === null || styles === void 0 ? void 0 : styles.achievementItem), { position: 'relative' }), "data-testid": "achievement-list-item", "data-unlocked": achievement.isUnlocked ? 'true' : 'false' },
|
|
808
|
-
React.createElement("div", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementIcon), styles === null || styles === void 0 ? void 0 : styles.achievementIcon), { opacity: isLocked ? 0.4 : 1 }) }, icon),
|
|
809
|
-
React.createElement("div", { style: { flex: 1 } },
|
|
810
|
-
React.createElement("h3", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementTitle), styles === null || styles === void 0 ? void 0 : styles.achievementTitle), { color: isLocked ? '#999' : undefined }) }, achievement.achievementTitle),
|
|
811
|
-
achievement.achievementDescription && (React.createElement("p", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementDescription), styles === null || styles === void 0 ? void 0 : styles.achievementDescription), { color: isLocked ? '#aaa' : '#666' }) },
|
|
886
|
+
? Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.lockedAchievementItem), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.lockedAchievementItem), styles === null || styles === void 0 ? void 0 : styles.lockedAchievementItem) : Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementItem), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementItem))), styles === null || styles === void 0 ? void 0 : styles.achievementItem), { position: 'relative' }), "data-testid": "achievement-list-item", "data-unlocked": achievement.isUnlocked ? 'true' : 'false' },
|
|
887
|
+
React.createElement("div", { style: Object.assign(Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementIcon), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementIcon), styles === null || styles === void 0 ? void 0 : styles.achievementIcon), { opacity: isLocked ? 0.4 : 1 }) }, icon),
|
|
888
|
+
React.createElement("div", { style: density === 'compact' ? { width: '100%', minWidth: 0 } : { flex: 1 } },
|
|
889
|
+
React.createElement("h3", { style: Object.assign(Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementTitle), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementTitle), styles === null || styles === void 0 ? void 0 : styles.achievementTitle), { color: isLocked ? '#999' : undefined }) }, achievement.achievementTitle),
|
|
890
|
+
achievement.achievementDescription && (React.createElement("p", { style: Object.assign(Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.achievementDescription), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.achievementDescription), styles === null || styles === void 0 ? void 0 : styles.achievementDescription), { color: isLocked ? '#aaa' : '#666' }) },
|
|
812
891
|
achievement.achievementDescription,
|
|
813
892
|
showUnlockConditions && isLocked && (React.createElement("span", { style: {
|
|
814
893
|
display: 'block',
|
|
815
|
-
fontSize: '12px',
|
|
816
|
-
marginTop: '4px',
|
|
894
|
+
fontSize: density === 'compact' ? '11px' : '12px',
|
|
895
|
+
marginTop: density === 'compact' ? '2px' : '4px',
|
|
817
896
|
fontStyle: 'italic',
|
|
818
897
|
color: '#888',
|
|
819
898
|
} },
|
|
820
899
|
"\uD83D\uDD13 ",
|
|
821
900
|
achievement.achievementDescription))))),
|
|
822
|
-
isLocked && (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.lockIcon), styles === null || styles === void 0 ? void 0 : styles.lockIcon) }, "\uD83D\uDD12"))));
|
|
901
|
+
isLocked && (React.createElement("div", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.lockIcon), densityStyles === null || densityStyles === void 0 ? void 0 : densityStyles.lockIcon), styles === null || styles === void 0 ? void 0 : styles.lockIcon) }, "\uD83D\uDD12"))));
|
|
823
902
|
})));
|
|
824
903
|
};
|
|
825
904
|
|
|
826
|
-
const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achievements', styles = {}, icons = {}, showLocked = true, showUnlockConditions = false, emptyState, renderAchievement, theme, }) => {
|
|
905
|
+
const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achievements', styles = {}, icons = {}, showLocked = true, showUnlockConditions = false, emptyState, renderAchievement, theme, hideScrollbar = false, density = 'comfortable', backdropBlur, }) => {
|
|
827
906
|
const context = React.useContext(AchievementContext);
|
|
828
907
|
const uiContext = React.useContext(AchievementUIContext);
|
|
829
908
|
React.useEffect(() => {
|
|
@@ -851,19 +930,30 @@ const AchievementsModal = ({ isOpen, onClose, achievements, title = '🏆 Achiev
|
|
|
851
930
|
? sourceAchievements
|
|
852
931
|
: sourceAchievements === null || sourceAchievements === void 0 ? void 0 : sourceAchievements.filter((achievement) => achievement.isUnlocked);
|
|
853
932
|
const resolvedTheme = theme || uiContext.ui.theme || 'modern';
|
|
933
|
+
const backdropBlurFilter = getBackdropBlurFilter(backdropBlur);
|
|
854
934
|
const mergedIcons = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultAchievementIcons), context === null || context === void 0 ? void 0 : context.icons), uiContext.icons), icons);
|
|
855
935
|
if (CustomModal) {
|
|
856
936
|
if (!modalAchievements) {
|
|
857
937
|
throw new Error('AchievementsModal requires either an achievements prop or an AchievementProvider parent.');
|
|
858
938
|
}
|
|
859
|
-
return (React.createElement(CustomModal, { isOpen: isOpen, onClose: onClose, achievements: modalAchievements, icons: mergedIcons, theme: resolvedTheme }));
|
|
939
|
+
return (React.createElement(CustomModal, { isOpen: isOpen, onClose: onClose, achievements: modalAchievements, icons: mergedIcons, theme: resolvedTheme, hideScrollbar: hideScrollbar, density: density, backdropBlur: backdropBlur }));
|
|
860
940
|
}
|
|
861
|
-
return (React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.overlay), styles === null || styles === void 0 ? void 0 : styles.overlay), role: "presentation", onClick: onClose, "data-testid": "achievements-modal-overlay" },
|
|
862
|
-
React.createElement("
|
|
941
|
+
return (React.createElement("div", { style: Object.assign(Object.assign(Object.assign({}, defaultStyles.badgesModal.overlay), styles === null || styles === void 0 ? void 0 : styles.overlay), getBackdropBlurStyles(backdropBlur)), role: "presentation", onClick: onClose, "data-backdrop-blur": backdropBlurFilter, "data-testid": "achievements-modal-overlay" },
|
|
942
|
+
hideScrollbar && (React.createElement("style", null, `
|
|
943
|
+
[data-react-achievements-modal-content][data-hide-scrollbar="true"] {
|
|
944
|
+
scrollbar-width: none;
|
|
945
|
+
-ms-overflow-style: none;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
[data-react-achievements-modal-content][data-hide-scrollbar="true"]::-webkit-scrollbar {
|
|
949
|
+
display: none;
|
|
950
|
+
}
|
|
951
|
+
`)),
|
|
952
|
+
React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.content), styles === null || styles === void 0 ? void 0 : styles.content), role: "dialog", "aria-modal": "true", "aria-labelledby": "achievements-modal-title", onClick: (event) => event.stopPropagation(), "data-hide-scrollbar": hideScrollbar ? 'true' : undefined, "data-density": density, "data-react-achievements-modal-content": true, "data-testid": "achievements-modal" },
|
|
863
953
|
React.createElement("div", { style: Object.assign(Object.assign({}, defaultStyles.badgesModal.header), styles === null || styles === void 0 ? void 0 : styles.header) },
|
|
864
954
|
React.createElement("h2", { id: "achievements-modal-title", style: { margin: 0 } }, title),
|
|
865
955
|
React.createElement("button", { onClick: onClose, style: Object.assign(Object.assign({}, defaultStyles.badgesModal.closeButton), styles === null || styles === void 0 ? void 0 : styles.closeButton), "aria-label": "Close" }, "\u00D7")),
|
|
866
|
-
React.createElement(AchievementsList, { achievements: modalAchievements, showLocked: showLocked, showUnlockConditions: showUnlockConditions, icons: icons, styles: styles, emptyState: emptyState, renderAchievement: renderAchievement }))));
|
|
956
|
+
React.createElement(AchievementsList, { achievements: modalAchievements, showLocked: showLocked, showUnlockConditions: showUnlockConditions, icons: icons, styles: styles, emptyState: emptyState, renderAchievement: renderAchievement, density: density }))));
|
|
867
957
|
};
|
|
868
958
|
|
|
869
959
|
const getPositionStyles$1 = (position) => {
|
|
@@ -883,7 +973,7 @@ const getPositionStyles$1 = (position) => {
|
|
|
883
973
|
return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
|
|
884
974
|
}
|
|
885
975
|
};
|
|
886
|
-
const AchievementsWidget = ({ position = 'bottom-right', placement = 'fixed', showAllAchievements = true, showUnlockConditions = false, showCount = true, icons, theme, label = 'Achievements', icon = '🏆', triggerClassName, renderTrigger, buttonStyles, modalStyles, modalTitle, emptyState, renderAchievement, }) => {
|
|
976
|
+
const AchievementsWidget = ({ position = 'bottom-right', placement = 'fixed', showAllAchievements = true, showUnlockConditions = false, showCount = true, icons, theme, density = 'comfortable', label = 'Achievements', icon = '🏆', triggerClassName, renderTrigger, buttonStyles, modalStyles, modalTitle, emptyState, renderAchievement, hideModalScrollbar = false, modalBackdropBlur, }) => {
|
|
887
977
|
const uiContext = React.useContext(AchievementUIContext);
|
|
888
978
|
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
|
889
979
|
const { unlockedAchievements, allAchievements, unlockedCount, totalCount } = useAchievementState();
|
|
@@ -923,7 +1013,7 @@ const AchievementsWidget = ({ position = 'bottom-right', placement = 'fixed', sh
|
|
|
923
1013
|
React.createElement("span", null, icon),
|
|
924
1014
|
React.createElement("span", { style: { flex: 1 } }, label),
|
|
925
1015
|
showCount && React.createElement("span", null, unlockedCount))),
|
|
926
|
-
React.createElement(AchievementsModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: modalAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles, title: modalTitle, emptyState: emptyState, renderAchievement: renderAchievement, theme: resolvedTheme })));
|
|
1016
|
+
React.createElement(AchievementsModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), achievements: modalAchievements, showUnlockConditions: showUnlockConditions, icons: icons, styles: modalStyles, title: modalTitle, emptyState: emptyState, renderAchievement: renderAchievement, theme: resolvedTheme, hideScrollbar: hideModalScrollbar, density: density, backdropBlur: modalBackdropBlur })));
|
|
927
1017
|
};
|
|
928
1018
|
|
|
929
1019
|
const getPositionStyles = (position) => {
|
|
@@ -945,11 +1035,11 @@ const getPositionStyles = (position) => {
|
|
|
945
1035
|
};
|
|
946
1036
|
/**
|
|
947
1037
|
* @deprecated Use `AchievementsWidget` for new integrations. This v3
|
|
948
|
-
* compatibility wrapper will be removed in
|
|
1038
|
+
* compatibility wrapper will be removed in 5.0.
|
|
949
1039
|
*/
|
|
950
1040
|
const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
|
|
951
1041
|
React.useEffect(() => {
|
|
952
|
-
warnDeprecation('`BadgesButton` is deprecated. Use `AchievementsWidget` instead. `BadgesButton` will be removed in
|
|
1042
|
+
warnDeprecation('`BadgesButton` is deprecated. Use `AchievementsWidget` instead. `BadgesButton` will be removed in 5.0.');
|
|
953
1043
|
}, []);
|
|
954
1044
|
// Get theme configuration for consistent styling
|
|
955
1045
|
const themeConfig = getTheme(theme) || builtInThemes.modern;
|
|
@@ -986,11 +1076,11 @@ const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed',
|
|
|
986
1076
|
/**
|
|
987
1077
|
* @deprecated Use `AchievementsModal`, `AchievementsWidget`, or
|
|
988
1078
|
* `AchievementsList` for new integrations. This v3 compatibility wrapper will
|
|
989
|
-
* be removed in
|
|
1079
|
+
* be removed in 5.0.
|
|
990
1080
|
*/
|
|
991
1081
|
const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, showAllAchievements = false, showUnlockConditions = false, allAchievements, }) => {
|
|
992
1082
|
React.useEffect(() => {
|
|
993
|
-
warnDeprecation('`BadgesModal` is deprecated. Use `AchievementsWidget` or `AchievementsList` instead. `BadgesModal` will be removed in
|
|
1083
|
+
warnDeprecation('`BadgesModal` is deprecated. Use `AchievementsWidget` or `AchievementsList` instead. `BadgesModal` will be removed in 5.0.');
|
|
994
1084
|
}, []);
|
|
995
1085
|
const achievementsToDisplay = showAllAchievements && allAchievements
|
|
996
1086
|
? allAchievements
|
|
@@ -1000,7 +1090,7 @@ const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, s
|
|
|
1000
1090
|
|
|
1001
1091
|
/**
|
|
1002
1092
|
* @deprecated Use `AchievementsWidget` for new integrations. This v3
|
|
1003
|
-
* compatibility wrapper will be removed in
|
|
1093
|
+
* compatibility wrapper will be removed in 5.0.
|
|
1004
1094
|
*/
|
|
1005
1095
|
const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right', placement = 'fixed', showAllAchievements = false, allAchievements, showUnlockConditions = false, icons, theme = 'modern', buttonStyles, modalStyles, }) => {
|
|
1006
1096
|
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
|
@@ -1011,11 +1101,11 @@ const BadgesButtonWithModal = ({ unlockedAchievements, position = 'bottom-right'
|
|
|
1011
1101
|
|
|
1012
1102
|
/**
|
|
1013
1103
|
* @deprecated Use the provider `ui.ConfettiComponent` option or the built-in
|
|
1014
|
-
* confetti default. This v3 compatibility wrapper will be removed in
|
|
1104
|
+
* confetti default. This v3 compatibility wrapper will be removed in 5.0.
|
|
1015
1105
|
*/
|
|
1016
1106
|
const ConfettiWrapper = ({ show }) => {
|
|
1017
1107
|
React.useEffect(() => {
|
|
1018
|
-
warnDeprecation('`ConfettiWrapper` is deprecated. Use the provider `ui.ConfettiComponent` option or built-in confetti defaults instead. `ConfettiWrapper` will be removed in
|
|
1108
|
+
warnDeprecation('`ConfettiWrapper` is deprecated. Use the provider `ui.ConfettiComponent` option or built-in confetti defaults instead. `ConfettiWrapper` will be removed in 5.0.');
|
|
1019
1109
|
}, []);
|
|
1020
1110
|
return React.createElement(BuiltInConfetti, { show: show, particleCount: 200 });
|
|
1021
1111
|
};
|
|
@@ -1050,17 +1140,11 @@ const LevelProgress = ({ level, currentXP, nextLevelXP, label = 'Level', valueLa
|
|
|
1050
1140
|
* Provides the v4 happy path for direct metric updates plus explicit state names.
|
|
1051
1141
|
*/
|
|
1052
1142
|
const useSimpleAchievements = () => {
|
|
1053
|
-
const { update, reset, getState, exportData, importData } = useAchievements();
|
|
1143
|
+
const { update, reset, getState, exportData, importData, engine } = useAchievements();
|
|
1054
1144
|
const achievementState = useAchievementState();
|
|
1055
1145
|
const track = (metric, value) => update({ [metric]: value });
|
|
1056
1146
|
const increment = (metric, amount = 1) => {
|
|
1057
|
-
|
|
1058
|
-
const currentMetricArray = currentState.metrics[metric] || [0];
|
|
1059
|
-
const currentValue = Array.isArray(currentMetricArray)
|
|
1060
|
-
? currentMetricArray[0]
|
|
1061
|
-
: currentMetricArray;
|
|
1062
|
-
const newValue = (typeof currentValue === 'number' ? currentValue : 0) + amount;
|
|
1063
|
-
update({ [metric]: newValue });
|
|
1147
|
+
return engine.increment(metric, amount);
|
|
1064
1148
|
};
|
|
1065
1149
|
const trackMultiple = (metrics) => update(metrics);
|
|
1066
1150
|
return {
|
|
@@ -1079,11 +1163,11 @@ const useSimpleAchievements = () => {
|
|
|
1079
1163
|
importData,
|
|
1080
1164
|
getAllAchievements: () => achievementState.allAchievements,
|
|
1081
1165
|
/**
|
|
1082
|
-
* @deprecated Use `unlockedIds` instead. This alias will be removed in
|
|
1166
|
+
* @deprecated Use `unlockedIds` instead. This alias will be removed in 5.0.
|
|
1083
1167
|
*/
|
|
1084
1168
|
unlocked: achievementState.unlockedIds,
|
|
1085
1169
|
/**
|
|
1086
|
-
* @deprecated Use `allAchievements` instead. This alias will be removed in
|
|
1170
|
+
* @deprecated Use `allAchievements` instead. This alias will be removed in 5.0.
|
|
1087
1171
|
*/
|
|
1088
1172
|
all: achievementState.allAchievements,
|
|
1089
1173
|
};
|
|
@@ -1093,12 +1177,14 @@ const useSimpleAchievements = () => {
|
|
|
1093
1177
|
* Built-in modal component
|
|
1094
1178
|
* Modern, theme-aware achievement modal with smooth animations
|
|
1095
1179
|
*/
|
|
1096
|
-
const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', }) => {
|
|
1180
|
+
const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', hideScrollbar = false, density = 'comfortable', backdropBlur, }) => {
|
|
1097
1181
|
// Merge custom icons with defaults
|
|
1098
1182
|
const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
|
|
1099
1183
|
// Get theme configuration
|
|
1100
1184
|
const themeConfig = getTheme(theme) || builtInThemes.modern;
|
|
1101
1185
|
const { modal: themeStyles } = themeConfig;
|
|
1186
|
+
const isCompact = density === 'compact';
|
|
1187
|
+
const backdropBlurFilter = getBackdropBlurFilter(backdropBlur);
|
|
1102
1188
|
React.useEffect(() => {
|
|
1103
1189
|
if (isOpen) {
|
|
1104
1190
|
// Lock body scroll when modal is open
|
|
@@ -1114,24 +1200,12 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
|
|
|
1114
1200
|
}, [isOpen]);
|
|
1115
1201
|
if (!isOpen)
|
|
1116
1202
|
return null;
|
|
1117
|
-
const overlayStyles = {
|
|
1118
|
-
position: 'fixed',
|
|
1119
|
-
top: 0,
|
|
1120
|
-
left: 0,
|
|
1121
|
-
right: 0,
|
|
1122
|
-
bottom: 0,
|
|
1123
|
-
backgroundColor: themeStyles.overlayColor,
|
|
1124
|
-
display: 'flex',
|
|
1125
|
-
alignItems: 'center',
|
|
1126
|
-
justifyContent: 'center',
|
|
1127
|
-
zIndex: 10000,
|
|
1128
|
-
animation: 'fadeIn 0.3s ease-in-out',
|
|
1129
|
-
};
|
|
1203
|
+
const overlayStyles = Object.assign({ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: themeStyles.overlayColor, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000, animation: 'fadeIn 0.3s ease-in-out' }, getBackdropBlurStyles(backdropBlur));
|
|
1130
1204
|
const modalStyles = {
|
|
1131
1205
|
background: themeStyles.background,
|
|
1132
1206
|
borderRadius: themeStyles.borderRadius,
|
|
1133
|
-
padding: '32px',
|
|
1134
|
-
maxWidth: '600px',
|
|
1207
|
+
padding: isCompact ? '18px' : '32px',
|
|
1208
|
+
maxWidth: isCompact ? '520px' : '600px',
|
|
1135
1209
|
width: '90%',
|
|
1136
1210
|
maxHeight: '80vh',
|
|
1137
1211
|
overflow: 'auto',
|
|
@@ -1143,18 +1217,18 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
|
|
|
1143
1217
|
display: 'flex',
|
|
1144
1218
|
justifyContent: 'space-between',
|
|
1145
1219
|
alignItems: 'center',
|
|
1146
|
-
marginBottom: '24px',
|
|
1220
|
+
marginBottom: isCompact ? '16px' : '24px',
|
|
1147
1221
|
};
|
|
1148
1222
|
const titleStyles = {
|
|
1149
1223
|
margin: 0,
|
|
1150
1224
|
color: themeStyles.textColor,
|
|
1151
|
-
fontSize: themeStyles.headerFontSize || '28px',
|
|
1225
|
+
fontSize: isCompact ? '22px' : themeStyles.headerFontSize || '28px',
|
|
1152
1226
|
fontWeight: 'bold',
|
|
1153
1227
|
};
|
|
1154
1228
|
const closeButtonStyles = {
|
|
1155
1229
|
background: 'none',
|
|
1156
1230
|
border: 'none',
|
|
1157
|
-
fontSize: '32px',
|
|
1231
|
+
fontSize: isCompact ? '26px' : '32px',
|
|
1158
1232
|
cursor: 'pointer',
|
|
1159
1233
|
color: themeStyles.textColor,
|
|
1160
1234
|
opacity: 0.6,
|
|
@@ -1162,17 +1236,17 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
|
|
|
1162
1236
|
padding: 0,
|
|
1163
1237
|
lineHeight: 1,
|
|
1164
1238
|
};
|
|
1165
|
-
const isBadgeLayout = themeStyles.achievementLayout === 'badge';
|
|
1239
|
+
const isBadgeLayout = isCompact || themeStyles.achievementLayout === 'badge';
|
|
1166
1240
|
const listStyles = isBadgeLayout
|
|
1167
1241
|
? {
|
|
1168
1242
|
display: 'grid',
|
|
1169
|
-
gridTemplateColumns:
|
|
1170
|
-
gap: '16px',
|
|
1243
|
+
gridTemplateColumns: `repeat(auto-fill, minmax(${isCompact ? '112px' : '140px'}, 1fr))`,
|
|
1244
|
+
gap: isCompact ? '10px' : '16px',
|
|
1171
1245
|
}
|
|
1172
1246
|
: {
|
|
1173
1247
|
display: 'flex',
|
|
1174
1248
|
flexDirection: 'column',
|
|
1175
|
-
gap: '12px',
|
|
1249
|
+
gap: isCompact ? '8px' : '12px',
|
|
1176
1250
|
};
|
|
1177
1251
|
const getAchievementItemStyles = (isUnlocked) => {
|
|
1178
1252
|
const baseStyles = {
|
|
@@ -1186,24 +1260,24 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
|
|
|
1186
1260
|
};
|
|
1187
1261
|
if (isBadgeLayout) {
|
|
1188
1262
|
// Badge layout: vertical, centered, square-ish
|
|
1189
|
-
return Object.assign(Object.assign({}, baseStyles), { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: '20px 12px', aspectRatio: '1 / 1.1', minHeight: '160px' });
|
|
1263
|
+
return Object.assign(Object.assign({}, baseStyles), { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: isCompact ? '12px 8px' : '20px 12px', aspectRatio: '1 / 1.1', minHeight: isCompact ? '118px' : '160px' });
|
|
1190
1264
|
}
|
|
1191
1265
|
else {
|
|
1192
1266
|
// Horizontal layout (default)
|
|
1193
|
-
return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: '16px', padding: '16px' });
|
|
1267
|
+
return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: isCompact ? '10px' : '16px', padding: isCompact ? '10px 12px' : '16px' });
|
|
1194
1268
|
}
|
|
1195
1269
|
};
|
|
1196
1270
|
const getIconContainerStyles = (isUnlocked) => {
|
|
1197
1271
|
if (isBadgeLayout) {
|
|
1198
1272
|
return {
|
|
1199
|
-
fontSize: '48px',
|
|
1273
|
+
fontSize: isCompact ? '32px' : '48px',
|
|
1200
1274
|
lineHeight: 1,
|
|
1201
|
-
marginBottom: '8px',
|
|
1275
|
+
marginBottom: isCompact ? '6px' : '8px',
|
|
1202
1276
|
opacity: isUnlocked ? 1 : 0.3,
|
|
1203
1277
|
};
|
|
1204
1278
|
}
|
|
1205
1279
|
return {
|
|
1206
|
-
fontSize: '40px',
|
|
1280
|
+
fontSize: isCompact ? '28px' : '40px',
|
|
1207
1281
|
flexShrink: 0,
|
|
1208
1282
|
lineHeight: 1,
|
|
1209
1283
|
opacity: isUnlocked ? 1 : 0.3,
|
|
@@ -1221,14 +1295,14 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
|
|
|
1221
1295
|
? {
|
|
1222
1296
|
margin: '0 0 4px 0',
|
|
1223
1297
|
color: themeStyles.textColor,
|
|
1224
|
-
fontSize: '14px',
|
|
1298
|
+
fontSize: isCompact ? '12px' : '14px',
|
|
1225
1299
|
fontWeight: 'bold',
|
|
1226
1300
|
lineHeight: '1.3',
|
|
1227
1301
|
}
|
|
1228
1302
|
: {
|
|
1229
|
-
margin: '0 0 8px 0',
|
|
1303
|
+
margin: isCompact ? '0 0 4px 0' : '0 0 8px 0',
|
|
1230
1304
|
color: themeStyles.textColor,
|
|
1231
|
-
fontSize: '18px',
|
|
1305
|
+
fontSize: isCompact ? '14px' : '18px',
|
|
1232
1306
|
fontWeight: 'bold',
|
|
1233
1307
|
overflow: 'hidden',
|
|
1234
1308
|
textOverflow: 'ellipsis',
|
|
@@ -1239,27 +1313,27 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
|
|
|
1239
1313
|
margin: 0,
|
|
1240
1314
|
color: themeStyles.textColor,
|
|
1241
1315
|
opacity: 0.7,
|
|
1242
|
-
fontSize: '11px',
|
|
1316
|
+
fontSize: isCompact ? '10px' : '11px',
|
|
1243
1317
|
lineHeight: '1.3',
|
|
1244
1318
|
}
|
|
1245
1319
|
: {
|
|
1246
1320
|
margin: 0,
|
|
1247
1321
|
color: themeStyles.textColor,
|
|
1248
1322
|
opacity: 0.8,
|
|
1249
|
-
fontSize: '14px',
|
|
1323
|
+
fontSize: isCompact ? '12px' : '14px',
|
|
1250
1324
|
};
|
|
1251
1325
|
const getLockIconStyles = () => {
|
|
1252
1326
|
if (isBadgeLayout) {
|
|
1253
1327
|
return {
|
|
1254
1328
|
position: 'absolute',
|
|
1255
|
-
top: '8px',
|
|
1256
|
-
right: '8px',
|
|
1257
|
-
fontSize: '18px',
|
|
1329
|
+
top: isCompact ? '6px' : '8px',
|
|
1330
|
+
right: isCompact ? '6px' : '8px',
|
|
1331
|
+
fontSize: isCompact ? '14px' : '18px',
|
|
1258
1332
|
opacity: 0.6,
|
|
1259
1333
|
};
|
|
1260
1334
|
}
|
|
1261
1335
|
return {
|
|
1262
|
-
fontSize: '24px',
|
|
1336
|
+
fontSize: isCompact ? '18px' : '24px',
|
|
1263
1337
|
flexShrink: 0,
|
|
1264
1338
|
opacity: 0.5,
|
|
1265
1339
|
};
|
|
@@ -1280,9 +1354,18 @@ const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'mode
|
|
|
1280
1354
|
opacity: 1;
|
|
1281
1355
|
}
|
|
1282
1356
|
}
|
|
1357
|
+
|
|
1358
|
+
[data-react-achievements-built-in-modal][data-hide-scrollbar="true"] {
|
|
1359
|
+
scrollbar-width: none;
|
|
1360
|
+
-ms-overflow-style: none;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
[data-react-achievements-built-in-modal][data-hide-scrollbar="true"]::-webkit-scrollbar {
|
|
1364
|
+
display: none;
|
|
1365
|
+
}
|
|
1283
1366
|
`),
|
|
1284
|
-
React.createElement("div", { style: overlayStyles, onClick: onClose, "data-testid": "built-in-modal-overlay" },
|
|
1285
|
-
React.createElement("div", { style: modalStyles, onClick: (e) => e.stopPropagation(), "data-testid": "built-in-modal" },
|
|
1367
|
+
React.createElement("div", { style: overlayStyles, onClick: onClose, "data-backdrop-blur": backdropBlurFilter, "data-testid": "built-in-modal-overlay" },
|
|
1368
|
+
React.createElement("div", { style: modalStyles, onClick: (e) => e.stopPropagation(), "data-hide-scrollbar": hideScrollbar ? 'true' : undefined, "data-density": density, "data-react-achievements-built-in-modal": true, "data-testid": "built-in-modal" },
|
|
1286
1369
|
React.createElement("div", { style: headerStyles },
|
|
1287
1370
|
React.createElement("h2", { style: titleStyles }, "\uD83C\uDFC6 Achievements"),
|
|
1288
1371
|
React.createElement("button", { onClick: onClose, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close modal" }, "\u00D7")),
|