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 { 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} />}
|
||||
|
||||
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 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" />
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user