import { Alpine } from "./alpine"; import { ALL_LIBRARIES, getSelectedLibrary, setSelectedLibrary } from "./storage"; import { showToast } from "./toast"; export function getCurrentLibraryId(): string { return getSelectedLibrary(); } export async function switchWithTransition( containerId: string, fetchFn: () => Promise, ): Promise { const container = document.getElementById(containerId); const loading = document.getElementById("loading-spinner"); if (!container || !loading) return; // View Transitions (native, where supported): the browser snapshots the // old shelves, fetchFn swaps the DOM, and the two snapshots crossfade — // no blank phase, no manual opacity choreography. The dim + spinner is // shown before the snapshot and hidden inside the update, so the dim // blends away as part of the crossfade. Browsers without the API fall // through to the dim + fade sequence below. const doc = document as Document & { startViewTransition?: (updateCallback: () => void | Promise) => { finished: Promise; }; }; if (doc.startViewTransition) { try { loading.classList.remove("hidden"); const transition = doc.startViewTransition(async () => { await fetchFn(); loading.classList.add("hidden"); }); await transition.finished; } catch (error) { loading.classList.add("hidden"); showToast("Failed to load library", "error"); console.error("Switch library error:", error); } return; } try { container.classList.add("opacity-0", "transition-opacity", "duration-150"); await new Promise((resolve) => setTimeout(resolve, 150)); loading.classList.remove("hidden"); await fetchFn(); // Lift the dim BEFORE the fade-in starts, so the new content's fade // plays fully visible instead of being hard-cut by the overlay // disappearing mid-transition. loading.classList.add("hidden"); container.classList.remove("duration-150"); container.classList.add("duration-300"); void container.offsetHeight; container.classList.remove("opacity-0"); setTimeout(() => { container.classList.remove("transition-opacity", "duration-300"); }, 300); } catch (error) { container.classList.remove("opacity-0", "transition-opacity", "duration-150", "duration-300"); showToast("Failed to load library", "error"); console.error("Switch library error:", error); } finally { loading.classList.add("hidden"); } } export interface LibrarySwitcherOptions { // Async switchers (SSR navigation awaits nothing, API-backed switchers // await their fetch) — hence void | Promise. onSwitch: (libraryId: string) => void | Promise; } export function initLibrarySwitcher(options: LibrarySwitcherOptions): void { const librarySelect = document.getElementById("library-select") as HTMLSelectElement; if (!librarySelect) return; const stored = localStorage.getItem("selectedLibrary"); if (stored === ALL_LIBRARIES) { librarySelect.value = ""; } else if (stored) { const option = librarySelect.querySelector(`option[value="${stored}"]`); if (option) { librarySelect.value = stored; } } librarySelect.addEventListener("change", async (e) => { const target = e.target as HTMLSelectElement; const libraryId = target.value; setSelectedLibrary(libraryId); await options.onSwitch(libraryId); }); } Alpine.data("librarySwitcher", () => ({}));