diff --git a/src/app/country-intel.ts b/src/app/country-intel.ts index fc31f0852d..5e59734767 100644 --- a/src/app/country-intel.ts +++ b/src/app/country-intel.ts @@ -9,6 +9,7 @@ import type { } from '@/components/CountryBriefPanel'; import { CountryDeepDivePanel } from '@/components/CountryDeepDivePanel'; import { reverseGeocode } from '@/utils/reverse-geocode'; +import { showToast } from '@/utils/toast'; import { getCountryAtCoordinates, getCountryCentroid, @@ -862,7 +863,7 @@ export class CountryIntelManager implements AppModule { openCountryStory(code: string, name: string): void { if (!dataFreshness.hasSufficientData() || this.ctx.latestClusters.length === 0) { - this.showToast('Data still loading — try again in a moment'); + showToast('Data still loading — try again in a moment'); return; } const posturePanel = this.ctx.panels['strategic-posture'] as StrategicPosturePanel | undefined; @@ -879,16 +880,6 @@ export class CountryIntelManager implements AppModule { openStoryModal(data); } - showToast(msg: string): void { - document.querySelector('.toast-notification')?.remove(); - const el = document.createElement('div'); - el.className = 'toast-notification'; - el.textContent = msg; - document.body.appendChild(el); - requestAnimationFrame(() => el.classList.add('visible')); - setTimeout(() => { el.classList.remove('visible'); setTimeout(() => el.remove(), 300); }, 3000); - } - private getCountryStrikes(code: string, hasGeoShape: boolean): typeof this.ctx.intelligenceCache.iranEvents & object { if (!this.ctx.intelligenceCache.iranEvents) return []; const seen = new Set(); diff --git a/src/app/event-handlers.ts b/src/app/event-handlers.ts index 1c60a0a9cc..42a88f96c2 100644 --- a/src/app/event-handlers.ts +++ b/src/app/event-handlers.ts @@ -1082,16 +1082,6 @@ export class EventHandlerManager implements AppModule { } } - showToast(msg: string): void { - document.querySelector('.toast-notification')?.remove(); - const el = document.createElement('div'); - el.className = 'toast-notification'; - el.textContent = msg; - document.body.appendChild(el); - requestAnimationFrame(() => el.classList.add('visible')); - setTimeout(() => { el.classList.remove('visible'); setTimeout(() => el.remove(), 300); }, 3000); - } - shouldShowIntelligenceNotifications(): boolean { return !this.ctx.isMobile && !!this.ctx.findingsBadge?.isPopupEnabled(); } diff --git a/src/components/UnifiedSettings.ts b/src/components/UnifiedSettings.ts index 23edf8ce5e..c7377ee67f 100644 --- a/src/components/UnifiedSettings.ts +++ b/src/components/UnifiedSettings.ts @@ -4,6 +4,7 @@ import { SITE_VARIANT } from '@/config/variant'; import { t } from '@/services/i18n'; import type { MapProvider } from '@/config/basemap'; import { escapeHtml } from '@/utils/sanitize'; +import { showToast } from '@/utils/toast'; import type { PanelConfig } from '@/types'; import { renderPreferences } from '@/services/preferences-content'; @@ -200,6 +201,7 @@ export class UnifiedSettings { const prefs = renderPreferences({ isDesktopApp: this.config.isDesktopApp, onMapProviderChange: this.config.onMapProviderChange, + onSettingSaved: () => showToast(t('modals.settingsWindow.saved')), }); this.overlay.innerHTML = ` diff --git a/src/services/preferences-content.ts b/src/services/preferences-content.ts index 5af7199829..842efaf693 100644 --- a/src/services/preferences-content.ts +++ b/src/services/preferences-content.ts @@ -15,6 +15,8 @@ const DESKTOP_RELEASES_URL = 'https://github.com/koala73/worldmonitor/releases'; export interface PreferencesHost { isDesktopApp: boolean; onMapProviderChange?: (provider: MapProvider) => void; + /** Called when a preference is saved (for visual feedback e.g. toast). Not called for import/export. */ + onSettingSaved?: () => void; } export interface PreferencesResult { @@ -279,44 +281,29 @@ export function renderPreferences(host: PreferencesHost): PreferencesResult { if (target.id === 'us-stream-quality') { setStreamQuality(target.value as StreamQuality); - return; - } - if (target.id === 'us-globe-visual-preset') { + } else if (target.id === 'us-globe-visual-preset') { setGlobeVisualPreset(target.value as GlobeVisualPreset); - return; - } - if (target.id === 'us-theme') { + } else if (target.id === 'us-theme') { setThemePreference(target.value as ThemePreference); - return; - } - if (target.id === 'us-font-family') { + } else if (target.id === 'us-font-family') { setFontFamily(target.value as FontFamily); - return; + } else if (target.id === 'us-map-provider') { } - if (target.id === 'us-map-provider') { const provider = target.value as MapProvider; setMapProvider(provider); renderMapThemeDropdown(container, provider); host.onMapProviderChange?.(provider); window.dispatchEvent(new CustomEvent('map-theme-changed')); - return; - } - if (target.id === 'us-map-theme') { + } else if (target.id === 'us-map-theme') { const provider = getMapProvider(); setMapTheme(provider, target.value); window.dispatchEvent(new CustomEvent('map-theme-changed')); - return; - } - if (target.id === 'us-live-streams-always-on') { + } else if (target.id === 'us-live-streams-always-on') { setLiveStreamsAlwaysOn(target.checked); - return; - } - if (target.id === 'us-language') { + } else if (target.id === 'us-language') { trackLanguageChange(target.value); void changeLanguage(target.value); - return; - } - if (target.id === 'us-cloud') { + } else if (target.id === 'us-cloud') { setAiFlowSetting('cloudLlm', target.checked); updateAiStatus(container); } else if (target.id === 'us-browser') { @@ -330,7 +317,10 @@ export function renderPreferences(host: PreferencesHost): PreferencesResult { setAiFlowSetting('headlineMemory', target.checked); } else if (target.id === 'us-badge-anim') { setAiFlowSetting('badgeAnimation', target.checked); + } else { + return; // not a preference we persist (e.g. import input already returned above) } + host.onSettingSaved?.(); }, { signal }); container.addEventListener('click', (e) => { diff --git a/src/utils/toast.ts b/src/utils/toast.ts new file mode 100644 index 0000000000..cb0e003086 --- /dev/null +++ b/src/utils/toast.ts @@ -0,0 +1,17 @@ +/** + * Global toast notification (body-level). Use for short-lived feedback e.g. "Settings saved". + * For in-panel toasts with custom styling (e.g. import/export), use component-specific logic. + */ +export function showToast(msg: string): void { + document.querySelector('.toast-notification')?.remove(); + const el = document.createElement('div'); + el.className = 'toast-notification'; + el.setAttribute('role', 'status'); + el.textContent = msg; + document.body.appendChild(el); + requestAnimationFrame(() => el.classList.add('visible')); + setTimeout(() => { + el.classList.remove('visible'); + setTimeout(() => el.remove(), 300); + }, 3000); +}