Add Controls tab with lights, locks, alarm, thermostats overlay

The five-tab nav now includes a Controls tab between Home and Media.
Opens a full-screen overlay with the alarm panel, each configured
thermostat, lights, and locks tiled in a responsive 2-column grid.
This commit is contained in:
root
2026-04-14 14:06:13 -05:00
parent 1dd32c6afe
commit 7886e72f38
5 changed files with 98 additions and 9 deletions

View File

@@ -3,6 +3,7 @@ import { Dashboard } from '@/components/layout';
import { ThermostatOverlay } from '@/components/climate';
import { LightsOverlay } from '@/components/lights';
import { LocksOverlay } from '@/components/locks';
import { ControlsOverlay } from '@/components/controls';
import { CalendarWidget } from '@/components/calendar';
import { TodoWidget } from '@/components/todo';
import { SettingsPanel, ConnectionModal } from '@/components/settings';
@@ -124,6 +125,7 @@ export default function App() {
const locksOverlayOpen = useUIStore((state) => state.locksOverlayOpen);
const thermostatsOverlayOpen = useUIStore((state) => state.thermostatsOverlayOpen);
const mediaOverlayOpen = useUIStore((state) => state.mediaOverlayOpen);
const controlsOverlayOpen = useUIStore((state) => state.controlsOverlayOpen);
const { isOpen: cameraOverlayOpen } = useCameraOverlay();
const isIdle = useIdle(env.photoFrameIdleTimeout);
@@ -273,6 +275,7 @@ export default function App() {
{locksOverlayOpen && <LocksOverlay />}
{thermostatsOverlayOpen && <ThermostatOverlay />}
{mediaOverlayOpen && <JellyfinOverlay />}
{controlsOverlayOpen && <ControlsOverlay />}
{cameraOverlayOpen && <CameraOverlay />}
{settingsOpen && <SettingsPanel />}
{isIdle && !alertCamera && <PhotoFrame intervalMs={env.photoFrameInterval} />}

View File

@@ -0,0 +1,54 @@
import { useUIStore } from '@/stores/uiStore';
import { useSettingsStore } from '@/stores/settingsStore';
import { LightsWidget } from '@/components/lights';
import { LocksWidget } from '@/components/locks';
import { ThermostatWidget } from '@/components/climate';
import { AlarmoPanel } from '@/components/alarm';
export function ControlsOverlay() {
const closeControlsOverlay = useUIStore((s) => s.closeControlsOverlay);
const config = useSettingsStore((s) => s.config);
return (
<div className="overlay-full">
<header className="h-16 bg-dark-secondary border-b border-dark-border flex items-center justify-between px-5 shrink-0">
<h2 className="text-2xl font-bold text-ink">Controls</h2>
<button
onClick={closeControlsOverlay}
className="btn btn-sm"
aria-label="Close controls"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
Close
</button>
</header>
<div className="flex-1 overflow-auto p-5">
<div className="grid grid-cols-12 auto-rows-min gap-4">
{config.alarm && (
<div className="col-span-12 md:col-span-6">
<AlarmoPanel />
</div>
)}
{config.thermostats.map((thermostat) => (
<div key={thermostat.entityId} className="col-span-12 md:col-span-6">
<ThermostatWidget config={thermostat} />
</div>
))}
{config.lights.length > 0 && (
<div className="col-span-12 md:col-span-6">
<LightsWidget />
</div>
)}
{config.locks.length > 0 && (
<div className="col-span-12 md:col-span-6">
<LocksWidget />
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export { ControlsOverlay } from './ControlsOverlay';

View File

@@ -84,38 +84,54 @@ export function Header() {
const connectionState = useConnectionState();
const cameraOverlayOpen = useUIStore((state) => state.cameraOverlayOpen);
const mediaOverlayOpen = useUIStore((state) => state.mediaOverlayOpen);
const controlsOverlayOpen = useUIStore((state) => state.controlsOverlayOpen);
const settingsOpen = useUIStore((state) => state.settingsOpen);
const openCameraOverlay = useUIStore((state) => state.openCameraOverlay);
const closeCameraOverlay = useUIStore((state) => state.closeCameraOverlay);
const openMediaOverlay = useUIStore((state) => state.openMediaOverlay);
const closeMediaOverlay = useUIStore((state) => state.closeMediaOverlay);
const openControlsOverlay = useUIStore((state) => state.openControlsOverlay);
const closeControlsOverlay = useUIStore((state) => state.closeControlsOverlay);
const openSettings = useUIStore((state) => state.openSettings);
const closeSettings = useUIStore((state) => state.closeSettings);
const people = useSettingsStore((state) => state.config.people);
const cameras = useSettingsStore((state) => state.config.cameras);
const [currentTime, setCurrentTime] = useState(new Date());
const activeTab: 'home' | 'media' | 'cameras' | 'settings' =
settingsOpen ? 'settings' : cameraOverlayOpen ? 'cameras' : mediaOverlayOpen ? 'media' : 'home';
const activeTab: 'home' | 'controls' | 'media' | 'cameras' | 'settings' =
settingsOpen
? 'settings'
: cameraOverlayOpen
? 'cameras'
: mediaOverlayOpen
? 'media'
: controlsOverlayOpen
? 'controls'
: 'home';
const goHome = () => {
const closeAll = () => {
closeCameraOverlay();
closeMediaOverlay();
closeControlsOverlay();
closeSettings();
};
const goHome = () => {
closeAll();
};
const goControls = () => {
closeAll();
openControlsOverlay();
};
const goMedia = () => {
closeCameraOverlay();
closeSettings();
closeAll();
openMediaOverlay();
};
const goCameras = () => {
closeMediaOverlay();
closeSettings();
closeAll();
openCameraOverlay();
};
const goSettings = () => {
closeMediaOverlay();
closeCameraOverlay();
closeAll();
openSettings();
};
@@ -193,6 +209,12 @@ export function Header() {
</svg>
Home
</button>
<button onClick={goControls} className={tabClass('controls')}>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
Controls
</button>
<button onClick={goMedia} className={tabClass('media')}>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />

View File

@@ -8,6 +8,7 @@ interface UIState {
locksOverlayOpen: boolean;
thermostatsOverlayOpen: boolean;
mediaOverlayOpen: boolean;
controlsOverlayOpen: boolean;
personAlertActive: boolean;
personAlertCamera: string | null;
@@ -46,6 +47,9 @@ interface UIState {
openMediaOverlay: () => void;
closeMediaOverlay: () => void;
openControlsOverlay: () => void;
closeControlsOverlay: () => void;
showPersonAlert: (camera: string) => void;
dismissPersonAlert: () => void;
@@ -69,6 +73,7 @@ export const useUIStore = create<UIState>((set) => ({
locksOverlayOpen: false,
thermostatsOverlayOpen: false,
mediaOverlayOpen: false,
controlsOverlayOpen: false,
personAlertActive: false,
personAlertCamera: null,
alarmoKeypadOpen: false,
@@ -111,6 +116,10 @@ export const useUIStore = create<UIState>((set) => ({
openMediaOverlay: () => set({ mediaOverlayOpen: true }),
closeMediaOverlay: () => set({ mediaOverlayOpen: false }),
// Controls overlay (lights / locks / alarm / thermostats)
openControlsOverlay: () => set({ controlsOverlayOpen: true }),
closeControlsOverlay: () => set({ controlsOverlayOpen: false }),
// Person detection alert
showPersonAlert: (camera) =>
set({