mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	refactor(web): NavigationBar (sidebar) styles
This commit is contained in:
		
							parent
							
								
									ff62338359
								
							
						
					
					
						commit
						ba0338e9d5
					
				@ -11,13 +11,13 @@ import { Router } from 'preact-router';
 | 
			
		||||
import Sidebar from './Sidebar';
 | 
			
		||||
import StyleGuide from './StyleGuide';
 | 
			
		||||
import Api, { FetchStatus, useConfig } from './api';
 | 
			
		||||
import { DarkModeProvider, SidebarProvider } from './context';
 | 
			
		||||
import { DarkModeProvider, DrawerProvider } from './context';
 | 
			
		||||
 | 
			
		||||
export default function App() {
 | 
			
		||||
  const { data, status } = useConfig();
 | 
			
		||||
  return (
 | 
			
		||||
    <DarkModeProvider>
 | 
			
		||||
      <SidebarProvider>
 | 
			
		||||
      <DrawerProvider>
 | 
			
		||||
        <div class="w-full">
 | 
			
		||||
          <AppBar title="Frigate" />
 | 
			
		||||
          {status !== FetchStatus.LOADED ? (
 | 
			
		||||
@ -41,7 +41,7 @@ export default function App() {
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </SidebarProvider>
 | 
			
		||||
      </DrawerProvider>
 | 
			
		||||
    </DarkModeProvider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,56 +1,45 @@
 | 
			
		||||
import { h, Fragment } from 'preact';
 | 
			
		||||
import Link from './components/Link';
 | 
			
		||||
import LinkedLogo from './components/LinkedLogo';
 | 
			
		||||
import { Link as RouterLink } from 'preact-router/match';
 | 
			
		||||
import { useCallback, useState } from 'preact/hooks';
 | 
			
		||||
import { useSidebar } from './context';
 | 
			
		||||
import { Match } from 'preact-router/match';
 | 
			
		||||
import { memo } from 'preact/compat';
 | 
			
		||||
import { useConfig } from './api';
 | 
			
		||||
import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer';
 | 
			
		||||
import { useCallback, useMemo } from 'preact/hooks';
 | 
			
		||||
 | 
			
		||||
export default function Sidebar() {
 | 
			
		||||
  const { data: config } = useConfig();
 | 
			
		||||
  const cameras = useMemo(() => Object.keys(config.cameras), [config]);
 | 
			
		||||
 | 
			
		||||
function NavLink({ className = '', href, text, ...other }) {
 | 
			
		||||
  const external = href.startsWith('http');
 | 
			
		||||
  const El = external ? Link : RouterLink;
 | 
			
		||||
  const props = external ? { rel: 'noopener nofollow', target: '_blank' } : {};
 | 
			
		||||
  return (
 | 
			
		||||
    <El
 | 
			
		||||
      activeClassName="bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:bg-gray-600 dark:focus:text-white dark:hover:text-white dark:text-gray-200"
 | 
			
		||||
      className={`block px-4 py-2 mt-2 text-sm font-semibold text-gray-900 bg-transparent rounded-lg dark:bg-transparent dark:hover:bg-gray-600 dark:focus:bg-gray-600 dark:focus:text-white dark:hover:text-white dark:text-gray-200 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline self-end ${className}`}
 | 
			
		||||
      href={href}
 | 
			
		||||
      {...other}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {text}
 | 
			
		||||
    </El>
 | 
			
		||||
    <NavigationDrawer header={<Header />}>
 | 
			
		||||
      <Destination href="/" text="Cameras" />
 | 
			
		||||
      <Match path="/cameras/:camera">
 | 
			
		||||
        {({ matches }) =>
 | 
			
		||||
          matches ? (
 | 
			
		||||
            <Fragment>
 | 
			
		||||
              <Separator />
 | 
			
		||||
              {cameras.map((camera) => (
 | 
			
		||||
                <Destination href={`/cameras/${camera}`} text={camera} />
 | 
			
		||||
              ))}
 | 
			
		||||
              <Separator />
 | 
			
		||||
            </Fragment>
 | 
			
		||||
          ) : null
 | 
			
		||||
        }
 | 
			
		||||
      </Match>
 | 
			
		||||
      <Destination href="/events" text="Events" />
 | 
			
		||||
      <Destination href="/debug" text="Debug" />
 | 
			
		||||
      <Separator />
 | 
			
		||||
      <div className="flex flex-grow" />
 | 
			
		||||
      <Destination className="self-end" href="https://blakeblackshear.github.io/frigate" text="Documentation" />
 | 
			
		||||
      <Destination className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" />
 | 
			
		||||
    </NavigationDrawer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Sidebar() {
 | 
			
		||||
  const { showSidebar, setShowSidebar } = useSidebar();
 | 
			
		||||
 | 
			
		||||
  const handleDismiss = useCallback(() => {
 | 
			
		||||
    setShowSidebar(false);
 | 
			
		||||
  }, [setShowSidebar]);
 | 
			
		||||
 | 
			
		||||
const Header = memo(function Header() {
 | 
			
		||||
  return (
 | 
			
		||||
    <Fragment>
 | 
			
		||||
      {showSidebar ? <div className="fixed inset-0 z-20" onClick={handleDismiss} /> : ''}
 | 
			
		||||
      <div
 | 
			
		||||
        className={`fixed left-0 top-0 bottom-0 lg:sticky max-h-screen flex flex-col w-64 text-gray-700 bg-white dark:text-gray-200 dark:bg-gray-900 flex-shrink-0 border-r border-gray-200 dark:border-gray-700 shadow lg:shadow-none z-20 lg:z-0 transform translate-x-0 ${
 | 
			
		||||
          !showSidebar ? '-translate-x-full' : ''
 | 
			
		||||
        } lg:translate-x-0 transition-transform duration-300`}
 | 
			
		||||
      >
 | 
			
		||||
        <div className="flex-shrink-0 p-4 flex flex-row items-center justify-between">
 | 
			
		||||
    <div class="text-gray-500">
 | 
			
		||||
      <LinkedLogo />
 | 
			
		||||
    </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <nav className="flex-col flex-grow md:block overflow-hidden px-4 pb-4 md:pb-0 md:overflow-y-auto">
 | 
			
		||||
          <NavLink onClick={handleDismiss} href="/" text="Cameras" />
 | 
			
		||||
          <NavLink onClick={handleDismiss} href="/events" text="Events" />
 | 
			
		||||
          <NavLink onClick={handleDismiss} href="/debug" text="Debug" />
 | 
			
		||||
          <hr className="border-solid border-gray-500 mt-2" />
 | 
			
		||||
          <NavLink className="self-end" href="https://blakeblackshear.github.io/frigate" text="Documentation" />
 | 
			
		||||
          <NavLink className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" />
 | 
			
		||||
        </nav>
 | 
			
		||||
      </div>
 | 
			
		||||
    </Fragment>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import MoreIcon from '../icons/More';
 | 
			
		||||
import AutoAwesomeIcon from '../icons/AutoAwesome';
 | 
			
		||||
import LightModeIcon from '../icons/LightMode';
 | 
			
		||||
import DarkModeIcon from '../icons/DarkMode';
 | 
			
		||||
import { useDarkMode, useSidebar } from '../context';
 | 
			
		||||
import { useDarkMode, useDrawer } from '../context';
 | 
			
		||||
import { useLayoutEffect, useCallback, useRef, useState } from 'preact/hooks';
 | 
			
		||||
 | 
			
		||||
// We would typically preserve these in component state
 | 
			
		||||
@ -18,10 +18,10 @@ let lastScrollY = window.scrollY;
 | 
			
		||||
export default function AppBar({ title }) {
 | 
			
		||||
  const [show, setShow] = useState(true);
 | 
			
		||||
  const [atZero, setAtZero] = useState(window.scrollY === 0);
 | 
			
		||||
  const [sidebarVisible, setSidebarVisible] = useState(true);
 | 
			
		||||
  const [_, setDrawerVisible] = useState(true);
 | 
			
		||||
  const [showMoreMenu, setShowMoreMenu] = useState(false);
 | 
			
		||||
  const { currentMode, persistedMode, setDarkMode } = useDarkMode();
 | 
			
		||||
  const { showSidebar, setShowSidebar } = useSidebar();
 | 
			
		||||
  const { showDrawer, setShowDrawer } = useDrawer();
 | 
			
		||||
 | 
			
		||||
  const handleSelectDarkMode = useCallback(
 | 
			
		||||
    (value, label) => {
 | 
			
		||||
@ -65,9 +65,9 @@ export default function AppBar({ title }) {
 | 
			
		||||
    setShowMoreMenu(false);
 | 
			
		||||
  }, [setShowMoreMenu]);
 | 
			
		||||
 | 
			
		||||
  const handleShowSidebar = useCallback(() => {
 | 
			
		||||
    setShowSidebar(true);
 | 
			
		||||
  }, [setShowSidebar]);
 | 
			
		||||
  const handleShowDrawer = useCallback(() => {
 | 
			
		||||
    setShowDrawer(true);
 | 
			
		||||
  }, [setShowDrawer]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
@ -76,7 +76,7 @@ export default function AppBar({ title }) {
 | 
			
		||||
      } ${!atZero ? 'shadow' : ''}`}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="lg:hidden">
 | 
			
		||||
        <Button color="black" className="rounded-full w-12 h-12" onClick={handleShowSidebar} type="text">
 | 
			
		||||
        <Button color="black" className="rounded-full w-12 h-12" onClick={handleShowDrawer} type="text">
 | 
			
		||||
          <MenuIcon />
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,16 @@
 | 
			
		||||
import { h } from 'preact';
 | 
			
		||||
import { Link as RouterLink } from 'preact-router/match';
 | 
			
		||||
 | 
			
		||||
export default function Link({ className, children, href, ...props }) {
 | 
			
		||||
export default function Link({
 | 
			
		||||
  activeClassName = '',
 | 
			
		||||
  className = 'text-blue-500 hover:underline',
 | 
			
		||||
  children,
 | 
			
		||||
  href,
 | 
			
		||||
  ...props
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <a className={`text-blue-500 dark:text-blue-400 hover:underline ${className}`} href={href} {...props}>
 | 
			
		||||
    <RouterLink activeClassName={activeClassName} className={className} href={href} {...props}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </a>
 | 
			
		||||
    </RouterLink>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ export default function Menu({ className, children, onDismiss, relativeTo }) {
 | 
			
		||||
  return relativeTo ? (
 | 
			
		||||
    <RelativeModal
 | 
			
		||||
      children={children}
 | 
			
		||||
      className={`${className || ''} pt-2 pb-2`}
 | 
			
		||||
      className={`${className || ''} py-2`}
 | 
			
		||||
      role="listbox"
 | 
			
		||||
      onDismiss={onDismiss}
 | 
			
		||||
      portalRootID="menus"
 | 
			
		||||
@ -48,5 +48,5 @@ export function MenuItem({ focus, icon: Icon, label, onSelect, value }) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function MenuSeparator() {
 | 
			
		||||
  return <div className="border-b border-gray-200 my-2" />;
 | 
			
		||||
  return <div className="border-b border-gray-200 dark:border-gray-800 my-2" />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										61
									
								
								web/src/components/NavigationDrawer.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								web/src/components/NavigationDrawer.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
import { h, Fragment } from 'preact';
 | 
			
		||||
import { Link } from 'preact-router/match';
 | 
			
		||||
import { useCallback, useState } from 'preact/hooks';
 | 
			
		||||
import { useDrawer } from '../context';
 | 
			
		||||
 | 
			
		||||
export default function NavigationDrawer({ children, header }) {
 | 
			
		||||
  const { showDrawer, setShowDrawer } = useDrawer();
 | 
			
		||||
 | 
			
		||||
  const handleDismiss = useCallback(() => {
 | 
			
		||||
    setShowDrawer(false);
 | 
			
		||||
  }, [setShowDrawer]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Fragment>
 | 
			
		||||
      {showDrawer ? <div className="fixed inset-0 z-20" onClick={handleDismiss} /> : ''}
 | 
			
		||||
      <div
 | 
			
		||||
        className={`fixed left-0 top-0 bottom-0 lg:sticky max-h-screen flex flex-col w-64 text-gray-700 bg-white dark:text-gray-200 dark:bg-gray-900 flex-shrink-0 border-r border-gray-200 dark:border-gray-700 shadow lg:shadow-none z-20 lg:z-0 transform translate-x-0 ${
 | 
			
		||||
          !showDrawer ? '-translate-x-full' : ''
 | 
			
		||||
        } lg:translate-x-0 transition-transform duration-300`}
 | 
			
		||||
        onClick={handleDismiss}
 | 
			
		||||
      >
 | 
			
		||||
        {header ? (
 | 
			
		||||
          <div className="flex-shrink-0 p-5 flex flex-row items-center justify-between border-b border-gray-200 dark:border-gray-700">
 | 
			
		||||
            {header}
 | 
			
		||||
          </div>
 | 
			
		||||
        ) : null}
 | 
			
		||||
 | 
			
		||||
        <nav className="flex flex-col flex-grow overflow-hidden overflow-y-auto p-2 space-y-2">{children}</nav>
 | 
			
		||||
      </div>
 | 
			
		||||
    </Fragment>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Destination({ className = '', href, text, ...other }) {
 | 
			
		||||
  const external = href.startsWith('http');
 | 
			
		||||
  const props = external ? { rel: 'noopener nofollow', target: '_blank' } : {};
 | 
			
		||||
 | 
			
		||||
  const { setShowDrawer } = useDrawer();
 | 
			
		||||
 | 
			
		||||
  const handleDismiss = useCallback(() => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      setShowDrawer(false);
 | 
			
		||||
    }, 250);
 | 
			
		||||
  }, [setShowDrawer]);
 | 
			
		||||
 | 
			
		||||
  const styleProps = {
 | 
			
		||||
    [external
 | 
			
		||||
      ? 'className'
 | 
			
		||||
      : 'class']: 'block p-2 text-sm font-semibold text-gray-900 rounded hover:bg-blue-500 dark:text-gray-200 hover:text-white dark:hover:text-white focus:outline-none ring-opacity-50 focus:ring-2 ring-blue-300',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Link activeClassName="bg-blue-500 bg-opacity-50 text-white" {...styleProps} href={href} {...props} {...other}>
 | 
			
		||||
      <div onClick={handleDismiss}>{text}</div>
 | 
			
		||||
    </Link>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Separator() {
 | 
			
		||||
  return <div className="border-b border-gray-200 dark:border-gray-700 -mx-2" />;
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { h, Fragment } from 'preact';
 | 
			
		||||
import { createPortal } from 'preact/compat';
 | 
			
		||||
import { DarkModeProvider } from '../context';
 | 
			
		||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
 | 
			
		||||
 | 
			
		||||
const WINDOW_PADDING = 20;
 | 
			
		||||
@ -75,7 +76,7 @@ export default function RelativeModal({ className, role = 'dialog', children, on
 | 
			
		||||
  }, [show, position.width, ref.current]);
 | 
			
		||||
 | 
			
		||||
  const menu = (
 | 
			
		||||
    <Fragment>
 | 
			
		||||
    <DarkModeProvider>
 | 
			
		||||
      <div className="absolute inset-0" onClick={handleDismiss} />
 | 
			
		||||
      <div
 | 
			
		||||
        className={`z-10 bg-white dark:bg-gray-700 dark:text-white absolute shadow-lg rounded w-auto max-h-48 transition-all duration-75 transform scale-90 opacity-0 ${
 | 
			
		||||
@ -90,7 +91,7 @@ export default function RelativeModal({ className, role = 'dialog', children, on
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </div>
 | 
			
		||||
    </Fragment>
 | 
			
		||||
    </DarkModeProvider>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return portalRoot ? createPortal(menu, portalRoot) : menu;
 | 
			
		||||
 | 
			
		||||
@ -65,14 +65,14 @@ export function useDarkMode() {
 | 
			
		||||
  return useContext(DarkMode);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Sidebar = createContext(null);
 | 
			
		||||
const Drawer = createContext(null);
 | 
			
		||||
 | 
			
		||||
export function SidebarProvider({ children }) {
 | 
			
		||||
  const [showSidebar, setShowSidebar] = useState(false);
 | 
			
		||||
export function DrawerProvider({ children }) {
 | 
			
		||||
  const [showDrawer, setShowDrawer] = useState(false);
 | 
			
		||||
 | 
			
		||||
  return <Sidebar.Provider value={{ showSidebar, setShowSidebar }}>{children}</Sidebar.Provider>;
 | 
			
		||||
  return <Drawer.Provider value={{ showDrawer, setShowDrawer }}>{children}</Drawer.Provider>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useSidebar() {
 | 
			
		||||
  return useContext(Sidebar);
 | 
			
		||||
export function useDrawer() {
 | 
			
		||||
  return useContext(Drawer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user