mirror of
https://github.com/docmost/docmost.git
synced 2025-11-18 06:01:11 +10:00
refactor layout
* ui polishing * frontend and backend fixes
This commit is contained in:
@ -2,10 +2,6 @@ import { ActionIcon, rem } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { IconUsersGroup } from "@tabler/icons-react";
|
||||
|
||||
interface IconPeopleCircleProps extends React.ComponentPropsWithoutRef<"svg"> {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function IconGroupCircle() {
|
||||
return (
|
||||
<ActionIcon variant="light" size="lg" color="gray" radius="xl">
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
.breadcrumb {
|
||||
a {
|
||||
color: var(--mantine-color-default-color);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
import { useAtomValue } from "jotai";
|
||||
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { findBreadcrumbPath } from "@/features/page/tree/utils";
|
||||
import {
|
||||
Button,
|
||||
Anchor,
|
||||
Popover,
|
||||
Breadcrumbs,
|
||||
ActionIcon,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { IconDots } from "@tabler/icons-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import classes from "./breadcrumb.module.css";
|
||||
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||
import { buildPageSlug } from "@/features/page/page.utils.ts";
|
||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||
|
||||
function getTitle(name: string, icon: string) {
|
||||
if (icon) {
|
||||
return `${icon} ${name}`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
export default function Breadcrumb() {
|
||||
const treeData = useAtomValue(treeDataAtom);
|
||||
const [breadcrumbNodes, setBreadcrumbNodes] = useState<
|
||||
SpaceTreeNode[] | null
|
||||
>(null);
|
||||
const { slugId } = useParams();
|
||||
const { data: currentPage } = usePageQuery(slugId);
|
||||
|
||||
useEffect(() => {
|
||||
if (treeData?.length > 0 && currentPage) {
|
||||
const breadcrumb = findBreadcrumbPath(treeData, currentPage.id);
|
||||
if (breadcrumb) {
|
||||
setBreadcrumbNodes(breadcrumb);
|
||||
}
|
||||
}
|
||||
}, [currentPage?.id, treeData]);
|
||||
|
||||
const HiddenNodesTooltipContent = () =>
|
||||
breadcrumbNodes?.slice(1, -2).map((node) => (
|
||||
<Button.Group orientation="vertical" key={node.id}>
|
||||
<Button
|
||||
justify="start"
|
||||
component={Link}
|
||||
to={buildPageSlug(node.slugId, node.name)}
|
||||
variant="default"
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
<Text truncate="end">{getTitle(node.name, node.icon)}</Text>
|
||||
</Button>
|
||||
</Button.Group>
|
||||
));
|
||||
|
||||
const getLastNthNode = (n: number) =>
|
||||
breadcrumbNodes && breadcrumbNodes[breadcrumbNodes.length - n];
|
||||
|
||||
const getBreadcrumbItems = () => {
|
||||
if (breadcrumbNodes?.length > 3) {
|
||||
return [
|
||||
<Anchor
|
||||
component={Link}
|
||||
to={buildPageSlug(breadcrumbNodes[0].slugId, breadcrumbNodes[0].name)}
|
||||
underline="never"
|
||||
key={breadcrumbNodes[0].slugId}
|
||||
>
|
||||
{getTitle(breadcrumbNodes[0].name, breadcrumbNodes[0].icon)}
|
||||
</Anchor>,
|
||||
<Popover
|
||||
width={250}
|
||||
position="bottom"
|
||||
withArrow
|
||||
shadow="xl"
|
||||
key="hidden-nodes"
|
||||
>
|
||||
<Popover.Target>
|
||||
<ActionIcon c="gray" variant="transparent">
|
||||
<IconDots size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<HiddenNodesTooltipContent />
|
||||
</Popover.Dropdown>
|
||||
</Popover>,
|
||||
<Anchor
|
||||
component={Link}
|
||||
to={buildPageSlug(getLastNthNode(2)?.slugId, getLastNthNode(2)?.name)}
|
||||
underline="never"
|
||||
key={getLastNthNode(2)?.slugId}
|
||||
>
|
||||
{getTitle(getLastNthNode(2)?.name, getLastNthNode(2)?.icon)}
|
||||
</Anchor>,
|
||||
<Anchor
|
||||
component={Link}
|
||||
to={buildPageSlug(getLastNthNode(1)?.slugId, getLastNthNode(1)?.name)}
|
||||
underline="never"
|
||||
key={getLastNthNode(1)?.slugId}
|
||||
>
|
||||
{getTitle(getLastNthNode(1)?.name, getLastNthNode(1)?.icon)}
|
||||
</Anchor>,
|
||||
];
|
||||
}
|
||||
|
||||
if (breadcrumbNodes) {
|
||||
return breadcrumbNodes.map((node) => (
|
||||
<Anchor
|
||||
component={Link}
|
||||
to={buildPageSlug(node.slugId, node.name)}
|
||||
underline="never"
|
||||
key={node.id}
|
||||
>
|
||||
{getTitle(node.name, node.icon)}
|
||||
</Anchor>
|
||||
));
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.breadcrumb}>
|
||||
{breadcrumbNodes ? (
|
||||
<Breadcrumbs>{getBreadcrumbItems()}</Breadcrumbs>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import { UserProvider } from "@/features/user/user-provider.tsx";
|
||||
import Shell from "./shell.tsx";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
|
||||
export default function DashboardLayout() {
|
||||
return (
|
||||
<UserProvider>
|
||||
<Shell>
|
||||
<Helmet>
|
||||
<title>Home</title>
|
||||
</Helmet>
|
||||
<Outlet />
|
||||
</Shell>
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
import { ActionIcon, Menu, Tooltip } from "@mantine/core";
|
||||
import {
|
||||
IconDots,
|
||||
IconHistory,
|
||||
IconLink,
|
||||
IconMessage,
|
||||
} from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { buildPageSlug } from "@/features/page/page.utils.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export default function Header() {
|
||||
const toggleAside = useToggleAside();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
<Button variant="default" style={{ border: "none" }} size="compact-sm">
|
||||
Share
|
||||
</Button>
|
||||
*/}
|
||||
|
||||
<Tooltip label="Comments" openDelay={250} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
style={{ border: "none" }}
|
||||
onClick={() => toggleAside("comments")}
|
||||
>
|
||||
<IconMessage size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<PageActionMenu />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PageActionMenu() {
|
||||
const [, setHistoryModalOpen] = useAtom(historyAtoms);
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const { slugId } = useParams();
|
||||
const { data: page, isLoading, isError } = usePageQuery(slugId);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const pageLink =
|
||||
window.location.host + buildPageSlug(page.slugId, page.title);
|
||||
clipboard.copy(pageLink);
|
||||
notifications.show({ message: "Link copied" });
|
||||
};
|
||||
|
||||
const openHistoryModal = () => {
|
||||
setHistoryModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
shadow="xl"
|
||||
position="bottom-end"
|
||||
offset={20}
|
||||
width={200}
|
||||
withArrow
|
||||
arrowPosition="center"
|
||||
>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="default" style={{ border: "none" }}>
|
||||
<IconDots size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<IconLink size={16} stroke={2} />}
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
Copy link
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
leftSection={<IconHistory size={16} stroke={2} />}
|
||||
onClick={openHistoryModal}
|
||||
>
|
||||
Page history
|
||||
</Menu.Item>
|
||||
|
||||
{/*
|
||||
<Menu.Divider />
|
||||
<Menu.Item leftSection={<IconTrash size={16} stroke={2} />}>
|
||||
Delete
|
||||
</Menu.Item>
|
||||
*/}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
.header,
|
||||
.footer {
|
||||
@media (max-width: 992px) {
|
||||
[data-layout="alt"] & {
|
||||
--_section-right: var(--app-shell-aside-offset, 0px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
|
||||
@media (min-width: 993px) {
|
||||
[data-layout="alt"] & {
|
||||
--_section-top: var(--_section-top, var(--app-shell-header-offset, 0px));
|
||||
--_section-height: var(
|
||||
--_section-height,
|
||||
calc(
|
||||
100dvh - var(--app-shell-header-offset, 0px) -
|
||||
var(--app-shell-footer-offset, 0px)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
.aside {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
.navbar {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
import {
|
||||
asideStateAtom,
|
||||
desktopSidebarAtom,
|
||||
} from "@/components/navbar/atoms/sidebar-atom.ts";
|
||||
import { useToggleSidebar } from "@/components/navbar/hooks/use-toggle-sidebar.ts";
|
||||
import { Navbar } from "@/components/navbar/navbar.tsx";
|
||||
import { AppShell, Burger, Group } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { useAtom } from "jotai";
|
||||
import classes from "./shell.module.css";
|
||||
import Header from "@/components/layouts/dashboard/header.tsx";
|
||||
import Breadcrumb from "@/components/layouts/components/breadcrumb.tsx";
|
||||
import Aside from "@/components/layouts/dashboard/aside.tsx";
|
||||
import { useMatchPath } from "@/hooks/use-match-path.tsx";
|
||||
import React from "react";
|
||||
|
||||
export default function Shell({ children }: { children: React.ReactNode }) {
|
||||
const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] =
|
||||
useDisclosure();
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
|
||||
const matchPath = useMatchPath();
|
||||
const isPageRoute = matchPath("/p/:pageId");
|
||||
const [{ isAsideOpen }] = useAtom(asideStateAtom);
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
layout="alt"
|
||||
header={{ height: 45 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
||||
}}
|
||||
aside={{
|
||||
width: 350,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !isAsideOpen, desktop: !isAsideOpen },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header className={classes.header} withBorder={false}>
|
||||
<Group justify="space-between" h="100%" px="md" wrap="nowrap">
|
||||
<Group
|
||||
h="100%"
|
||||
maw="60%"
|
||||
px="md"
|
||||
wrap="nowrap"
|
||||
style={{ overflow: "hidden" }}
|
||||
>
|
||||
<Burger
|
||||
opened={mobileOpened}
|
||||
onClick={toggleMobile}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
<Burger
|
||||
opened={desktopOpened}
|
||||
onClick={toggleDesktop}
|
||||
visibleFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
{isPageRoute && <Breadcrumb />}
|
||||
</Group>
|
||||
|
||||
{isPageRoute && (
|
||||
<Group justify="flex-end" h="100%" px="md" wrap="nowrap">
|
||||
<Header />
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar className={classes.navbar} withBorder={false}>
|
||||
<Navbar />
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main>{children}</AppShell.Main>
|
||||
|
||||
{isPageRoute && (
|
||||
<AppShell.Aside className={classes.aside} withBorder={false}>
|
||||
<Aside />
|
||||
</AppShell.Aside>
|
||||
)}
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
.header {
|
||||
height: 100%;
|
||||
margin-bottom: rem(120px);
|
||||
background-color: var(--mantine-color-body);
|
||||
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
padding-right: var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
padding: rem(8px) rem(12px);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
text-decoration: none;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 500;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
}
|
||||
}
|
||||
70
apps/client/src/components/layouts/global/app-header.tsx
Normal file
70
apps/client/src/components/layouts/global/app-header.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Group } from "@mantine/core";
|
||||
import { IconSquareLetterDFilled } from "@tabler/icons-react";
|
||||
import classes from "./app-header.module.css";
|
||||
import React from "react";
|
||||
import TopMenu from "@/components/layouts/global/top-menu.tsx";
|
||||
import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { useAtom } from "jotai/index";
|
||||
import {
|
||||
desktopSidebarAtom,
|
||||
mobileSidebarAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
||||
|
||||
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
|
||||
|
||||
export function AppHeader() {
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
|
||||
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
|
||||
|
||||
const isHomeRoute = location.pathname.startsWith("/home");
|
||||
|
||||
const items = links.map((link) => (
|
||||
<Link key={link.label} to={link.link} className={classes.link}>
|
||||
{link.label}
|
||||
</Link>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group h="100%" px="md" justify="space-between" wrap={"nowrap"}>
|
||||
<Group>
|
||||
<IconSquareLetterDFilled size={30} />
|
||||
|
||||
{!isHomeRoute && (
|
||||
<>
|
||||
<SidebarToggle
|
||||
aria-label="sidebar toggle"
|
||||
opened={mobileOpened}
|
||||
onClick={toggleMobile}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<SidebarToggle
|
||||
aria-label="sidebar toggle"
|
||||
opened={desktopOpened}
|
||||
onClick={toggleDesktop}
|
||||
visibleFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Group ml={50} gap={5} className={classes.links} visibleFrom="sm">
|
||||
{items}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Group px={"xl"}>
|
||||
<TopMenu />
|
||||
</Group>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
.header, .navbar, .aside {
|
||||
background-color: light-dark(#f6f7f9, var(--mantine-color-dark-8));
|
||||
}
|
||||
|
||||
.navbar, .aside {
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.aside {
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
margin-top: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Box, ScrollArea, Text } from "@mantine/core";
|
||||
import CommentList from "@/features/comment/components/comment-list.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { asideStateAtom } from "@/components/navbar/atoms/sidebar-atom.ts";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
export default function Aside() {
|
||||
@ -0,0 +1,77 @@
|
||||
import { AppShell, Container } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
asideStateAtom,
|
||||
desktopSidebarAtom,
|
||||
mobileSidebarAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
|
||||
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
|
||||
import Aside from "@/components/layouts/global/aside.tsx";
|
||||
import classes from "./app-shell.module.css";
|
||||
|
||||
export default function GlobalAppShell({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const [{ isAsideOpen }] = useAtom(asideStateAtom);
|
||||
|
||||
const location = useLocation();
|
||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||
const isSpaceRoute = location.pathname.startsWith("/s/");
|
||||
const isHomeRoute = location.pathname.startsWith("/home");
|
||||
const isPageRoute = location.pathname.includes("/p/");
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 45 }}
|
||||
navbar={
|
||||
!isHomeRoute && {
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: {
|
||||
mobile: !mobileOpened,
|
||||
desktop: !desktopOpened,
|
||||
},
|
||||
}
|
||||
}
|
||||
aside={
|
||||
isPageRoute && {
|
||||
width: 350,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !isAsideOpen, desktop: !isAsideOpen },
|
||||
}
|
||||
}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header px="md" className={classes.header}>
|
||||
<AppHeader />
|
||||
</AppShell.Header>
|
||||
{!isHomeRoute && (
|
||||
<AppShell.Navbar className={classes.navbar} withBorder={false}>
|
||||
{isSpaceRoute && <SpaceSidebar />}
|
||||
{isSettingsRoute && <SettingsSidebar />}
|
||||
</AppShell.Navbar>
|
||||
)}
|
||||
<AppShell.Main>
|
||||
{isSettingsRoute ? (
|
||||
<Container size={800}>{children}</Container>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</AppShell.Main>
|
||||
|
||||
{isPageRoute && (
|
||||
<AppShell.Aside className={classes.aside} p="md" withBorder={false}>
|
||||
<Aside />
|
||||
</AppShell.Aside>
|
||||
)}
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { atomWithWebStorage } from "@/lib/jotai-helper.ts";
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const mobileSidebarAtom = atom<boolean>(false);
|
||||
|
||||
export const desktopSidebarAtom = atomWithWebStorage<boolean>(
|
||||
"showSidebar",
|
||||
true,
|
||||
);
|
||||
|
||||
export const desktopAsideAtom = atom<boolean>(false);
|
||||
|
||||
type AsideStateType = {
|
||||
tab: string;
|
||||
isAsideOpen: boolean;
|
||||
};
|
||||
|
||||
export const asideStateAtom = atom<AsideStateType>({
|
||||
tab: "",
|
||||
isAsideOpen: false,
|
||||
});
|
||||
13
apps/client/src/components/layouts/global/layout.tsx
Normal file
13
apps/client/src/components/layouts/global/layout.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { UserProvider } from "@/features/user/user-provider.tsx";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<UserProvider>
|
||||
<GlobalAppShell>
|
||||
<Outlet />
|
||||
</GlobalAppShell>
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
124
apps/client/src/components/layouts/global/top-menu.tsx
Normal file
124
apps/client/src/components/layouts/global/top-menu.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { Avatar, Group, Menu, rem, UnstyledButton, Text } from "@mantine/core";
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconLogout,
|
||||
IconSettings,
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
||||
import { UserAvatar } from "@/components/ui/user-avatar.tsx";
|
||||
|
||||
export default function TopMenu() {
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const { logout } = useAuth();
|
||||
|
||||
const user = currentUser?.user;
|
||||
const workspace = currentUser?.workspace;
|
||||
|
||||
return (
|
||||
<Menu width={250} position="bottom-end" withArrow shadow={"lg"}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
<Group gap={7} wrap={"nowrap"}>
|
||||
<Avatar
|
||||
src={workspace.logo}
|
||||
alt={workspace.name}
|
||||
radius="xl"
|
||||
size={20}
|
||||
/>
|
||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||
{workspace.name}
|
||||
</Text>
|
||||
<IconChevronDown
|
||||
style={{ width: rem(12), height: rem(12) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Workspace</Menu.Label>
|
||||
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
||||
leftSection={
|
||||
<IconSettings
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Workspace settings
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
||||
leftSection={
|
||||
<IconUsers
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Manage members
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Label>Account</Menu.Label>
|
||||
<Menu.Item component={Link} to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<UserAvatar
|
||||
radius="xl"
|
||||
size={"sm"}
|
||||
avatarUrl={user.avatarUrl}
|
||||
name={user.name}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Text size="sm" fw={500} lineClamp={1}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{user.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
||||
leftSection={
|
||||
<IconUserCircle
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
My profile
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item
|
||||
onClick={logout}
|
||||
leftSection={
|
||||
<IconLogout
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import { UserProvider } from "@/features/user/user-provider.tsx";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import SettingsShell from "@/components/layouts/settings/settings-shell.tsx";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
|
||||
export default function SettingsLayout() {
|
||||
return (
|
||||
<UserProvider>
|
||||
<SettingsShell>
|
||||
<Helmet>
|
||||
<title>Settings</title>
|
||||
</Helmet>
|
||||
<Outlet />
|
||||
</SettingsShell>
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
import { desktopSidebarAtom } from "@/components/navbar/atoms/sidebar-atom.ts";
|
||||
import { AppShell, Container } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { useAtom } from "jotai";
|
||||
import React from "react";
|
||||
import SettingsSidebar from "@/components/layouts/settings/settings-sidebar.tsx";
|
||||
|
||||
export default function SettingsShell({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
layout="alt"
|
||||
header={{ height: 45 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Navbar>
|
||||
<SettingsSidebar />
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main>
|
||||
<Container size={800}>{children}</Container>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { atomWithWebStorage } from '@/lib/jotai-helper';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const desktopSidebarAtom = atomWithWebStorage('showSidebar', true);
|
||||
|
||||
export const desktopAsideAtom = atom(false);
|
||||
|
||||
type AsideStateType = {
|
||||
tab: string,
|
||||
isAsideOpen: boolean,
|
||||
}
|
||||
|
||||
export const asideStateAtom = atom<AsideStateType>({
|
||||
tab: '',
|
||||
isAsideOpen: false,
|
||||
});
|
||||
@ -1,92 +0,0 @@
|
||||
.navbar {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-left: calc(var(--mantine-spacing-md) * -1);
|
||||
margin-right: calc(var(--mantine-spacing-md) * -1);
|
||||
margin-bottom: var(--mantine-spacing-md);
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
}
|
||||
|
||||
.searchCode {
|
||||
font-weight: 700;
|
||||
font-size: rem(10px);
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-7));
|
||||
}
|
||||
|
||||
.menuItems {
|
||||
padding-left: calc(var(--mantine-spacing-md) - var(--mantine-spacing-xs));
|
||||
padding-right: calc(var(--mantine-spacing-md) - var(--mantine-spacing-xs));
|
||||
padding-bottom: var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
padding: rem(4px) var(--mantine-spacing-xs);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-weight: 500;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
|
||||
&:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemInner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menuItemIcon {
|
||||
margin-right: var(--mantine-spacing-sm);
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
}
|
||||
|
||||
.pages {
|
||||
padding-left: calc(var(--mantine-spacing-md) - rem(6px));
|
||||
padding-right: calc(var(--mantine-spacing-md) - rem(6px));
|
||||
padding-bottom: var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.pagesHeader {
|
||||
padding-left: calc(var(--mantine-spacing-md) + rem(2px));
|
||||
padding-right: var(--mantine-spacing-md);
|
||||
margin-bottom: rem(5px);
|
||||
}
|
||||
|
||||
.pageLink {
|
||||
display: block;
|
||||
padding: rem(8px) var(--mantine-spacing-xs);
|
||||
text-decoration: none;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
}
|
||||
}
|
||||
|
||||
.activeButton {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
import {
|
||||
UnstyledButton,
|
||||
Text,
|
||||
Group,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
rem,
|
||||
} from "@mantine/core";
|
||||
import { spotlight } from "@mantine/spotlight";
|
||||
import {
|
||||
IconSearch,
|
||||
IconPlus,
|
||||
IconSettings,
|
||||
IconHome,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import classes from "./navbar.module.css";
|
||||
import { UserButton } from "./user-button";
|
||||
import React from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { SearchSpotlight } from "@/features/search/search-spotlight";
|
||||
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import SpaceContent from "@/features/page/tree/components/space-content.tsx";
|
||||
import clsx from "clsx";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
|
||||
interface PrimaryMenuItem {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
path?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const primaryMenu: PrimaryMenuItem[] = [
|
||||
{ icon: IconHome, label: "Home", path: "/home" },
|
||||
{ icon: IconSearch, label: "Search" },
|
||||
{ icon: IconSettings, label: "Settings" },
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const [tree] = useAtom(treeApiAtom);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const handleMenuItemClick = (label: string) => {
|
||||
if (label === "Home") {
|
||||
navigate("/home");
|
||||
}
|
||||
|
||||
if (label === "Search") {
|
||||
spotlight.open();
|
||||
}
|
||||
|
||||
if (label === "Settings") {
|
||||
navigate("/settings/workspace");
|
||||
}
|
||||
};
|
||||
|
||||
function handleCreatePage() {
|
||||
tree?.create({ parentId: null, type: "internal", index: 0 });
|
||||
}
|
||||
|
||||
const primaryMenuItems = primaryMenu.map((menuItem) => (
|
||||
<UnstyledButton
|
||||
key={menuItem.label}
|
||||
className={clsx(
|
||||
classes.menu,
|
||||
location.pathname.toLowerCase() === menuItem?.path
|
||||
? classes.activeButton
|
||||
: "",
|
||||
)}
|
||||
onClick={() => handleMenuItemClick(menuItem.label)}
|
||||
>
|
||||
<div className={classes.menuItemInner}>
|
||||
<menuItem.icon size={18} className={classes.menuItemIcon} stroke={2} />
|
||||
<span>{menuItem.label}</span>
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className={classes.navbar}>
|
||||
<div className={classes.section}>
|
||||
<UserButton />
|
||||
</div>
|
||||
|
||||
<div className={classes.section}>
|
||||
<div className={classes.menuItems}>{primaryMenuItems}</div>
|
||||
</div>
|
||||
|
||||
<div className={classes.section}>
|
||||
<Group className={classes.pagesHeader} justify="space-between">
|
||||
<Text size="xs" fw={500} c="dimmed">
|
||||
Pages
|
||||
</Text>
|
||||
|
||||
<Tooltip label="Create page" withArrow position="right">
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size={18}
|
||||
onClick={handleCreatePage}
|
||||
>
|
||||
<IconPlus
|
||||
style={{ width: rem(12), height: rem(12) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<div className={classes.pages}>
|
||||
<SpaceContent />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<SearchSpotlight />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
.user {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core';
|
||||
import { IconChevronRight } from '@tabler/icons-react';
|
||||
import classes from './user-button.module.css';
|
||||
import { useAtom } from 'jotai/index';
|
||||
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
|
||||
|
||||
export function UserButton() {
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
|
||||
return (
|
||||
<UnstyledButton className={classes.user}>
|
||||
<Group>
|
||||
<Avatar
|
||||
src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-9.png"
|
||||
radius="xl"
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
{currentUser?.user.name}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{currentUser?.user.email}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<IconChevronRight style={{ width: rem(14), height: rem(14) }} stroke={1.5} />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Group, Text, ScrollArea, ActionIcon, rem } from "@mantine/core";
|
||||
import {
|
||||
IconUser,
|
||||
@ -9,7 +9,7 @@ import {
|
||||
IconSpaces,
|
||||
IconBrush,
|
||||
} from "@tabler/icons-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import classes from "./settings.module.css";
|
||||
|
||||
interface DataItem {
|
||||
@ -51,8 +51,13 @@ const groupedData: DataGroup[] = [
|
||||
];
|
||||
|
||||
export default function SettingsSidebar() {
|
||||
const pathname = useLocation().pathname;
|
||||
const [active, setActive] = useState(pathname);
|
||||
const location = useLocation();
|
||||
const [active, setActive] = useState(location.pathname);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setActive(location.pathname);
|
||||
}, [location.pathname]);
|
||||
|
||||
const menuItems = groupedData.map((group) => (
|
||||
<div key={group.heading}>
|
||||
@ -65,9 +70,6 @@ export default function SettingsSidebar() {
|
||||
data-active={active.startsWith(item.path) || undefined}
|
||||
key={item.label}
|
||||
to={item.path}
|
||||
onClick={() => {
|
||||
setActive(item.path);
|
||||
}}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{item.label}</span>
|
||||
@ -77,32 +79,20 @@ export default function SettingsSidebar() {
|
||||
));
|
||||
|
||||
return (
|
||||
<nav className={classes.navbar}>
|
||||
<div>
|
||||
<Group className={classes.header} justify="flex-start">
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
to="/home"
|
||||
variant="transparent"
|
||||
c="gray"
|
||||
aria-label="Home"
|
||||
>
|
||||
<IconArrowLeft stroke={2} />
|
||||
</ActionIcon>
|
||||
<Text fw={500}>Settings</Text>
|
||||
</Group>
|
||||
<div className={classes.navbar}>
|
||||
<Group className={classes.title} justify="flex-start">
|
||||
<ActionIcon
|
||||
onClick={() => navigate(-1)}
|
||||
variant="transparent"
|
||||
c="gray"
|
||||
aria-label="Back"
|
||||
>
|
||||
<IconArrowLeft stroke={2} />
|
||||
</ActionIcon>
|
||||
<Text fw={500}>Settings</Text>
|
||||
</Group>
|
||||
|
||||
<ScrollArea h="80vh" w="100%">
|
||||
{menuItems}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className={classes.footer}>
|
||||
<Link to="/home" className={classes.link}>
|
||||
<IconArrowLeft className={classes.linkIcon} stroke={1.5} />
|
||||
<span>Return to the app</span>
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
<ScrollArea w="100%">{menuItems}</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,29 +1,16 @@
|
||||
.navbar {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/*border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));*/
|
||||
}
|
||||
|
||||
.navbarMain {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: var(--mantine-spacing-md);
|
||||
margin-bottom: calc(var(--mantine-spacing-md) * 1.5);
|
||||
.title {
|
||||
padding-bottom: var(--mantine-spacing-sm);
|
||||
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: var(--mantine-spacing-md);
|
||||
margin-top: var(--mantine-spacing-md);
|
||||
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
21
apps/client/src/components/ui/page-list-skeleton.tsx
Normal file
21
apps/client/src/components/ui/page-list-skeleton.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Skeleton } from '@mantine/core';
|
||||
|
||||
export default function PageListSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
<Skeleton height={25} my="xs" radius="xs" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
34
apps/client/src/components/ui/sidebar-toggle-button.tsx
Normal file
34
apps/client/src/components/ui/sidebar-toggle-button.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import {
|
||||
IconLayoutSidebarRightCollapse,
|
||||
IconLayoutSidebarRightExpand,
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
ActionIcon,
|
||||
BoxProps,
|
||||
ElementProps,
|
||||
MantineColor,
|
||||
MantineSize,
|
||||
} from "@mantine/core";
|
||||
import React from "react";
|
||||
|
||||
export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
||||
size?: MantineSize | `compact-${MantineSize}` | (string & {});
|
||||
color?: MantineColor;
|
||||
opened?: boolean;
|
||||
}
|
||||
|
||||
export default function SidebarToggle({
|
||||
opened,
|
||||
size = "sm",
|
||||
...others
|
||||
}: SidebarToggleProps) {
|
||||
return (
|
||||
<ActionIcon size={size} {...others} variant="subtle" color="gray">
|
||||
{opened ? (
|
||||
<IconLayoutSidebarRightExpand />
|
||||
) : (
|
||||
<IconLayoutSidebarRightCollapse />
|
||||
)}
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user