mirror of
https://github.com/docmost/docmost.git
synced 2025-11-20 23:11:10 +10:00
refactor layout
* ui polishing * frontend and backend fixes
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
42
apps/client/src/components/layouts/global/aside.tsx
Normal file
42
apps/client/src/components/layouts/global/aside.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Box, ScrollArea, Text } from "@mantine/core";
|
||||
import CommentList from "@/features/comment/components/comment-list.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
export default function Aside() {
|
||||
const [{ tab }] = useAtom(asideStateAtom);
|
||||
|
||||
let title: string;
|
||||
let component: ReactNode;
|
||||
|
||||
switch (tab) {
|
||||
case "comments":
|
||||
component = <CommentList />;
|
||||
title = "Comments";
|
||||
break;
|
||||
default:
|
||||
component = null;
|
||||
title = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p="md">
|
||||
{component && (
|
||||
<>
|
||||
<Text mb="md" fw={500}>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<ScrollArea
|
||||
style={{ height: "85vh" }}
|
||||
scrollbarSize={5}
|
||||
type="scroll"
|
||||
>
|
||||
<div style={{ paddingBottom: "200px" }}>{component}</div>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
export function useToggleSidebar(sidebarAtom: any) {
|
||||
const [sidebarState, setSidebarState] = useAtom(sidebarAtom);
|
||||
return () => {
|
||||
setSidebarState(!sidebarState);
|
||||
}
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user