Просмотр исходного кода

refactor(web): NavigationBar (sidebar) styles

Paul Armstrong 4 лет назад
Родитель
Сommit
ba0338e9d5

+ 3 - 3
web/src/App.jsx

@@ -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>
   );
 }

+ 36 - 47
web/src/Sidebar.jsx

@@ -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>
+    <div class="text-gray-500">
+      <LinkedLogo />
+    </div>
   );
-}
+});

+ 7 - 7
web/src/components/AppBar.jsx

@@ -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>

+ 10 - 3
web/src/components/Link.jsx

@@ -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>
   );
 }

+ 2 - 2
web/src/components/Menu.jsx

@@ -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 - 0
web/src/components/NavigationDrawer.jsx

@@ -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" />;
+}

+ 3 - 2
web/src/components/RelativeModal.jsx

@@ -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;

+ 6 - 6
web/src/context/index.jsx

@@ -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);
 }