chore: migrate all FA icons to Lucide (@lucide/svelte)

- Replaced all active FontAwesome <span class="fas fa-*"> icons with
  Lucide components across 145 files (excluding /idaa/ which is intentional)
- Fixed merge script bug: consolidated lucide-svelte imports into @lucide/svelte
- Replaced dynamic toggle patterns (fa-toggle-on/off) with ToggleRight/ToggleLeft
- Replaced fa-eye/fa-eye-slash with Eye/EyeOff
- Replaced fa-bug/fa-bug-slash with Bug/BugOff
- Replaced fa-sync fa-spin with RefreshCw + animate-spin
- Replaced fa-microchip with Cpu
- Fixed {@const} placement in element_manage_event_file_li.svelte
- Removed obsolete CSS hover rules for .unlock_icon/.lock_icon
- svelte-check: 0 errors, 0 warnings
This commit is contained in:
Scott Idem
2026-03-16 18:07:43 -04:00
parent c9050264a5
commit b543c8a930
147 changed files with 587 additions and 754 deletions

View File

@@ -0,0 +1,142 @@
/**
* src/lib/pwa/pwa_install.svelte.ts
*
* Global PWA install prompt state using Svelte 5 universal reactivity.
*
* Usage:
* - Call `pwa_install.init()` once in the root +layout.svelte (browser $effect).
* - Import `pwa_install` in any component to read state and trigger the prompt.
*
* Platform behaviour:
* - Chrome / Android / Desktop Chrome: intercepts `beforeinstallprompt`,
* lets us show a custom button that calls the native install flow.
* - iOS Safari: `beforeinstallprompt` never fires; we detect iOS + not-standalone
* and show manual "Share → Add to Home Screen" instructions instead.
* - If already running in standalone mode (i.e. already installed) → hide everything.
*
* Dismiss: persists for DISMISS_DAYS days via localStorage key `ae_pwa_install_dismissed`.
*/
import { browser } from '$app/environment';
// `BeforeInstallPromptEvent` is non-standard and not included in TypeScript's default lib.
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
}
const DISMISS_KEY = 'ae_pwa_install_dismissed';
const DISMISS_DAYS = 7;
// --- Module-level reactive state (Svelte 5 universal reactivity) ---
let _deferred_prompt = $state<BeforeInstallPromptEvent | null>(null);
let _is_installed = $state(false);
let _is_dismissed = $state(false);
let _initialized = false; // plain boolean — not reactive, guards against double-init
// --- Private helpers (SSR-safe) ---
function _in_standalone(): boolean {
if (!browser) return false;
return (
window.matchMedia('(display-mode: standalone)').matches ||
// iOS Safari sets navigator.standalone (non-standard)
('standalone' in window.navigator && (window.navigator as any).standalone === true)
);
}
function _is_ios(): boolean {
if (!browser) return false;
// Cover iPhone/iPod/iPad (incl. iPadOS 13+ which reports MacIntel with touch points)
return (
/iphone|ipad|ipod/i.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
);
}
// --- Public API ---
export const pwa_install = {
/**
* True if the browser has captured a deferred install prompt
* (Chrome / Android / PWA-capable desktop browsers).
*/
get can_prompt(): boolean {
return _deferred_prompt !== null && !_is_installed && !_is_dismissed;
},
/**
* True if on iOS Safari and NOT already installed.
* We cannot trigger a native prompt on iOS so we show manual instructions instead.
*/
get is_ios_nudge(): boolean {
return _is_ios() && !_in_standalone() && !_is_dismissed && !_is_installed;
},
/** True if the install UI should be rendered at all. */
get should_show(): boolean {
return pwa_install.can_prompt || pwa_install.is_ios_nudge;
},
/** True once the app is confirmed installed (standalone mode or appinstalled event). */
get is_installed(): boolean {
return _is_installed;
},
/**
* Register browser event listeners. Call ONCE from root +layout.svelte.
* Safe to call multiple times (idempotent).
*/
init(): void {
if (!browser || _initialized) return;
_initialized = true;
// Already running as installed PWA — nothing to prompt.
if (_in_standalone()) {
_is_installed = true;
return;
}
// Restore dismiss state from localStorage (with expiry check).
const raw = localStorage.getItem(DISMISS_KEY);
if (raw) {
const days_since = (Date.now() - parseInt(raw, 10)) / 86_400_000;
if (days_since < DISMISS_DAYS) {
_is_dismissed = true;
} else {
// Expired — clear so the nudge can reappear.
localStorage.removeItem(DISMISS_KEY);
}
}
// Capture the deferred install prompt (Chrome/Android/Desktop Chrome).
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
_deferred_prompt = e as BeforeInstallPromptEvent;
});
// Fired by the browser after the user accepts the install prompt.
window.addEventListener('appinstalled', () => {
_is_installed = true;
_deferred_prompt = null;
});
},
/** Trigger the native install prompt. No-op if no deferred prompt is available. */
async prompt(): Promise<void> {
if (!_deferred_prompt) return;
// Grab and clear to prevent double-fire.
const evt = _deferred_prompt;
_deferred_prompt = null;
await evt.prompt();
const { outcome } = await evt.userChoice;
if (outcome === 'accepted') {
_is_installed = true;
}
},
/** Dismiss the nudge; it will not re-appear for DISMISS_DAYS days. */
dismiss(): void {
_is_dismissed = true;
if (browser) localStorage.setItem(DISMISS_KEY, Date.now().toString());
},
};