- 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>
55 lines
2.2 KiB
TypeScript
55 lines
2.2 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Live wall kiosk controls + reconnect banner (server-26#234). State lives in
|
|
* lib/useKiosk.ts; this file is presentation only.
|
|
*/
|
|
import { useEffect, useState } from "react";
|
|
import type { KioskBanner as Banner, KioskState } from "@/lib/useKiosk";
|
|
|
|
const btn = "px-2 py-0.5 rounded border border-line text-ink-2 hover:text-ink whitespace-nowrap";
|
|
|
|
export function KioskControls({ kiosk, state, onToggleKiosk }: {
|
|
kiosk: boolean;
|
|
state: KioskState;
|
|
onToggleKiosk: () => void;
|
|
}) {
|
|
return (
|
|
<>
|
|
{state.canFullscreen && (
|
|
<button type="button" onClick={state.toggleFullscreen} aria-pressed={state.fullscreen} className={btn}
|
|
title={state.fullscreen ? "Leave full screen" : "Full screen"}>
|
|
{state.fullscreen ? "Exit full screen" : "Full screen"}
|
|
</button>
|
|
)}
|
|
{kiosk && state.wake === "on" && (
|
|
<span className="text-ink-muted whitespace-nowrap" title="Screen Wake Lock held — this display will not sleep">Awake</span>
|
|
)}
|
|
<button type="button" onClick={onToggleKiosk} aria-pressed={kiosk} className={btn}
|
|
title={kiosk ? "Back to the normal app layout" : "Wall display: no nav bar, large type, screen kept awake"}>
|
|
{kiosk ? "Exit kiosk" : "Kiosk"}
|
|
</button>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function KioskBanner({ banner, onRetry }: { banner: Banner | null; onRetry: () => void }) {
|
|
const [now, setNow] = useState(() => Date.now());
|
|
useEffect(() => {
|
|
if (!banner?.retryAt) return;
|
|
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
return () => clearInterval(id);
|
|
}, [banner?.retryAt]);
|
|
if (!banner) return null;
|
|
const secs = banner.retryAt ? Math.max(0, Math.ceil((banner.retryAt - now) / 1000)) : null;
|
|
return (
|
|
<div role="alert" className="shrink-0 flex items-center gap-3 px-3 py-1.5 border-b border-line bg-raised text-xs text-ink">
|
|
<span className="font-semibold">{banner.tone === "offline" ? "Offline" : "Reconnecting"}</span>
|
|
<span className="text-ink-2">{banner.message}{secs != null ? ` Retrying in ${secs}s.` : ""}</span>
|
|
{banner.tone === "error" && (
|
|
<button type="button" onClick={onRetry} className={`${btn} ml-auto`}>Retry now</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|