Start work on tab layout
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
import type { Link } from 'type-route';
|
||||||
|
|
||||||
|
export interface TabProps {
|
||||||
|
link?: Link;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tab = ({ link, icon, title, active }: TabProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="group relative flex-grow">
|
||||||
|
<div
|
||||||
|
className={`peer flex cursor-pointer select-none py-1 dark:text-white ${
|
||||||
|
active
|
||||||
|
? 'z-10 -mr-1 rounded-t-lg bg-gray-100 dark:bg-primaryDark'
|
||||||
|
: 'dark:bg-secondaryDark'
|
||||||
|
}`}
|
||||||
|
{...(link && link)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-full px-2 ${
|
||||||
|
active ? '' : 'border-l group-first:border-l-0 dark:border-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="my-auto">{icon}</div>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,6 +19,7 @@ export const IconButton = ({
|
|||||||
nested,
|
nested,
|
||||||
active,
|
active,
|
||||||
disabled,
|
disabled,
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
}: IconButtonProps): JSX.Element => {
|
}: IconButtonProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
@@ -39,7 +40,7 @@ export const IconButton = ({
|
|||||||
disabled
|
disabled
|
||||||
? 'cursor-not-allowed text-gray-400 dark:text-gray-700'
|
? 'cursor-not-allowed text-gray-400 dark:text-gray-700'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
} ${className ?? ''}`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import type React from 'react';
|
|
||||||
|
|
||||||
import { FiMessageCircle, FiSettings } from 'react-icons/fi';
|
|
||||||
import { RiMindMap, RiRoadMapLine } from 'react-icons/ri';
|
|
||||||
import { VscExtensions } from 'react-icons/vsc';
|
|
||||||
|
|
||||||
import { NavLinkButton } from '@components/layout/Sidebar/NavLinkButton';
|
|
||||||
import { routes, useRoute } from '@core/router';
|
|
||||||
import { toggleMobileNav } from '@core/slices/appSlice';
|
|
||||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
|
||||||
|
|
||||||
export interface ButtonNavProps {
|
|
||||||
toggleSettingsOpen: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ButtonNav = ({
|
|
||||||
toggleSettingsOpen,
|
|
||||||
}: ButtonNavProps): JSX.Element => {
|
|
||||||
const route = useRoute();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="z-30 flex justify-between border-t border-gray-300 bg-white px-6 py-2 dark:border-gray-600 dark:bg-primaryDark">
|
|
||||||
<div
|
|
||||||
onClick={(): void => {
|
|
||||||
dispatch(toggleMobileNav());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NavLinkButton
|
|
||||||
active={route.name === 'messages'}
|
|
||||||
link={routes.messages().link}
|
|
||||||
>
|
|
||||||
<FiMessageCircle className="h-5 w-5" />
|
|
||||||
</NavLinkButton>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={(): void => {
|
|
||||||
dispatch(toggleMobileNav());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NavLinkButton
|
|
||||||
active={route.name === 'nodes'}
|
|
||||||
link={routes.nodes().link}
|
|
||||||
>
|
|
||||||
<RiMindMap className="h-5 w-5" />
|
|
||||||
</NavLinkButton>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={(): void => {
|
|
||||||
dispatch(toggleMobileNav());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NavLinkButton active={route.name === 'map'} link={routes.map().link}>
|
|
||||||
<RiRoadMapLine className="h-5 w-5" />
|
|
||||||
</NavLinkButton>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
onClick={(): void => {
|
|
||||||
dispatch(toggleMobileNav());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NavLinkButton
|
|
||||||
active={route.name === 'extensions'}
|
|
||||||
link={routes.extensions().link}
|
|
||||||
>
|
|
||||||
<VscExtensions className="h-5 w-5" />
|
|
||||||
</NavLinkButton>
|
|
||||||
</div>
|
|
||||||
<NavLinkButton
|
|
||||||
action={(): void => {
|
|
||||||
toggleSettingsOpen();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FiSettings className="h-5 w-5" />
|
|
||||||
</NavLinkButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import type React from 'react';
|
|
||||||
|
|
||||||
import { m } from 'framer-motion';
|
|
||||||
import type { Link } from 'type-route';
|
|
||||||
|
|
||||||
export interface NavLinkButtonProps {
|
|
||||||
link?: Link;
|
|
||||||
active?: boolean;
|
|
||||||
action?: () => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NavLinkButton = ({
|
|
||||||
link,
|
|
||||||
active,
|
|
||||||
action,
|
|
||||||
children,
|
|
||||||
}: NavLinkButtonProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<m.div
|
|
||||||
whileHover={{ scale: 1.01 }}
|
|
||||||
whileTap={{ scale: 0.99 }}
|
|
||||||
animate={active ? 'selected' : 'deselected'}
|
|
||||||
initial={{ borderColor: '#1C1D23' }}
|
|
||||||
variants={{
|
|
||||||
selected: { borderColor: '#67ea94' },
|
|
||||||
deselected: { borderColor: '#1C1D23' },
|
|
||||||
}}
|
|
||||||
className="cursor-pointer rounded-full border-2 p-3 hover:bg-opacity-80 hover:shadow-md dark:bg-secondaryDark dark:text-white"
|
|
||||||
onClick={(): void => {
|
|
||||||
action && action();
|
|
||||||
}}
|
|
||||||
{...(link && link)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</m.div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { ButtonNav } from '@components/layout/Sidebar/ButtonNav';
|
|
||||||
import { Settings } from '@components/layout/Sidebar/Settings/Index';
|
import { Settings } from '@components/layout/Sidebar/Settings/Index';
|
||||||
import { useAppSelector } from '@hooks/useAppSelector';
|
import { useAppSelector } from '@hooks/useAppSelector';
|
||||||
|
|
||||||
export interface SidebarProps {
|
export interface SidebarProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
setSettingsOpen: (settingsOpen: boolean) => void;
|
||||||
|
settingsOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
export const Sidebar = ({
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
settingsOpen,
|
||||||
|
setSettingsOpen,
|
||||||
|
children,
|
||||||
|
}: SidebarProps): JSX.Element => {
|
||||||
const appState = useAppSelector((state) => state.app);
|
const appState = useAppSelector((state) => state.app);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,11 +27,6 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
|||||||
<div className="absolute h-full w-full">{children}</div>
|
<div className="absolute h-full w-full">{children}</div>
|
||||||
<Settings open={settingsOpen} setOpen={setSettingsOpen} />
|
<Settings open={settingsOpen} setOpen={setSettingsOpen} />
|
||||||
</div>
|
</div>
|
||||||
<ButtonNav
|
|
||||||
toggleSettingsOpen={(): void => {
|
|
||||||
setSettingsOpen(!settingsOpen);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { FiMessageCircle, FiSettings } from 'react-icons/fi';
|
||||||
|
import { RiMindMap, RiRoadMapLine } from 'react-icons/ri';
|
||||||
|
import { VscExtensions } from 'react-icons/vsc';
|
||||||
|
|
||||||
|
import { routes, useRoute } from '@app/core/router';
|
||||||
import { IconButton } from '@components/generic/button/IconButton';
|
import { IconButton } from '@components/generic/button/IconButton';
|
||||||
import { Sidebar } from '@components/layout/Sidebar';
|
import { Sidebar } from '@components/layout/Sidebar';
|
||||||
|
|
||||||
import { ErrorFallback } from '../ErrorFallback';
|
import { ErrorFallback } from '../ErrorFallback';
|
||||||
|
import { Tab } from '../Tab';
|
||||||
|
|
||||||
export interface LayoutProps {
|
export interface LayoutProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -20,10 +26,14 @@ export const Layout = ({
|
|||||||
sidebarContents,
|
sidebarContents,
|
||||||
children,
|
children,
|
||||||
}: LayoutProps): JSX.Element => {
|
}: LayoutProps): JSX.Element => {
|
||||||
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full bg-gray-100 dark:bg-secondaryDark md:overflow-hidden md:shadow-xl">
|
<div className="relative flex w-full bg-gray-100 dark:bg-secondaryDark md:overflow-hidden md:shadow-xl">
|
||||||
<div className="flex flex-grow">
|
<div className="flex flex-grow">
|
||||||
<Sidebar>
|
<Sidebar settingsOpen={settingsOpen} setSettingsOpen={setSettingsOpen}>
|
||||||
<div className="flex gap-2 border-b border-gray-300 p-2 dark:border-gray-600">
|
<div className="flex gap-2 border-b border-gray-300 p-2 dark:border-gray-600">
|
||||||
<IconButton icon={icon} />
|
<IconButton icon={icon} />
|
||||||
<div className="my-auto text-lg font-medium dark:text-white">
|
<div className="my-auto text-lg font-medium dark:text-white">
|
||||||
@@ -34,8 +44,45 @@ export const Layout = ({
|
|||||||
</Sidebar>
|
</Sidebar>
|
||||||
</div>
|
</div>
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<div className="flex h-full w-full bg-gray-300 dark:bg-secondaryDark">
|
<div className="flex h-full w-full flex-col bg-gray-300 dark:bg-secondaryDark">
|
||||||
{children}
|
<div className="flex w-full border-b border-gray-300 pr-2 pt-1 dark:border-gray-600">
|
||||||
|
<div className="mx-1">
|
||||||
|
<IconButton
|
||||||
|
className="rounded-b-none"
|
||||||
|
onClick={(): void => {
|
||||||
|
setSettingsOpen(!settingsOpen);
|
||||||
|
}}
|
||||||
|
nested
|
||||||
|
active={settingsOpen}
|
||||||
|
icon={<FiSettings />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Tab
|
||||||
|
title="Messages"
|
||||||
|
icon={<FiMessageCircle />}
|
||||||
|
link={routes.messages().link}
|
||||||
|
active={route.name === 'messages'}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
title="Nodes"
|
||||||
|
icon={<RiMindMap />}
|
||||||
|
link={routes.nodes().link}
|
||||||
|
active={route.name === 'nodes'}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
title="Map"
|
||||||
|
icon={<RiRoadMapLine />}
|
||||||
|
link={routes.map().link}
|
||||||
|
active={route.name === 'map'}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
title="Extensions"
|
||||||
|
icon={<VscExtensions />}
|
||||||
|
link={routes.extensions().link}
|
||||||
|
active={route.name === 'extensions'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-grow">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user