Files
server-26/drb-frontend/components/live/KioskControls.tsx
T
Logan CusanoandClaude Opus 5.5 afd2bc463d 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>
2026-10-05 02:58:22 -04:00

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>
);
}