Live wall: saved views in the URL hash, per-viewer memory, kiosk mode (#234)

- liveWall.ts: WallLink carries layers (short stable keys), z/lat/lon,
  base, tourMs, kiosk and an f.<name> filter pass-through for #235;
  parse/serialise stay pure and round-trip; old inc/cam/tour links parse.
- MapView: overlays/basemap/viewport come from initialLayers/initialBase/
  initialView (defaults unchanged); onViewStateChange reports layer, base
  and region-view changes (debounced). Initially-checked overlays now start
  "shown" (react-leaflet adds a checked overlay before the control can fire
  overlayadd). Closing an incident flies back to the pre-drill view (#137).
- wallPrefs.ts: per-browser last layers/base/view in localStorage, every
  access wrapped; precedence hash > remembered > defaults.
- LiveView: one effect mirrors wall + map state into the hash; hash edits
  remount the map with the new view; Copy view link; configurable tour.
- Kiosk (?kiosk=1 or #kiosk): no nav, fullscreen button (auto on first
  touch), Screen Wake Lock re-acquired on visibility, reconnect banner
  (offline / useCalls error) with backoff reload, 18-20 px type via a
  data-kiosk attribute and Tailwind arbitrary variants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-05 02:58:22 -04:00
co-authored by Claude Opus 5.5
parent 7bee87b147
commit afd2bc463d
7 changed files with 717 additions and 61 deletions
+7 -1
View File
@@ -6,6 +6,7 @@ import { useAuth } from "@/components/AuthProvider";
import { Nav } from "@/components/Nav";
import { MarketingHeader } from "@/components/marketing/MarketingHeader";
import { MarketingFooter } from "@/components/marketing/MarketingFooter";
import { useKioskFlag } from "@/lib/useKiosk";
// Public marketing surface — exact paths, not prefixes, so e.g. /features/x
// (if it ever exists) doesn't accidentally get pulled into marketing chrome.
@@ -36,6 +37,7 @@ export function ChromeSwitcher({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { user, loading, orgId } = useAuth();
const router = useRouter();
const kiosk = useKioskFlag();
useEffect(() => {
if (loading) return;
@@ -62,7 +64,11 @@ export function ChromeSwitcher({ children }: { children: React.ReactNode }) {
}
// Live ("/") is full-bleed under its own top bar, not the padded
// max-width container the rest of the app uses.
// max-width container the rest of the app uses. In kiosk mode (?kiosk=1 or
// #kiosk, server-26#234) the wall gets the whole screen: no nav at all.
if (pathname === "/" && user && kiosk) {
return <main className="h-screen">{children}</main>;
}
if (pathname === "/" && user) {
return (
<>