diff --git a/webui/src/Layout/Sidebar.tsx b/webui/src/Layout/Sidebar.tsx index 51af11ea9..a139f75d1 100644 --- a/webui/src/Layout/Sidebar.tsx +++ b/webui/src/Layout/Sidebar.tsx @@ -1,4 +1,14 @@ -import React, { createContext, memo, useContext, useEffect, useMemo, useRef, useState } from 'react' +import React, { + createContext, + CSSProperties, + memo, + ReactNode, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react' import { CSidebarNav, CNavItem, @@ -6,7 +16,6 @@ import { CSidebarBrand, CSidebarToggler, CSidebarHeader, - CNavGroup, CBackdrop, } from '@coreui/react' import { @@ -33,6 +42,7 @@ import { createPortal } from 'react-dom' import classNames from 'classnames' import { useLocalStorage, useMediaQuery } from 'usehooks-ts' import { Link } from '@tanstack/react-router' +import { Transition, TransitionStatus } from 'react-transition-group' export interface SidebarStateProps { showToggle: boolean @@ -65,103 +75,51 @@ export function SidebarStateProvider({ children }: React.PropsWithChildren): Rea return {children} } -type NavItem = { +interface SidebarMenuItemProps { name: string - icon: IconDefinition + icon: IconDefinition | null notifications?: React.ComponentType> path?: string - show?: boolean - dropdown?: { name: string; icon?: IconDefinition; path: string; target?: string }[] + target?: string } -const primaryNavItems: NavItem[] = [ - { name: 'Connections', icon: faPlug, path: '/connections' }, - { name: 'Buttons', icon: faTh, path: '/buttons' }, - { name: 'Surfaces', icon: faGamepad, path: '/surfaces', notifications: SurfacesTabNotifyIcon }, - { name: 'Triggers', icon: faClock, path: '/triggers' }, - { name: 'Variables', icon: faDollarSign, path: '/variables' }, - { name: 'Settings', icon: faCog, path: '/settings' }, - { name: 'Import / Export', icon: faFileImport, path: '/import-export' }, - { name: 'Log', icon: faClipboardList, path: '/log' }, - { name: 'Cloud', icon: faCloud, path: '/cloud', show: window.localStorage.getItem('show_companion_cloud') === '1' }, - { - name: 'Interactive Buttons', - icon: faSquareCaretRight, - dropdown: [ - { name: 'Emulator', path: '/emulator', target: '_new' }, - { name: 'Web buttons', path: '/tablet', target: '_new' }, - ], - }, -] - -const secondaryNavItems: NavItem[] = [ - { - name: 'Help & Community', - icon: faQuestionCircle, - dropdown: [ - { name: 'Bugs & Features', icon: faBug, path: 'https://github.com/bitfocus/companion/issues', target: '_new' }, - { name: 'Facebook', icon: faUsers, path: 'https://www.facebook.com/groups/companion/', target: '_new' }, - { name: 'Slack Chat', icon: faComments, path: 'https://bitfocus.io/api/slackinvite', target: '_new' }, - { name: 'Donate', icon: faDollarSign, path: 'https://donorbox.org/bitfocus-opensource', target: '_new' }, - ], - }, -] - -interface MenuProps extends React.HTMLAttributes { - navItems: NavItem[] -} +function SidebarMenuItemLabel(item: SidebarMenuItemProps) { + return ( + <> + {item.icon ? ( + + ) : ( + + + + )} -function SidebarMenu({ navItems, className }: MenuProps) { - // const routerLocation = useLocation() - const routerLocation = { pathname: 'abcd' } + {item.name} + {item.target === '_new' && } + {!!item.notifications && } + + ) +} - const isActive = (prefix: string) => - routerLocation.pathname.startsWith(prefix + '/') || routerLocation.pathname === prefix +function SidebarMenuItem(item: SidebarMenuItemProps) { + return ( + + + + + + ) +} - const subItemIconOrDefault = (icon?: IconDefinition) => - icon ? ( - - ) : ( - - - - ) +interface SidebarMenuItemGroupProps extends SidebarMenuItemProps { + children?: Array +} +function SidebarMenuItemGroup(item: SidebarMenuItemGroupProps) { return ( - - {navItems - .filter((item) => item.show !== false) - .map((item) => - item.path ? ( - - - - {item.name} - {!!item.notifications && } - - - ) : ( - - - {item.name} - {!!item.notifications && } - - } - > - {item.dropdown?.map((subItem) => ( - - {subItemIconOrDefault(subItem.icon)} -
{subItem.name}
- {subItem.target === '_new' && } -
- ))} -
- ) - )} -
+ } to={item.path}> + {item.children} + ) } @@ -182,8 +140,55 @@ export const MySidebar = memo(function MySidebar() { - - + + + + + + + + + + + + + + {window.localStorage.getItem('show_companion_cloud') === '1' && ( + + )} + + + + + + + + + + + + + setUnfoldable((val) => !val)} /> @@ -287,3 +292,132 @@ function CSidebar({ children, unfoldable }: React.PropsWithChildren ) } + +interface CNavGroupProps { + to?: string + + /** + * A string of all className you want applied to the component. + */ + className?: string + /** + * Make nav group more compact by cutting all `padding` in half. + */ + compact?: boolean + /** + * Set group toggler label. + */ + toggler: ReactNode + /** + * Show nav group items. + */ + visible?: boolean +} + +/* + * A variant of CNavGroup from coreui-react that allows for making the group item be a link + */ +function CNavGroup({ + children, + to, + className, + compact, + toggler, + visible, + ...rest +}: React.PropsWithChildren) { + const [height, setHeight] = useState() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const navItemsRef = useRef(null) + + const [_visible, setVisible] = useState(Boolean(visible)) + + const handleTogglerOnCLick = (event: React.MouseEvent) => { + event.preventDefault() + setVisible(!_visible) + } + + const style: CSSProperties = { + height: 0, + } + + const onEntering = () => { + navItemsRef.current && setHeight(navItemsRef.current.scrollHeight) + } + + const onEntered = () => { + setHeight('auto') + } + + const onExit = () => { + navItemsRef.current && setHeight(navItemsRef.current.scrollHeight) + } + + const onExiting = () => { + // @ts-expect-error reflow is necessary to get correct height of the element + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const reflow = navItemsRef.current?.offsetHeight + setHeight(0) + } + + const onExited = () => { + setHeight(0) + } + + const transitionStyles = { + entering: { display: 'block', height: height }, + entered: { display: 'block', height: height }, + exiting: { display: 'block', height: height }, + exited: { height: height }, + unmounted: {}, + } + + return ( +
  • + {to ? ( + handleTogglerOnCLick(event)}> + { + e.stopPropagation() + setVisible(true) + }} + > + {toggler} + + + ) : ( + handleTogglerOnCLick(event)}> + {toggler} + + )} + + + {(state) => ( +
      + {children} +
    + )} +
    +
  • + ) +} diff --git a/webui/src/Surfaces/KnownSurfacesTable.tsx b/webui/src/Surfaces/KnownSurfacesTable.tsx index 26e40a15b..4962999ff 100644 --- a/webui/src/Surfaces/KnownSurfacesTable.tsx +++ b/webui/src/Surfaces/KnownSurfacesTable.tsx @@ -12,7 +12,7 @@ import { observer } from 'mobx-react-lite' import { NonIdealState } from '../Components/NonIdealState.js' import { WindowLinkOpen } from '../Helpers/Window.js' -export const KnownSurfacesTable = observer(function SurfacesPage() { +export const KnownSurfacesTable = observer(function KnownSurfacesTable() { const { surfaces, socket } = useContext(RootAppStoreContext) const editModalRef = useRef(null) diff --git a/webui/src/Surfaces/index.tsx b/webui/src/Surfaces/index.tsx index e8ab5e03a..03c04707a 100644 --- a/webui/src/Surfaces/index.tsx +++ b/webui/src/Surfaces/index.tsx @@ -1,49 +1,12 @@ import React, { useCallback, useContext, useRef, useState } from 'react' -import { CAlert, CButton, CButtonGroup, CCallout, CNav, CNavItem, CNavLink, CTabContent, CTabPane } from '@coreui/react' +import { CAlert, CButton, CButtonGroup, CCallout } from '@coreui/react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faAdd, faSync } from '@fortawesome/free-solid-svg-icons' import { AddSurfaceGroupModal, AddSurfaceGroupModalRef } from './AddGroupModal.js' import { RootAppStoreContext } from '../Stores/RootAppStore.js' -import { observer } from 'mobx-react-lite' import { SurfaceDiscoveryTable } from './SurfaceDiscoveryTable.js' import { KnownSurfacesTable } from './KnownSurfacesTable.js' import { OutboundSurfacesTable } from './OutboundSurfacesTable.js' -import { Link, Outlet } from '@tanstack/react-router' - -export const SurfacesPage = observer(function SurfacesPage() { - return ( -
    -
    -

    Surfaces

    -
    - -
    - - - - Configured Surfaces - - - - - Discover - - - - - Remote Surfaces - - - - - - - - -
    -
    - ) -}) export function ConfiguredSurfacesTab() { const { socket } = useContext(RootAppStoreContext) @@ -81,6 +44,8 @@ export function ConfiguredSurfacesTab() { return ( <> +

    Configured Surfaces

    +

    Currently connected surfaces. If your streamdeck is missing from this list, you might need to close the Elgato Streamdeck application and click the Rescan button below. @@ -121,6 +86,8 @@ export function ConfiguredSurfacesTab() { export function DiscoverSurfacesTab() { return ( <> +

    Discover Surfaces

    +

    Discovered remote surfaces, such as Companion Satellite and Stream Deck Studio will be listed here. You can easily configure them to connect to Companion from here. @@ -136,6 +103,8 @@ export function DiscoverSurfacesTab() { export function OutboundSurfacesTab() { return ( <> +

    Remote Surfaces

    +

    The Stream Deck Studio supports network connection. You can set up the connection from Companion here, or use the Discovered Surfaces tab. diff --git a/webui/src/routeTree.gen.ts b/webui/src/routeTree.gen.ts index c44248ccd..5757e0fbd 100644 --- a/webui/src/routeTree.gen.ts +++ b/webui/src/routeTree.gen.ts @@ -27,7 +27,6 @@ import { Route as IndexImport } from './routes/app/index.tsx' import { Route as ConnectionDebugconnectionIdImport } from './routes/self-contained/connection-debug.$connectionId.tsx' import { Route as VariablesImport } from './routes/app/variables.tsx' import { Route as TriggersImport } from './routes/app/triggers.tsx' -import { Route as SurfacesImport } from './routes/app/surfaces.tsx' import { Route as SettingsImport } from './routes/app/settings.tsx' import { Route as LogImport } from './routes/app/log.tsx' import { Route as ImportExportImport } from './routes/app/import-export.tsx' @@ -163,12 +162,6 @@ const TriggersRoute = TriggersImport.update({ getParentRoute: () => appRoute, } as any) -const SurfacesRoute = SurfacesImport.update({ - id: '/surfaces', - path: '/surfaces', - getParentRoute: () => appRoute, -} as any) - const SettingsRoute = SettingsImport.update({ id: '/settings', path: '/settings', @@ -224,27 +217,27 @@ const TriggersControlIdRoute = TriggersControlIdImport.update({ } as any) const SurfacesOutboundRoute = SurfacesOutboundImport.update({ - id: '/outbound', - path: '/outbound', - getParentRoute: () => SurfacesRoute, + id: '/surfaces/outbound', + path: '/surfaces/outbound', + getParentRoute: () => appRoute, } as any) const SurfacesDiscoverRoute = SurfacesDiscoverImport.update({ - id: '/discover', - path: '/discover', - getParentRoute: () => SurfacesRoute, + id: '/surfaces/discover', + path: '/surfaces/discover', + getParentRoute: () => appRoute, } as any) const SurfacesConfiguredRoute = SurfacesConfiguredImport.update({ - id: '/configured', - path: '/configured', - getParentRoute: () => SurfacesRoute, + id: '/surfaces/configured', + path: '/surfaces/configured', + getParentRoute: () => appRoute, } as any) const SurfacesSplatRoute = SurfacesSplatImport.update({ - id: '/$', - path: '/$', - getParentRoute: () => SurfacesRoute, + id: '/surfaces/$', + path: '/surfaces/$', + getParentRoute: () => appRoute, } as any) const ButtonsPageRoute = ButtonsPageImport.update({ @@ -383,13 +376,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsImport parentRoute: typeof appImport } - '/_app/surfaces': { - id: '/_app/surfaces' - path: '/surfaces' - fullPath: '/surfaces' - preLoaderRoute: typeof SurfacesImport - parentRoute: typeof appImport - } '/_app/triggers': { id: '/_app/triggers' path: '/triggers' @@ -441,31 +427,31 @@ declare module '@tanstack/react-router' { } '/_app/surfaces/$': { id: '/_app/surfaces/$' - path: '/$' + path: '/surfaces/$' fullPath: '/surfaces/$' preLoaderRoute: typeof SurfacesSplatImport - parentRoute: typeof SurfacesImport + parentRoute: typeof appImport } '/_app/surfaces/configured': { id: '/_app/surfaces/configured' - path: '/configured' + path: '/surfaces/configured' fullPath: '/surfaces/configured' preLoaderRoute: typeof SurfacesConfiguredImport - parentRoute: typeof SurfacesImport + parentRoute: typeof appImport } '/_app/surfaces/discover': { id: '/_app/surfaces/discover' - path: '/discover' + path: '/surfaces/discover' fullPath: '/surfaces/discover' preLoaderRoute: typeof SurfacesDiscoverImport - parentRoute: typeof SurfacesImport + parentRoute: typeof appImport } '/_app/surfaces/outbound': { id: '/_app/surfaces/outbound' - path: '/outbound' + path: '/surfaces/outbound' fullPath: '/surfaces/outbound' preLoaderRoute: typeof SurfacesOutboundImport - parentRoute: typeof SurfacesImport + parentRoute: typeof appImport } '/_app/triggers/$controlId': { id: '/_app/triggers/$controlId' @@ -497,24 +483,6 @@ const ButtonsRouteChildren: ButtonsRouteChildren = { const ButtonsRouteWithChildren = ButtonsRoute._addFileChildren(ButtonsRouteChildren) -interface SurfacesRouteChildren { - SurfacesSplatRoute: typeof SurfacesSplatRoute - SurfacesConfiguredRoute: typeof SurfacesConfiguredRoute - SurfacesDiscoverRoute: typeof SurfacesDiscoverRoute - SurfacesOutboundRoute: typeof SurfacesOutboundRoute -} - -const SurfacesRouteChildren: SurfacesRouteChildren = { - SurfacesSplatRoute: SurfacesSplatRoute, - SurfacesConfiguredRoute: SurfacesConfiguredRoute, - SurfacesDiscoverRoute: SurfacesDiscoverRoute, - SurfacesOutboundRoute: SurfacesOutboundRoute, -} - -const SurfacesRouteWithChildren = SurfacesRoute._addFileChildren( - SurfacesRouteChildren, -) - interface TriggersRouteChildren { TriggersControlIdRoute: typeof TriggersControlIdRoute TriggersIndexRoute: typeof TriggersIndexRoute @@ -537,10 +505,13 @@ interface appRouteChildren { ImportExportRoute: typeof ImportExportRoute LogRoute: typeof LogRoute SettingsRoute: typeof SettingsRoute - SurfacesRoute: typeof SurfacesRouteWithChildren TriggersRoute: typeof TriggersRouteWithChildren VariablesRoute: typeof VariablesRoute IndexRoute: typeof IndexRoute + SurfacesSplatRoute: typeof SurfacesSplatRoute + SurfacesConfiguredRoute: typeof SurfacesConfiguredRoute + SurfacesDiscoverRoute: typeof SurfacesDiscoverRoute + SurfacesOutboundRoute: typeof SurfacesOutboundRoute } const appRouteChildren: appRouteChildren = { @@ -551,10 +522,13 @@ const appRouteChildren: appRouteChildren = { ImportExportRoute: ImportExportRoute, LogRoute: LogRoute, SettingsRoute: SettingsRoute, - SurfacesRoute: SurfacesRouteWithChildren, TriggersRoute: TriggersRouteWithChildren, VariablesRoute: VariablesRoute, IndexRoute: IndexRoute, + SurfacesSplatRoute: SurfacesSplatRoute, + SurfacesConfiguredRoute: SurfacesConfiguredRoute, + SurfacesDiscoverRoute: SurfacesDiscoverRoute, + SurfacesOutboundRoute: SurfacesOutboundRoute, } const appRouteWithChildren = appRoute._addFileChildren(appRouteChildren) @@ -578,7 +552,6 @@ export interface FileRoutesByFullPath { '/import-export': typeof ImportExportRoute '/log': typeof LogRoute '/settings': typeof SettingsRoute - '/surfaces': typeof SurfacesRouteWithChildren '/triggers': typeof TriggersRouteWithChildren '/variables': typeof VariablesRoute '/connection-debug/$connectionId': typeof ConnectionDebugconnectionIdRoute @@ -612,7 +585,6 @@ export interface FileRoutesByTo { '/import-export': typeof ImportExportRoute '/log': typeof LogRoute '/settings': typeof SettingsRoute - '/surfaces': typeof SurfacesRouteWithChildren '/variables': typeof VariablesRoute '/connection-debug/$connectionId': typeof ConnectionDebugconnectionIdRoute '/emulator/$emulatorId': typeof EmulatorEmulatorIdlazyRoute @@ -647,7 +619,6 @@ export interface FileRoutesById { '/_app/import-export': typeof ImportExportRoute '/_app/log': typeof LogRoute '/_app/settings': typeof SettingsRoute - '/_app/surfaces': typeof SurfacesRouteWithChildren '/_app/triggers': typeof TriggersRouteWithChildren '/_app/variables': typeof VariablesRoute '/connection-debug/$connectionId': typeof ConnectionDebugconnectionIdRoute @@ -684,7 +655,6 @@ export interface FileRouteTypes { | '/import-export' | '/log' | '/settings' - | '/surfaces' | '/triggers' | '/variables' | '/connection-debug/$connectionId' @@ -717,7 +687,6 @@ export interface FileRouteTypes { | '/import-export' | '/log' | '/settings' - | '/surfaces' | '/variables' | '/connection-debug/$connectionId' | '/emulator/$emulatorId' @@ -750,7 +719,6 @@ export interface FileRouteTypes { | '/_app/import-export' | '/_app/log' | '/_app/settings' - | '/_app/surfaces' | '/_app/triggers' | '/_app/variables' | '/connection-debug/$connectionId' @@ -837,10 +805,13 @@ export const routeTree = rootRoute "/_app/import-export", "/_app/log", "/_app/settings", - "/_app/surfaces", "/_app/triggers", "/_app/variables", - "/_app/" + "/_app/", + "/_app/surfaces/$", + "/_app/surfaces/configured", + "/_app/surfaces/discover", + "/_app/surfaces/outbound" ] }, "/emulator.html": { @@ -904,16 +875,6 @@ export const routeTree = rootRoute "filePath": "app/settings.tsx", "parent": "/_app" }, - "/_app/surfaces": { - "filePath": "app/surfaces.tsx", - "parent": "/_app", - "children": [ - "/_app/surfaces/$", - "/_app/surfaces/configured", - "/_app/surfaces/discover", - "/_app/surfaces/outbound" - ] - }, "/_app/triggers": { "filePath": "app/triggers.tsx", "parent": "/_app", @@ -945,19 +906,19 @@ export const routeTree = rootRoute }, "/_app/surfaces/$": { "filePath": "app/surfaces/$.tsx", - "parent": "/_app/surfaces" + "parent": "/_app" }, "/_app/surfaces/configured": { "filePath": "app/surfaces/configured.tsx", - "parent": "/_app/surfaces" + "parent": "/_app" }, "/_app/surfaces/discover": { "filePath": "app/surfaces/discover.tsx", - "parent": "/_app/surfaces" + "parent": "/_app" }, "/_app/surfaces/outbound": { "filePath": "app/surfaces/outbound.tsx", - "parent": "/_app/surfaces" + "parent": "/_app" }, "/_app/triggers/$controlId": { "filePath": "app/triggers/$controlId.tsx", diff --git a/webui/src/routes/app/surfaces.tsx b/webui/src/routes/app/surfaces.tsx deleted file mode 100644 index 7be9f1520..000000000 --- a/webui/src/routes/app/surfaces.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { SurfacesPage } from '../../Surfaces/index.js' - -export const Route = createFileRoute('/_app/surfaces')({ - component: SurfacesPage, -}) diff --git a/webui/src/scss/_layout.scss b/webui/src/scss/_layout.scss index c2d239463..9c33e5208 100644 --- a/webui/src/scss/_layout.scss +++ b/webui/src/scss/_layout.scss @@ -237,10 +237,20 @@ body { .nav-group-toggle::after { transform: rotate(180deg); } - &.show > .nav-group-toggle::after { + .show > .nav-group-toggle::after { transform: rotate(0deg); } } + + .nav-group-toggle-link { + padding-top: 0; + padding-left: 0; + padding-bottom: 0; + + .nav-link { + margin-right: 16px; + } + } } .secondary-panel-inner {