diff --git a/achievement/index.html b/achievement/index.html
index b483442..f5705ba 100644
--- a/achievement/index.html
+++ b/achievement/index.html
@@ -222,6 +222,13 @@
border: 1px solid var(--achievement-card-border);
background: var(--achievement-card-purple);
}
+ .badge-card:focus,
+ .badge-card:focus-visible,
+ .badge-card:active {
+ outline: none;
+ box-shadow: none;
+ transform: none;
+ }
.badge-card::before {
content: '';
position: absolute;
@@ -300,20 +307,21 @@
.footer-text {
margin-top: 24px;
}
- .loading {
+ .achievement-loading {
position: absolute;
left: 0;
top: 320px;
z-index: 7;
- display: none;
width: 375px;
color: rgba(255, 255, 255, 0.72);
- font-size: 12px;
- line-height: 16px;
- text-align: center;
}
- .loading.active {
- display: block;
+ .achievement-loading .hy-loading__mascot {
+ width: 76px;
+ height: 76px;
+ }
+ .achievement-loading .hy-loading__spinner {
+ border-color: rgba(255, 255, 255, 0.22);
+ border-top-color: #ffffff;
}
.toast {
position: fixed;
@@ -504,21 +512,14 @@
>
-
Name
+
Earn 0 achievements
@@ -561,10 +562,17 @@
- Loading...
+
+
+
+ Loading...
+
@@ -641,73 +649,6 @@
function validTab(value) {
return tabs[value] ? value : 'honors';
}
- function isLocalOrigin() {
- var host = window.location.hostname;
- return (
- host === '127.0.0.1' ||
- host === 'localhost' ||
- host === '::1'
- );
- }
- function previewMode() {
- if (!isLocalOrigin()) return '';
- return String(
- new URLSearchParams(window.location.search).get(
- 'preview'
- ) || ''
- ).toLowerCase();
- }
- function previewAchievement(index, unlocked) {
- return {
- definition: {
- achievement_id: 'preview_' + index,
- title: 'Pirple Pearl',
- description:
- 'Complete the required achievement goal to collect this badge.',
- display_config_json: JSON.stringify({
- owner_count: 10,
- }),
- primary_badge_resource: {
- name: 'Pirple Pearl',
- preview_url:
- ASSET +
- (unlocked
- ? 'badge-gold.png'
- : 'badge-silver.png'),
- },
- },
- user_status: unlocked ? 'unlocked' : 'in_progress',
- unlocked_at_ms: unlocked
- ? new Date('2026-01-01T00:00:00Z').getTime()
- : 0,
- };
- }
- function applyLocalPreview() {
- var mode = previewMode();
- if (!mode) return false;
- var sample = [0, 1, 2, 3, 4, 5, 6, 7, 8].map(
- function (_, index) {
- return previewAchievement(
- index + 1,
- mode === 'unlocked' ||
- (mode !== 'locked' &&
- (index === 0 || index >= 3))
- );
- }
- );
- state.profile = {
- name: 'Name',
- avatar: ASSET + 'profile-avatar.png',
- };
- state.achievements.honors = mode === 'empty' ? [] : sample;
- state.achievements.event = [];
- state.loaded.honors = true;
- state.loaded.event = true;
- render();
- if (mode === 'locked') openModal(sample[1]);
- if (mode === 'unlocked') openModal(sample[0]);
- return true;
- }
function showToast(message) {
var toast = byId('toast');
toast.textContent = message;
@@ -759,7 +700,7 @@
profile.displayUserId ||
profile.user_id ||
profile.userId ||
- 'Name'
+ ''
);
}
function profileAvatar(profile) {
@@ -822,17 +763,24 @@
node.src = fallback;
}
};
+ if (!next) {
+ node.removeAttribute('src');
+ return;
+ }
if (node.getAttribute('src') !== next) node.src = next;
}
function renderStaticFrame(node, src) {
+ if (!src) {
+ node.innerHTML = '';
+ return;
+ }
node.innerHTML =
'
';
var img = node.querySelector('img');
img.onerror = function () {
img.onerror = null;
- if (src === ASSET + 'header-frame.png') return;
- state.profileFrameURL = ASSET + 'header-frame.png';
- renderStaticFrame(node, ASSET + 'header-frame.png');
+ state.profileFrameURL = '';
+ renderStaticFrame(node, '');
};
}
function isDynamicEffect(url) {
@@ -847,9 +795,7 @@
function renderProfileFrame(profile) {
var node = byId('profileFrame');
if (!node) return;
- var url =
- frameURL(profileFrameData(profile)) ||
- ASSET + 'header-frame.png';
+ var url = frameURL(profileFrameData(profile));
if (state.profileFrameURL === url) return;
state.profileFrameURL = url;
if (state.profileFramePlayer) {
@@ -863,8 +809,8 @@
waitForWindowLoad: false,
});
state.profileFramePlayer.play(url).catch(function () {
- state.profileFrameURL = ASSET + 'header-frame.png';
- renderStaticFrame(node, ASSET + 'header-frame.png');
+ state.profileFrameURL = '';
+ renderStaticFrame(node, '');
});
return;
}
@@ -892,8 +838,8 @@
resource.preview_url ||
resource.asset_url ||
resource.animation_url ||
- ASSET +
- (unlocked ? 'badge-gold.png' : 'badge-silver.png')
+ resource.url ||
+ ''
);
}
function isUnlocked(item) {
@@ -907,7 +853,7 @@
function titleOf(item) {
var resource = resourceOf(item);
var def = definitionOf(item);
- return def.title || resource.name || 'Pirple Pearl';
+ return def.title || resource.name || '';
}
function descriptionOf(item) {
var config = configOf(item);
@@ -997,11 +943,7 @@
function renderHeader() {
var profile = state.profile || {};
byId('userName').textContent = profileName(profile);
- setImage(
- byId('profileAvatar'),
- profileAvatar(profile),
- ASSET + 'profile-avatar.png'
- );
+ setImage(byId('profileAvatar'), profileAvatar(profile), '');
renderProfileFrame(profile);
byId('earnedText').innerHTML = earnedText(unlockedCount());
}
@@ -1017,9 +959,9 @@
'" aria-label="' +
escapeHTML(titleOf(item)) +
'">' +
- '
' +
+ (img
+ ? '
'
+ : '') +
'' +
escapeHTML(titleOf(item)) +
'
' +
@@ -1201,30 +1143,28 @@
}
function renderCollectors(item) {
var custom = collectorAvatarsOf(item);
- var avatars = custom.length
- ? custom
- : [
- ASSET + 'collector-1.png',
- ASSET + 'collector-2.png',
- ASSET + 'collector-3.png',
- ASSET + 'collector-4.png',
- ASSET + 'collector-5.png',
- ];
byId('collectorList').innerHTML =
- avatars
+ custom
.slice(0, 5)
.map(function (src) {
return (
'
'
);
})
- .join('') + '...
';
+ .join('') +
+ (custom.length
+ ? '...
'
+ : '');
}
function openModal(item) {
state.selected = item;
var unlocked = isUnlocked(item);
var resource = resourceOf(item);
- byId('modalBadge').src = resourceImage(resource, unlocked);
+ setImage(
+ byId('modalBadge'),
+ resourceImage(resource, unlocked),
+ ''
+ );
byId('modalBadge').classList.toggle('locked', !unlocked);
byId('modalTitle').textContent = titleOf(item);
byId('modalDesc').textContent = descriptionOf(item);
@@ -1295,6 +1235,7 @@
byId('badgeGrid').addEventListener('click', function (event) {
var target = event.target.closest('[data-index]');
if (!target) return;
+ target.blur();
var list = state.achievements[state.tab] || [];
var item = list[Number(target.dataset.index)];
if (item) openModal(item);
@@ -1314,7 +1255,6 @@
render();
window.HyAppBridge.ready({ page: 'achievement' });
- if (applyLocalPreview()) return;
loadTab(state.tab).finally(function () {
loadUserProfile();
loadCounts();
diff --git a/common/png/loading-cat.webp b/common/png/loading-cat.webp
new file mode 100644
index 0000000..bc69439
Binary files /dev/null and b/common/png/loading-cat.webp differ
diff --git a/common/theme.css b/common/theme.css
index 4661feb..8d369fe 100644
--- a/common/theme.css
+++ b/common/theme.css
@@ -103,3 +103,64 @@
--hy-theme-radius-pill: 0;
--hy-theme-radius-circle: 0;
}
+
+.hy-loading {
+ display: none;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ color: var(--hy-theme-muted);
+ text-align: center;
+ pointer-events: none;
+}
+
+.hy-loading.active,
+.hy-loading.is-active {
+ display: flex;
+}
+
+.hy-loading__mascot {
+ width: 72px;
+ height: 72px;
+ background: url('./png/loading-cat.webp') center / contain no-repeat;
+ animation: hy-loading-float 1.6s ease-in-out infinite;
+}
+
+.hy-loading__spinner {
+ width: 24px;
+ height: 24px;
+ border: 2px solid rgba(125, 87, 199, 0.18);
+ border-top-color: var(--hy-theme-button);
+ border-radius: 50%;
+ animation: hy-loading-spin 0.86s linear infinite;
+}
+
+.hy-loading__text {
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 16px;
+}
+
+@keyframes hy-loading-float {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-6px);
+ }
+}
+
+@keyframes hy-loading-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .hy-loading__mascot,
+ .hy-loading__spinner {
+ animation: none;
+ }
+}