/** * Every browser and platform fact this plugin reads, in one seam. * * Nothing here owns state or decides anything: the window's visibility, the * notification permission, the notification icon, and the one platform hint the * settings copy uses. Each read is contained, because a page a plugin does not * control may hide any of them — and because a contained read is what lets the * delivery path keep going when the answer is unreadable. * * @module @dsh-plugin/session-notify/client/platform */ /** * The window's Notification constructor, or undefined when this environment has * none (a plain Node test, or a browser without the API). * @returns the constructor, or undefined. */ export function notificationApi() { const Ctor = globalThis.Notification return typeof Ctor === 'function' ? Ctor : undefined } /** Permission lookup that never throws in a browser without the API. */ export function notificationPermission() { const Ctor = notificationApi() if (Ctor === undefined) return 'unsupported' const permission = Ctor.permission if (permission === 'granted' || permission === 'denied' || permission === 'default') return permission return 'default' } /** * Whether the Harness window is somewhere the user cannot see the app. * * Two independent browser facts are consulted, and ANY of them counts as "not * in the foreground": the page's visibility and the document's focus. They are * read at delivery time rather than remembered from an event, because a missed * blur in the desktop shell made this plugin believe the window was in front * and swallow the alert. * @returns whether the alert must go to the system notification channel. */ export function windowIsAway() { try { if (document.visibilityState === 'hidden') return true } catch { /* an unreadable visibility state leaves the focus fact */ } try { if (typeof document.hasFocus === 'function') return !document.hasFocus() } catch { /* an unreadable focus state leaves the default below */ } return false } /** Read the window's live focus state rather than a cached value. */ export function currentFocus() { return !windowIsAway() } /** Focus the window without letting a refusal stop the navigation. */ export function focusWindow() { try { globalThis.focus?.() } catch { /* focus is best effort on every platform */ } } /** * The system-notification icon, built once when this bundle materializes: this * deployment's own static URL when the page serves this package's assets, else * the SVG inline. Icons are cosmetic on every platform, so each step is * contained — and materialization happens on first import, not when the bundle * script runs, so a page that never activates the plugin never gets here. * @type {string | undefined} an icon URL, or undefined when neither form works. */ export const systemIcon = (() => { try { const url = new URL('./icon.svg', document.baseURI).href if (url !== '') return url } catch { /* fall through to the inline form */ } try { const svg = '' return `data:image/svg+xml,${svg}` } catch { return undefined } })() /** One best-effort platform hint, used for settings help text only. */ export function platformKey() { try { const agent = navigator.userAgentData const platform = String((agent?.platform ?? navigator.platform) || navigator.userAgent || '') if (/win/i.test(platform)) return 'win' if (/mac|iphone|ipad/i.test(platform)) return 'mac' if (/linux|x11/i.test(platform)) return 'linux' } catch { /* an unreadable navigator keeps the neutral hint */ } return 'plain' }