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:
@@ -3,6 +3,7 @@ import { Dashboard } from '@/components/layout';
|
|||||||
import { ThermostatOverlay } from '@/components/climate';
|
import { ThermostatOverlay } from '@/components/climate';
|
||||||
import { LightsOverlay } from '@/components/lights';
|
import { LightsOverlay } from '@/components/lights';
|
||||||
import { LocksOverlay } from '@/components/locks';
|
import { LocksOverlay } from '@/components/locks';
|
||||||
|
import { ControlsOverlay } from '@/components/controls';
|
||||||
import { CalendarWidget } from '@/components/calendar';
|
import { CalendarWidget } from '@/components/calendar';
|
||||||
import { TodoWidget } from '@/components/todo';
|
import { TodoWidget } from '@/components/todo';
|
||||||
import { SettingsPanel, ConnectionModal } from '@/components/settings';
|
import { SettingsPanel, ConnectionModal } from '@/components/settings';
|
||||||
@@ -124,6 +125,7 @@ export default function App() {
|
|||||||
const locksOverlayOpen = useUIStore((state) => state.locksOverlayOpen);
|
const locksOverlayOpen = useUIStore((state) => state.locksOverlayOpen);
|
||||||
const thermostatsOverlayOpen = useUIStore((state) => state.thermostatsOverlayOpen);
|
const thermostatsOverlayOpen = useUIStore((state) => state.thermostatsOverlayOpen);
|
||||||
const mediaOverlayOpen = useUIStore((state) => state.mediaOverlayOpen);
|
const mediaOverlayOpen = useUIStore((state) => state.mediaOverlayOpen);
|
||||||
|
const controlsOverlayOpen = useUIStore((state) => state.controlsOverlayOpen);
|
||||||
const { isOpen: cameraOverlayOpen } = useCameraOverlay();
|
const { isOpen: cameraOverlayOpen } = useCameraOverlay();
|
||||||
const isIdle = useIdle(env.photoFrameIdleTimeout);
|
const isIdle = useIdle(env.photoFrameIdleTimeout);
|
||||||
|
|
||||||
@@ -273,6 +275,7 @@ export default function App() {
|
|||||||
{locksOverlayOpen && <LocksOverlay />}
|
{locksOverlayOpen && <LocksOverlay />}
|
||||||
{thermostatsOverlayOpen && <ThermostatOverlay />}
|
{thermostatsOverlayOpen && <ThermostatOverlay />}
|
||||||
{mediaOverlayOpen && <JellyfinOverlay />}
|
{mediaOverlayOpen && <JellyfinOverlay />}
|
||||||
|
{controlsOverlayOpen && <ControlsOverlay />}
|
||||||
{cameraOverlayOpen && <CameraOverlay />}
|
{cameraOverlayOpen && <CameraOverlay />}
|
||||||
{settingsOpen && <SettingsPanel />}
|
{settingsOpen && <SettingsPanel />}
|
||||||
{isIdle && !alertCamera && <PhotoFrame intervalMs={env.photoFrameInterval} />}
|
{isIdle && !alertCamera && <PhotoFrame intervalMs={env.photoFrameInterval} />}
|
||||||
|
|||||||
54
src/components/controls/ControlsOverlay.tsx
Normal file
54
src/components/controls/ControlsOverlay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/controls/index.ts
Normal file
1
src/components/controls/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ControlsOverlay } from './ControlsOverlay';
|
||||||
@@ -84,38 +84,54 @@ export function Header() {
|
|||||||
const connectionState = useConnectionState();
|
const connectionState = useConnectionState();
|
||||||
const cameraOverlayOpen = useUIStore((state) => state.cameraOverlayOpen);
|
const cameraOverlayOpen = useUIStore((state) => state.cameraOverlayOpen);
|
||||||
const mediaOverlayOpen = useUIStore((state) => state.mediaOverlayOpen);
|
const mediaOverlayOpen = useUIStore((state) => state.mediaOverlayOpen);
|
||||||
|
const controlsOverlayOpen = useUIStore((state) => state.controlsOverlayOpen);
|
||||||
const settingsOpen = useUIStore((state) => state.settingsOpen);
|
const settingsOpen = useUIStore((state) => state.settingsOpen);
|
||||||
const openCameraOverlay = useUIStore((state) => state.openCameraOverlay);
|
const openCameraOverlay = useUIStore((state) => state.openCameraOverlay);
|
||||||
const closeCameraOverlay = useUIStore((state) => state.closeCameraOverlay);
|
const closeCameraOverlay = useUIStore((state) => state.closeCameraOverlay);
|
||||||
const openMediaOverlay = useUIStore((state) => state.openMediaOverlay);
|
const openMediaOverlay = useUIStore((state) => state.openMediaOverlay);
|
||||||
const closeMediaOverlay = useUIStore((state) => state.closeMediaOverlay);
|
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 openSettings = useUIStore((state) => state.openSettings);
|
||||||
const closeSettings = useUIStore((state) => state.closeSettings);
|
const closeSettings = useUIStore((state) => state.closeSettings);
|
||||||
const people = useSettingsStore((state) => state.config.people);
|
const people = useSettingsStore((state) => state.config.people);
|
||||||
const cameras = useSettingsStore((state) => state.config.cameras);
|
const cameras = useSettingsStore((state) => state.config.cameras);
|
||||||
const [currentTime, setCurrentTime] = useState(new Date());
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
|
|
||||||
const activeTab: 'home' | 'media' | 'cameras' | 'settings' =
|
const activeTab: 'home' | 'controls' | 'media' | 'cameras' | 'settings' =
|
||||||
settingsOpen ? 'settings' : cameraOverlayOpen ? 'cameras' : mediaOverlayOpen ? 'media' : 'home';
|
settingsOpen
|
||||||
|
? 'settings'
|
||||||
|
: cameraOverlayOpen
|
||||||
|
? 'cameras'
|
||||||
|
: mediaOverlayOpen
|
||||||
|
? 'media'
|
||||||
|
: controlsOverlayOpen
|
||||||
|
? 'controls'
|
||||||
|
: 'home';
|
||||||
|
|
||||||
const goHome = () => {
|
const closeAll = () => {
|
||||||
closeCameraOverlay();
|
closeCameraOverlay();
|
||||||
closeMediaOverlay();
|
closeMediaOverlay();
|
||||||
|
closeControlsOverlay();
|
||||||
closeSettings();
|
closeSettings();
|
||||||
};
|
};
|
||||||
|
const goHome = () => {
|
||||||
|
closeAll();
|
||||||
|
};
|
||||||
|
const goControls = () => {
|
||||||
|
closeAll();
|
||||||
|
openControlsOverlay();
|
||||||
|
};
|
||||||
const goMedia = () => {
|
const goMedia = () => {
|
||||||
closeCameraOverlay();
|
closeAll();
|
||||||
closeSettings();
|
|
||||||
openMediaOverlay();
|
openMediaOverlay();
|
||||||
};
|
};
|
||||||
const goCameras = () => {
|
const goCameras = () => {
|
||||||
closeMediaOverlay();
|
closeAll();
|
||||||
closeSettings();
|
|
||||||
openCameraOverlay();
|
openCameraOverlay();
|
||||||
};
|
};
|
||||||
const goSettings = () => {
|
const goSettings = () => {
|
||||||
closeMediaOverlay();
|
closeAll();
|
||||||
closeCameraOverlay();
|
|
||||||
openSettings();
|
openSettings();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -193,6 +209,12 @@ export function Header() {
|
|||||||
</svg>
|
</svg>
|
||||||
Home
|
Home
|
||||||
</button>
|
</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')}>
|
<button onClick={goMedia} className={tabClass('media')}>
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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" />
|
<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" />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface UIState {
|
|||||||
locksOverlayOpen: boolean;
|
locksOverlayOpen: boolean;
|
||||||
thermostatsOverlayOpen: boolean;
|
thermostatsOverlayOpen: boolean;
|
||||||
mediaOverlayOpen: boolean;
|
mediaOverlayOpen: boolean;
|
||||||
|
controlsOverlayOpen: boolean;
|
||||||
personAlertActive: boolean;
|
personAlertActive: boolean;
|
||||||
personAlertCamera: string | null;
|
personAlertCamera: string | null;
|
||||||
|
|
||||||
@@ -46,6 +47,9 @@ interface UIState {
|
|||||||
openMediaOverlay: () => void;
|
openMediaOverlay: () => void;
|
||||||
closeMediaOverlay: () => void;
|
closeMediaOverlay: () => void;
|
||||||
|
|
||||||
|
openControlsOverlay: () => void;
|
||||||
|
closeControlsOverlay: () => void;
|
||||||
|
|
||||||
showPersonAlert: (camera: string) => void;
|
showPersonAlert: (camera: string) => void;
|
||||||
dismissPersonAlert: () => void;
|
dismissPersonAlert: () => void;
|
||||||
|
|
||||||
@@ -69,6 +73,7 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
locksOverlayOpen: false,
|
locksOverlayOpen: false,
|
||||||
thermostatsOverlayOpen: false,
|
thermostatsOverlayOpen: false,
|
||||||
mediaOverlayOpen: false,
|
mediaOverlayOpen: false,
|
||||||
|
controlsOverlayOpen: false,
|
||||||
personAlertActive: false,
|
personAlertActive: false,
|
||||||
personAlertCamera: null,
|
personAlertCamera: null,
|
||||||
alarmoKeypadOpen: false,
|
alarmoKeypadOpen: false,
|
||||||
@@ -111,6 +116,10 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
openMediaOverlay: () => set({ mediaOverlayOpen: true }),
|
openMediaOverlay: () => set({ mediaOverlayOpen: true }),
|
||||||
closeMediaOverlay: () => set({ mediaOverlayOpen: false }),
|
closeMediaOverlay: () => set({ mediaOverlayOpen: false }),
|
||||||
|
|
||||||
|
// Controls overlay (lights / locks / alarm / thermostats)
|
||||||
|
openControlsOverlay: () => set({ controlsOverlayOpen: true }),
|
||||||
|
closeControlsOverlay: () => set({ controlsOverlayOpen: false }),
|
||||||
|
|
||||||
// Person detection alert
|
// Person detection alert
|
||||||
showPersonAlert: (camera) =>
|
showPersonAlert: (camera) =>
|
||||||
set({
|
set({
|
||||||
|
|||||||
Reference in New Issue
Block a user