mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
feat: add kiosk mode toggle button to header
Closes #1122 Adds a kiosk mode toggle button in the header next to the logout button. Previously, users had to manually add ?kiosk=1 to the URL to enable kiosk mode (which hides navigation and filters for wall displays). Now users can: - Click the toggle button to enter/exit kiosk mode instantly - Button shows maximize icon when off, minimize icon when on - Hover reveals 'Kiosk' or 'Exit' label - Blue highlight when active for clear visual feedback - State persists in sessionStorage like the URL param method Implementation: - Added setKioskMode() and subscribeToKioskMode() to url.ts for programmatic control and reactive state tracking - Added toggle button in AppLayout header with lucide icons - Uses same sessionStorage key as URL param approach for compatibility
This commit is contained in:
@@ -44,6 +44,8 @@ import MonitorIcon from 'lucide-solid/icons/monitor';
|
||||
import BellIcon from 'lucide-solid/icons/bell';
|
||||
import SettingsIcon from 'lucide-solid/icons/settings';
|
||||
import NetworkIcon from 'lucide-solid/icons/network';
|
||||
import Maximize2Icon from 'lucide-solid/icons/maximize-2';
|
||||
import Minimize2Icon from 'lucide-solid/icons/minimize-2';
|
||||
import { TokenRevealDialog } from './components/TokenRevealDialog';
|
||||
import { useAlertsActivation } from './stores/alertsActivation';
|
||||
import { UpdateProgressModal } from './components/UpdateProgressModal';
|
||||
@@ -53,7 +55,7 @@ import { AIStatusIndicator } from './components/AI/AIStatusIndicator';
|
||||
import { aiChatStore } from './stores/aiChat';
|
||||
import { useResourcesAsLegacy } from './hooks/useResources';
|
||||
import { updateSystemSettingsFromResponse, markSystemSettingsLoadedWithDefaults } from './stores/systemSettings';
|
||||
import { initKioskMode } from './utils/url';
|
||||
import { initKioskMode, isKioskMode, setKioskMode, subscribeToKioskMode } from './utils/url';
|
||||
|
||||
|
||||
const Dashboard = lazy(() =>
|
||||
@@ -1041,6 +1043,23 @@ function AppLayout(props: {
|
||||
|
||||
const [seenPlatforms, setSeenPlatforms] = createSignal<Record<string, boolean>>(readSeenPlatforms());
|
||||
|
||||
// Reactive kiosk mode state
|
||||
const [kioskMode, setKioskModeSignal] = createSignal(isKioskMode());
|
||||
|
||||
// Subscribe to kiosk mode changes from other sources (like URL params)
|
||||
onMount(() => {
|
||||
const unsubscribe = subscribeToKioskMode((enabled) => {
|
||||
setKioskModeSignal(enabled);
|
||||
});
|
||||
onCleanup(unsubscribe);
|
||||
});
|
||||
|
||||
const toggleKioskMode = () => {
|
||||
const newValue = !kioskMode();
|
||||
setKioskMode(newValue);
|
||||
setKioskModeSignal(newValue);
|
||||
};
|
||||
|
||||
const persistSeenPlatforms = (map: Record<string, boolean>) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
@@ -1288,6 +1307,27 @@ function AppLayout(props: {
|
||||
<div class="flex items-center gap-2">
|
||||
{/* AI Patrol Status Indicator */}
|
||||
<AIStatusIndicator />
|
||||
{/* Kiosk Mode Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleKioskMode}
|
||||
class={`group relative flex h-7 items-center justify-center gap-1 rounded-full px-2 text-xs transition-all duration-500 ease-in-out focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 ${kioskMode()
|
||||
? 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300 dark:hover:bg-blue-800'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={kioskMode() ? 'Exit kiosk mode (show navigation)' : 'Enter kiosk mode (hide navigation)'}
|
||||
aria-label={kioskMode() ? 'Exit kiosk mode' : 'Enter kiosk mode'}
|
||||
aria-pressed={kioskMode()}
|
||||
>
|
||||
<Show when={kioskMode()} fallback={<Maximize2Icon class="h-3 w-3 flex-shrink-0" />}>
|
||||
<Minimize2Icon class="h-3 w-3 flex-shrink-0" />
|
||||
</Show>
|
||||
<span
|
||||
class="max-w-0 overflow-hidden whitespace-nowrap opacity-0 transition-all duration-500 ease-in-out group-hover:ml-1 group-hover:max-w-[80px] group-hover:opacity-100 group-focus-visible:ml-1 group-focus-visible:max-w-[80px] group-focus-visible:opacity-100"
|
||||
>
|
||||
{kioskMode() ? 'Exit' : 'Kiosk'}
|
||||
</span>
|
||||
</button>
|
||||
<Show when={props.proxyAuthInfo()?.username}>
|
||||
<span class="text-xs px-2 py-1 text-gray-600 dark:text-gray-400">
|
||||
{props.proxyAuthInfo()?.username}
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
const FALLBACK_BASE_URL = 'http://localhost:7655';
|
||||
const KIOSK_MODE_KEY = 'pulse_kiosk_mode';
|
||||
|
||||
// Reactive kiosk mode tracking - listeners are notified when kiosk mode changes
|
||||
type KioskListener = (enabled: boolean) => void;
|
||||
const kioskListeners = new Set<KioskListener>();
|
||||
|
||||
function notifyKioskListeners(enabled: boolean): void {
|
||||
kioskListeners.forEach(listener => listener(enabled));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to kiosk mode changes.
|
||||
* Returns an unsubscribe function.
|
||||
*/
|
||||
export function subscribeToKioskMode(listener: KioskListener): () => void {
|
||||
kioskListeners.add(listener);
|
||||
return () => kioskListeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and persist kiosk mode from URL parameters.
|
||||
* When ?kiosk=1 or ?kiosk=true is in the URL, this stores the preference
|
||||
@@ -49,6 +66,25 @@ export function isKioskMode(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set kiosk mode on or off.
|
||||
* Updates sessionStorage and notifies reactive listeners.
|
||||
*/
|
||||
export function setKioskMode(enabled: boolean): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
if (enabled) {
|
||||
window.sessionStorage.setItem(KIOSK_MODE_KEY, 'true');
|
||||
} else {
|
||||
window.sessionStorage.removeItem(KIOSK_MODE_KEY);
|
||||
}
|
||||
notifyKioskListeners(enabled);
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
export function getPulseBaseUrl(): string {
|
||||
if (typeof window === 'undefined' || !window.location) {
|
||||
return FALLBACK_BASE_URL;
|
||||
|
||||
Reference in New Issue
Block a user