/** * Mirrors UI state into the query string so a view can be linked and reloaded * exactly as it was: which day is open, which model is plotted, which variables * are compared. * * Writes use `replaceState` rather than `goto`, so mirroring state never adds a * history entry or re-runs a load - the back button still means "the page * before", not "the previous day I clicked". */ import { browser } from '$app/environment'; import { replaceState } from '$app/navigation'; export function syncSearchParams(url: URL, updates: Record): void { if (!browser) return; const next = new URL(url); for (const [key, value] of Object.entries(updates)) { if (value == null || value === '') next.searchParams.delete(key); else next.searchParams.set(key, value); } if (next.href === url.href) return; try { replaceState(next, {}); } catch { // A page whose state settles during mount can get here before the router // has taken over. The URL is cosmetic, so retry on the next frame rather // than letting it break the page. requestAnimationFrame(() => { try { replaceState(next, {}); } catch { /* give up: the view still works, it just isn't linkable yet */ } }); } } /** Reads a comma-separated list, dropping empties. */ export function readList(url: URL, key: string): string[] | null { const raw = url.searchParams.get(key); if (!raw) return null; const list = raw .split(',') .map((s) => s.trim()) .filter(Boolean); return list.length > 0 ? list : null; }