diff --git a/src/popup/index.html b/src/popup/index.html
index 9b96c6c..a21d04f 100644
--- a/src/popup/index.html
+++ b/src/popup/index.html
@@ -21,12 +21,29 @@
.header {
margin-bottom: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
}
h1 {
font-size: 18px;
color: #333;
- margin-bottom: 8px;
+ margin: 0;
+ flex: 1;
+ }
+
+ .sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
}
.platform-status {
@@ -106,43 +123,105 @@
color: #999;
}
- /* 语言选择器样式 */
- .language-selector {
- padding: 12px;
- background: white;
- border-radius: 8px;
- margin-bottom: 16px;
+ /* Header actions */
+ .header-actions {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
- .language-label {
- font-size: 12px;
- color: #666;
- margin-bottom: 6px;
- display: block;
+ .menu-container {
+ position: relative;
}
- .language-select {
- width: 100%;
- padding: 8px 12px;
- font-size: 14px;
- border: 1px solid #ddd;
+ .icon-button {
+ width: 32px;
+ height: 32px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid transparent;
border-radius: 6px;
- background: white;
- color: #333;
+ background: transparent;
+ color: #666;
cursor: pointer;
transition: all 0.2s;
}
- .language-select:hover {
- border-color: #1890ff;
+ .icon-button:hover {
+ background: rgba(24, 144, 255, 0.08);
+ border-color: rgba(24, 144, 255, 0.35);
+ color: #1890ff;
}
- .language-select:focus {
+ .icon-button:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
}
+ .icon {
+ width: 18px;
+ height: 18px;
+ display: block;
+ }
+
+ .menu {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ min-width: 160px;
+ padding: 6px;
+ background: white;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
+ z-index: 1000;
+ }
+
+ .menu-item {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 10px;
+ border: none;
+ border-radius: 6px;
+ background: transparent;
+ color: #333;
+ font-size: 13px;
+ cursor: pointer;
+ transition: background 0.2s, color 0.2s;
+ }
+
+ .menu-item::after {
+ content: '';
+ width: 16px;
+ text-align: right;
+ color: #1890ff;
+ font-size: 12px;
+ line-height: 1;
+ }
+
+ .menu-item:hover {
+ background: #f5f5f5;
+ }
+
+ .menu-item:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
+ }
+
+ .menu-item[data-selected='true'] {
+ background: rgba(24, 144, 255, 0.08);
+ color: #1890ff;
+ }
+
+ .menu-item[data-selected='true']::after {
+ content: '✓';
+ }
+
/* Toast提示样式 */
.toast {
position: fixed;
@@ -283,16 +362,24 @@
-
-
-
-
-
+
diff --git a/src/popup/popup.ts b/src/popup/popup.ts
index dcf1f4d..ab3c9f9 100644
--- a/src/popup/popup.ts
+++ b/src/popup/popup.ts
@@ -4,7 +4,8 @@ import { PlatformName } from '../types/platform';
import { PlatformUsageStats, STORAGE_KEYS } from '../types/stats';
import { APP_VERSION_WITH_PREFIX } from '../shared/version';
import { I18nService } from '../utils/i18n';
-import { LanguageChangeMessage } from '../types/i18n';
+import { LANGUAGE_DISPLAY_NAMES } from '../types/i18n';
+import type { LanguageChangeMessage, SupportedLanguage } from '../types/i18n';
import { detectPlatformFromUrl } from '../utils/platform-detector';
/**
@@ -46,6 +47,38 @@ function localizeHTML() {
});
}
+function updateLanguageMenuSelection(language: SupportedLanguage) {
+ const languageMenu = document.getElementById('language-menu');
+ if (!languageMenu) return;
+
+ languageMenu.querySelectorAll('[data-lang]').forEach((item) => {
+ const isSelected = item.dataset.lang === language;
+ item.setAttribute('aria-checked', isSelected ? 'true' : 'false');
+ item.dataset.selected = isSelected ? 'true' : 'false';
+ });
+}
+
+function setLanguageMenuOpen(open: boolean, options?: { focusButton?: boolean }) {
+ const languageButton = document.getElementById('language-button') as HTMLButtonElement | null;
+ const languageMenu = document.getElementById('language-menu') as HTMLElement | null;
+ if (!languageButton || !languageMenu) return;
+
+ languageMenu.hidden = !open;
+ languageButton.setAttribute('aria-expanded', open ? 'true' : 'false');
+
+ if (open) {
+ const selectedItem =
+ languageMenu.querySelector('[data-selected="true"]') ||
+ languageMenu.querySelector('[data-lang]');
+ selectedItem?.focus();
+ return;
+ }
+
+ if (options?.focusButton) {
+ languageButton.focus();
+ }
+}
+
/**
* Update platform display name based on current tab
* Extracts duplicated logic and removes global state mutation
@@ -78,6 +111,16 @@ function handleLanguageChange(message: any) {
// Update all internationalized text when language changes
localizeHTML();
+ // Update language menu selection
+ const newLanguage = (message as LanguageChangeMessage).language;
+ updateLanguageMenuSelection(newLanguage);
+
+ // Keep tooltip up-to-date for icon-only button
+ const languageButton = document.getElementById('language-button') as HTMLButtonElement | null;
+ if (languageButton) {
+ languageButton.title = I18nService.t('popup_language_label');
+ }
+
// Update platform name with new translations
updatePlatformDisplayName().catch(error => {
logger.error('Failed to update platform name after language change:', error);
@@ -99,8 +142,8 @@ function setupLanguageChangeListener() {
// 初始化Popup
async function initPopup() {
- // 1. First initialize language selector (loads and sets currentLanguage asynchronously)
- await initLanguageSelector();
+ // 1. First initialize language menu (loads and sets currentLanguage asynchronously)
+ await initLanguageMenu();
// 2. After language initialization, localize HTML with correct language
localizeHTML();
@@ -315,42 +358,75 @@ function showToast(message: string, duration = 2000) {
}
/**
- * 初始化语言选择器
+ * 初始化语言菜单
*/
-async function initLanguageSelector() {
- const languageSelect = document.getElementById('language-select') as HTMLSelectElement;
- if (!languageSelect) return;
+async function initLanguageMenu() {
+ const languageButton = document.getElementById('language-button') as HTMLButtonElement | null;
+ const languageMenu = document.getElementById('language-menu') as HTMLElement | null;
+ if (!languageButton || !languageMenu) return;
+
+ const menuContainer = languageButton.closest('.menu-container');
+ if (!menuContainer) return;
// 获取当前语言并设置选中状态
const currentLang = await I18nService.getCurrentLanguage();
- languageSelect.value = currentLang;
+ updateLanguageMenuSelection(currentLang);
+
+ // Icon-only button: add localized tooltip
+ languageButton.title = I18nService.t('popup_language_label');
+
+ languageButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ setLanguageMenuOpen(languageMenu.hidden);
+ });
// 监听语言切换
- languageSelect.addEventListener('change', async (e) => {
- const newLang = (e.target as HTMLSelectElement).value as 'zh_CN' | 'zh_TW' | 'en';
+ languageMenu.querySelectorAll('[data-lang]').forEach((item) => {
+ item.addEventListener('click', async () => {
+ const newLang = item.dataset.lang as SupportedLanguage;
- try {
- // 设置新语言
- await I18nService.setLanguage(newLang);
+ // Close menu immediately for better UX
+ setLanguageMenuOpen(false, { focusButton: true });
- // 更新页面所有文本
- localizeHTML();
+ try {
+ // 设置新语言
+ await I18nService.setLanguage(newLang);
- // 显示Toast提示
- const langName = {
- 'zh_CN': '简体中文',
- 'zh_TW': '繁體中文',
- 'en': 'English'
- }[newLang];
+ // 更新页面所有文本
+ localizeHTML();
- showToast(I18nService.t('toast_language_changed', [langName]));
+ // 更新 tooltip
+ languageButton.title = I18nService.t('popup_language_label');
- // 更新平台名称翻译(使用提取的函数,消除重复代码)
- await updatePlatformDisplayName();
- } catch (error) {
- logger.error('Failed to change language:', error as Error);
- showToast(I18nService.t('toast_language_change_failed'));
- }
+ // 更新菜单选中状态
+ updateLanguageMenuSelection(newLang);
+
+ // 显示Toast提示
+ showToast(I18nService.t('toast_language_changed', [LANGUAGE_DISPLAY_NAMES[newLang]]));
+
+ // 更新平台名称翻译(使用提取的函数,消除重复代码)
+ await updatePlatformDisplayName();
+ } catch (error) {
+ logger.error('Failed to change language:', error as Error);
+ showToast(I18nService.t('toast_language_change_failed'));
+ }
+ });
+ });
+
+ // 点击其他区域关闭菜单
+ document.addEventListener('click', (e) => {
+ if (languageMenu.hidden) return;
+ const target = e.target as Node | null;
+ if (target && menuContainer.contains(target)) return;
+ setLanguageMenuOpen(false);
+ });
+
+ // ESC 关闭菜单
+ document.addEventListener('keydown', (e) => {
+ if (e.key !== 'Escape') return;
+ if (languageMenu.hidden) return;
+ e.preventDefault();
+ setLanguageMenuOpen(false, { focusButton: true });
});
}
diff --git a/src/shared/version.ts b/src/shared/version.ts
index fa34a61..3cd2f2e 100644
--- a/src/shared/version.ts
+++ b/src/shared/version.ts
@@ -10,7 +10,7 @@
* 1. Edit version in package.json
* 2. Run: npm run sync:version (or automatically via prebuild/predev)
*
- * Generated at: 2025-12-30T05:36:20.170Z
+ * Generated at: 2025-12-30T06:54:19.507Z
*/
/**