feat(settings): badge pulse animation with settings toggle (#676)

* feat: animate panel count badge on new data arrival

* feat(settings): gate badge pulse animation behind toggle (off by default)

PR #671 adds CSS pulse on panel count badges. This commit gates it
behind a new `badgeAnimation` setting in UnifiedSettings so users
opt-in. Adds i18n keys for all 18 locales.

---------

Co-authored-by: jeronlxj <jeronliaw@u.nus.edu>
This commit is contained in:
Elie Habib
2026-03-01 19:21:36 +04:00
committed by GitHub
parent 98150d639d
commit 45b00e9d7d
23 changed files with 83 additions and 1 deletions

1
package-lock.json generated
View File

@@ -6382,7 +6382,6 @@
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"

View File

@@ -3,6 +3,7 @@ import { invokeTauri } from '../services/tauri-bridge';
import { t } from '../services/i18n';
import { h, replaceChildren, safeHtml } from '../utils/dom-utils';
import { trackPanelResized } from '@/services/analytics';
import { getAiFlowSettings } from '@/services/ai-flow-settings';
export interface PanelOptions {
id: string;
@@ -661,7 +662,13 @@ export class Panel {
public setCount(count: number): void {
if (this.countEl) {
const prev = parseInt(this.countEl.textContent ?? '0', 10);
this.countEl.textContent = count.toString();
if (count > prev && getAiFlowSettings().badgeAnimation) {
this.countEl.classList.remove('bump');
void this.countEl.offsetWidth;
this.countEl.classList.add('bump');
}
}
}

View File

@@ -172,6 +172,8 @@ export class UnifiedSettings {
this.updateAiStatus();
} else if (target.id === 'us-map-flash') {
setAiFlowSetting('mapNewsFlash', target.checked);
} else if (target.id === 'us-badge-anim') {
setAiFlowSetting('badgeAnimation', target.checked);
}
});
@@ -288,6 +290,10 @@ export class UnifiedSettings {
html += `<div class="ai-flow-section-label">${t('components.insights.sectionMap')}</div>`;
html += this.toggleRowHtml('us-map-flash', t('components.insights.mapFlashLabel'), t('components.insights.mapFlashDesc'), settings.mapNewsFlash);
// Panels section
html += `<div class="ai-flow-section-label">${t('components.insights.sectionPanels')}</div>`;
html += this.toggleRowHtml('us-badge-anim', t('components.insights.badgeAnimLabel'), t('components.insights.badgeAnimDesc'), settings.badgeAnimation);
// AI Analysis section (web-only)
if (!this.config.isDesktopApp) {
html += `<div class="ai-flow-section-label">${t('components.insights.sectionAi')}</div>`;

View File

@@ -910,6 +910,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Cloud AI (Groq & OpenRouter)",
"aiFlowCloudDesc": "إرسال العناوين إلى السحابة لتلخيص الذكاء الاصطناعي (موصى به)",

View File

@@ -1176,6 +1176,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Cloud-KI (Groq & OpenRouter)",
"aiFlowCloudDesc": "Schlagzeilen zur KI-Zusammenfassung an die Cloud senden (empfohlen)",

View File

@@ -949,6 +949,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Cloud AI (Groq & OpenRouter)",
"aiFlowCloudDesc": "Αποστολή τίτλων στο cloud για σύνοψη AI (συνιστάται)",

View File

@@ -981,6 +981,9 @@
"streamQualityDesc": "Set quality for all live streams (lower saves bandwidth)",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Cloud AI (Groq & OpenRouter)",
"aiFlowCloudDesc": "Send headlines to cloud for AI summarization (recommended)",

View File

@@ -1176,6 +1176,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "IA en la nube (Groq & OpenRouter)",
"aiFlowCloudDesc": "Enviar titulares a la nube para resumen con IA (recomendado)",

View File

@@ -910,6 +910,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "IA Cloud (Groq & OpenRouter)",
"aiFlowCloudDesc": "Envoyer les titres au cloud pour le résumé IA (recommandé)",

View File

@@ -1176,6 +1176,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "IA Cloud (Groq & OpenRouter)",
"aiFlowCloudDesc": "Invia i titoli al cloud per il riepilogo IA (consigliato)",

View File

@@ -910,6 +910,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "クラウドAIGroq & OpenRouter",
"aiFlowCloudDesc": "見出しをクラウドに送信してAI要約推奨",

View File

@@ -967,6 +967,9 @@
"streamQualityDesc": "모든 라이브 스트림의 품질 설정 (낮추면 대역폭 절약)",
"mapFlashLabel": "실시간 이벤트 펄스",
"mapFlashDesc": "속보 수신 시 지도에서 해당 위치를 깜박임",
"sectionPanels": "패널",
"badgeAnimLabel": "카운트 배지 펄스",
"badgeAnimDesc": "새 항목이 도착하면 패널 카운트 배지에 애니메이션 적용",
"aiFlowTitle": "설정",
"aiFlowCloudLabel": "클라우드 AI (Groq 및 OpenRouter)",
"aiFlowCloudDesc": "헤드라인을 클라우드로 전송하여 AI 요약 (권장)",

View File

@@ -979,6 +979,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Cloud AI (Groq & OpenRouter)",
"aiFlowCloudDesc": "Koppen naar de cloud sturen voor AI-samenvatting (aanbevolen)",

View File

@@ -1176,6 +1176,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Cloud AI (Groq & OpenRouter)",
"aiFlowCloudDesc": "Wyślij nagłówki do chmury w celu podsumowania AI (zalecane)",

View File

@@ -979,6 +979,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "IA na nuvem (Groq & OpenRouter)",
"aiFlowCloudDesc": "Enviar manchetes para a nuvem para resumo IA (recomendado)",

View File

@@ -910,6 +910,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Облачный ИИ (Groq & OpenRouter)",
"aiFlowCloudDesc": "Отправлять заголовки в облако для ИИ-суммирования (рекомендуется)",

View File

@@ -979,6 +979,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Moln-AI (Groq & OpenRouter)",
"aiFlowCloudDesc": "Skicka rubriker till molnet för AI-sammanfattning (rekommenderat)",

View File

@@ -910,6 +910,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Cloud AI (Groq & OpenRouter)",
"aiFlowCloudDesc": "ส่งพาดหัวข่าวไปยังคลาวด์เพื่อสรุปด้วย AI (แนะนำ)",

View File

@@ -910,6 +910,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Bulut AI (Groq & OpenRouter)",
"aiFlowCloudDesc": "Basliklari AI ozetleme icin buluta gonder (onerilen)",

View File

@@ -910,6 +910,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "Cloud AI (Groq & OpenRouter)",
"aiFlowCloudDesc": "Gửi tiêu đề tới cloud để AI tóm tắt (khuyến nghị)",

View File

@@ -910,6 +910,9 @@
"sectionAi": "AI Analysis",
"mapFlashLabel": "Live Event Pulse",
"mapFlashDesc": "Flash locations on the map when breaking news arrives",
"sectionPanels": "Panels",
"badgeAnimLabel": "Count Badge Pulse",
"badgeAnimDesc": "Animate panel count badges when new items arrive",
"aiFlowTitle": "Settings",
"aiFlowCloudLabel": "云端AIGroq & OpenRouter",
"aiFlowCloudDesc": "将标题发送到云端进行AI摘要推荐",

View File

@@ -9,6 +9,7 @@
const STORAGE_KEY_BROWSER_MODEL = 'wm-ai-flow-browser-model';
const STORAGE_KEY_CLOUD_LLM = 'wm-ai-flow-cloud-llm';
const STORAGE_KEY_MAP_NEWS_FLASH = 'wm-map-news-flash';
const STORAGE_KEY_BADGE_ANIMATION = 'wm-badge-animation';
const STORAGE_KEY_STREAM_QUALITY = 'wm-stream-quality';
const EVENT_NAME = 'ai-flow-changed';
const STREAM_QUALITY_EVENT = 'stream-quality-changed';
@@ -17,6 +18,7 @@ export interface AiFlowSettings {
browserModel: boolean;
cloudLlm: boolean;
mapNewsFlash: boolean;
badgeAnimation: boolean;
}
function readBool(key: string, defaultValue: boolean): boolean {
@@ -41,12 +43,14 @@ const STORAGE_KEY_MAP: Record<keyof AiFlowSettings, string> = {
browserModel: STORAGE_KEY_BROWSER_MODEL,
cloudLlm: STORAGE_KEY_CLOUD_LLM,
mapNewsFlash: STORAGE_KEY_MAP_NEWS_FLASH,
badgeAnimation: STORAGE_KEY_BADGE_ANIMATION,
};
const DEFAULTS: AiFlowSettings = {
browserModel: false,
cloudLlm: true,
mapNewsFlash: true,
badgeAnimation: false,
};
export function getAiFlowSettings(): AiFlowSettings {
@@ -54,6 +58,7 @@ export function getAiFlowSettings(): AiFlowSettings {
browserModel: readBool(STORAGE_KEY_BROWSER_MODEL, DEFAULTS.browserModel),
cloudLlm: readBool(STORAGE_KEY_CLOUD_LLM, DEFAULTS.cloudLlm),
mapNewsFlash: readBool(STORAGE_KEY_MAP_NEWS_FLASH, DEFAULTS.mapNewsFlash),
badgeAnimation: readBool(STORAGE_KEY_BADGE_ANIMATION, DEFAULTS.badgeAnimation),
};
}

View File

@@ -1061,6 +1061,17 @@ body.panel-resize-active iframe {
background: var(--border);
padding: 2px 6px;
border-radius: 2px;
transition: color 0.3s ease, background 0.3s ease;
}
.panel-count.bump {
animation: count-bump 0.5s ease-out;
}
@keyframes count-bump {
0% { transform: scale(1); background: var(--border); color: var(--text-dim); }
40% { transform: scale(1.3); background: var(--accent); color: var(--bg); }
100% { transform: scale(1); background: var(--border); color: var(--text-dim); }
}