create main menu panel LO

This commit is contained in:
yzned
2025-07-20 16:41:40 +03:00
committed by yzned
parent 39b3a837df
commit 42b27b7ddc
13 changed files with 237 additions and 32 deletions

View File

@@ -1,17 +0,0 @@
import { observer } from "mobx-react-lite";
import { useAccountStore } from "@/contexts/AccountContext";
import { Button } from "./ui/button";
export const MainMenu = observer(() => {
const { isMainMenuOpen, setIsMainMenuOpen } = useAccountStore();
return (
<div
data-open={isMainMenuOpen}
className="w-[30px] data-[open=true]:w-[124px] transition-all border-r p-2"
>
<Button onClick={() => setIsMainMenuOpen(!isMainMenuOpen)}>
{isMainMenuOpen ? "Close" : "Open"}
</Button>
</div>
);
});

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,199 @@
import { observer } from "mobx-react-lite";
import { useAccountStore } from "@/contexts/AccountContext";
import Plus from "@/icons/plus.svg?react";
import Books from "@/icons/books.svg?react";
import Discord from "@/icons/discord.svg?react";
import Profile from "@/icons/profile.svg?react";
import { LINKS } from "@/lib/constants";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { Button, type ButtonVariantType } from "../ui/button";
import { Link } from "@tanstack/react-router";
import Logo from "@/icons/logo.svg?react";
import LogoWithText from "@/icons/logo-with-text.svg?react";
import Sidebar from "@/icons/sidebar.svg?react";
interface MenuItemType extends ButtonVariantType {
name: string;
icon: ReactNode;
path: string;
}
const MENU_ITEMS: MenuItemType[] = [
{
name: "New Chat",
icon: <Plus className="scale-80" />,
path: "/",
variant: "primary",
},
{
name: "All chats",
icon: <Books className="scale-90" />,
path: "/all-chats",
variant: "secondary",
},
{
name: "Support",
icon: <Discord width={12} height={12} className="text-[#5865F2]" />,
path: LINKS.discord,
variant: "secondary",
},
];
const MenuItem = ({ item }: { item: MenuItemType }) => {
const { isMainMenuOpen } = useAccountStore();
return (
<Link
to={item.path}
className="group flex items-center gap-3 w-full whitespace-nowrap"
viewTransition={{ types: ["warp"] }}
>
<Button
data-variant={item.variant}
variant={item.variant}
className="w-8 h-8 data-[variant=secondary]:group-hover:bg-fill-150 data-[variant=primary]:group-hover:bg-fill-700 transition-colors"
>
{item.icon}
</Button>
<div
data-open={isMainMenuOpen}
className="data-[open=true]:opacity-100 opacity-0 transition-opacity duration-200 font-[500] text-[14px]"
>
{item.name}
</div>
</Link>
);
};
const MainMenu = observer(() => {
const { isMainMenuOpen } = useAccountStore();
return (
<div
data-open={isMainMenuOpen}
className="w-[64px] data-[open=true]:w-[227px] transition-all border-r border-r border-fill-150"
>
<MainMenuContent />
</div>
);
});
const MOCK_CHATS = Array.from({ length: 20 }, (_, i) => ({
id: i,
title: `Chat ${i + 1}`,
subtitle: `Last message preview...`,
}));
const MainMenuContent = observer(() => {
const { isMainMenuOpen, setIsMainMenuOpen } = useAccountStore();
const containerRef = useRef<HTMLDivElement>(null);
const [hasScrolled, setHasScrolled] = useState(false);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = () => {
setHasScrolled(container.scrollTop > 0);
};
container.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
container.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<div
ref={containerRef}
className="relative w-full h-full overflow-auto flex flex-col"
>
<header
data-scrolled={hasScrolled}
className="data-[scrolled=true]:border-b data-[scrolled=true]:border-fill-150 sticky top-0 z-10 bg-white pt-[30px] space-y-12 flex flex-col w-full px-4 pb-4 transition-shadow"
>
<div className="relative w-full h-[32px]">
{isMainMenuOpen ? (
<div className="flex items-center justify-between w-full">
<LogoWithText />
<Button
className="w-8 h-8 hover:cursor-w-resize"
variant="ghost"
onClick={() => setIsMainMenuOpen(false)}
>
<Sidebar />
</Button>
</div>
) : (
<div className="relative group px-1">
<Logo className="transition-opacity duration-150 group-hover:opacity-0" />
<Button
className="w-8 h-8 absolute top-0 left-0 opacity-0 group-hover:opacity-100 transition-opacity duration-150 hover:cursor-e-resize"
variant="ghost"
onClick={() => setIsMainMenuOpen(true)}
>
<Sidebar />
</Button>
</div>
)}
</div>
<div
data-open={isMainMenuOpen}
className="space-y-4 w-full data-[open=true]:flex data-[open=true]:flex-col data-[open=true]:items-center"
>
{MENU_ITEMS.map((item) => (
<MenuItem item={item} key={item.name} />
))}
</div>
</header>
<div
data-open={isMainMenuOpen}
className="data-[open=false]:opacity-0 transition-opacity px-4 flex-1 mt-5"
>
<p className="text-text-light-200 font-[500] text-[14px] mb-2">
Recent
</p>
<ul className="space-y-2 pb-10">
{MOCK_CHATS.map((chat) => (
<li
key={chat.id}
className="rounded-md px-3 py-2 hover:bg-fill-100 cursor-pointer transition-colors"
>
<p className="text-sm font-medium text-text-light-900 truncate">
{chat.title}
</p>
<p className="text-xs text-text-light-400 truncate">
{chat.subtitle}
</p>
</li>
))}
</ul>
</div>
<footer className="sticky bottom-0 z-10 bg-white w-full p-4 border-t border-fill-150">
<Button
data-open={isMainMenuOpen}
className="w-8 h-8 p-2 transition-all data-[open=true]:w-full data-[open=true]:h-10 items-center justify-start data-[open=true]:py-2 data-[open=true]:px-4"
variant="secondary"
>
<Profile />
<div
data-open={isMainMenuOpen}
className="data-[open=true]:opacity-100 opacity-0 transition-opacity duration-200 font-[500] text-[14px]"
>
username
</div>
</Button>
</footer>
</div>
);
});
export { MainMenu, MenuItem, MENU_ITEMS, MainMenuContent };

View File

@@ -3,6 +3,8 @@ import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
type ButtonVariantType = VariantProps<typeof buttonVariants>;
const buttonVariants = cva(
"h-[52px] inline-flex items-center justify-center gap-2 font-medium whitespace-nowrap rounded-[8px] px-4 text-[14px] leading-[130%] tracking-[0.01em] transition-colors hover:cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:*:fill-text-secondary [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
@@ -45,4 +47,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
);
Button.displayName = "Button";
export { Button, buttonVariants };
export { Button, buttonVariants, type ButtonVariantType };

View File

@@ -16,6 +16,7 @@ import { useToast } from "@/lib/hooks/use-toast";
import Discord from "@/icons/discord.svg?react";
import Copy from "@/icons/copy.svg?react";
import { TextLink } from "./text-link";
import { LINKS } from "@/lib/constants";
const ToastProvider = ToastPrimitives.Provider;
@@ -308,10 +309,7 @@ const TOASTER_DESCRIPTION_PATTERNS = {
Error code ]
</TextLink>
<TextLink
className="flex items-center gap-1"
href="https://discord.gg/cytonic"
>
<TextLink className="flex items-center gap-1" href={LINKS.discord}>
<Discord className="text-feedback-info-900" width={15} height={12} />[
Support ]
</TextLink>

3
src/icons/books.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4781 11.1594L10.4038 1.29686C10.3769 1.16789 10.3249 1.04549 10.2506 0.936688C10.1763 0.827889 10.0813 0.734839 9.97093 0.662882C9.86059 0.590925 9.73711 0.541479 9.6076 0.517386C9.47809 0.493293 9.34509 0.495026 9.21625 0.522487L6.29062 1.15124C6.03184 1.2079 5.8059 1.36445 5.66194 1.58683C5.51798 1.80921 5.46764 2.07944 5.52187 2.33874L7.59625 12.2012C7.64248 12.4263 7.76477 12.6285 7.94255 12.774C8.12034 12.9194 8.34279 12.9992 8.5725 13C8.64351 12.9999 8.71431 12.9924 8.78375 12.9775L11.7094 12.3487C11.9685 12.2919 12.1946 12.1351 12.3386 11.9123C12.4826 11.6896 12.5327 11.4189 12.4781 11.1594ZM6.5 2.13436C6.5 2.13061 6.5 2.12874 6.5 2.12874L9.425 1.50374L9.63313 2.49561L6.70813 3.12499L6.5 2.13436ZM6.91375 4.10124L9.84 3.47311L10.0487 4.46686L7.125 5.09561L6.91375 4.10124ZM7.32875 6.07436L10.255 5.44561L11.0863 9.39811L8.16 10.0269L7.32875 6.07436ZM11.5 11.3712L8.575 11.9962L8.36687 11.0044L11.2919 10.375L11.5 11.3656C11.5 11.3694 11.5 11.3712 11.5 11.3712ZM4.5 0.999987H1.5C1.23478 0.999987 0.98043 1.10534 0.792893 1.29288C0.605357 1.48042 0.5 1.73477 0.5 1.99999V12C0.5 12.2652 0.605357 12.5196 0.792893 12.7071C0.98043 12.8946 1.23478 13 1.5 13H4.5C4.76522 13 5.01957 12.8946 5.20711 12.7071C5.39464 12.5196 5.5 12.2652 5.5 12V1.99999C5.5 1.73477 5.39464 1.48042 5.20711 1.29288C5.01957 1.10534 4.76522 0.999987 4.5 0.999987ZM1.5 1.99999H4.5V2.99999H1.5V1.99999ZM1.5 3.99999H4.5V9.99999H1.5V3.99999ZM4.5 12H1.5V11H4.5V12Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

3
src/icons/plus.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6C12 6.13261 11.9473 6.25979 11.8536 6.35355C11.7598 6.44732 11.6326 6.5 11.5 6.5H6.5V11.5C6.5 11.6326 6.44732 11.7598 6.35355 11.8536C6.25979 11.9473 6.13261 12 6 12C5.86739 12 5.74021 11.9473 5.64645 11.8536C5.55268 11.7598 5.5 11.6326 5.5 11.5V6.5H0.5C0.367392 6.5 0.240215 6.44732 0.146447 6.35355C0.0526785 6.25979 0 6.13261 0 6C0 5.86739 0.0526785 5.74021 0.146447 5.64645C0.240215 5.55268 0.367392 5.5 0.5 5.5H5.5V0.5C5.5 0.367392 5.55268 0.240215 5.64645 0.146447C5.74021 0.0526785 5.86739 0 6 0C6.13261 0 6.25979 0.0526785 6.35355 0.146447C6.44732 0.240215 6.5 0.367392 6.5 0.5V5.5H11.5C11.6326 5.5 11.7598 5.55268 11.8536 5.64645C11.9473 5.74021 12 5.86739 12 6Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 812 B

3
src/icons/profile.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.125 9.25C14.125 10.0658 13.8831 10.8634 13.4298 11.5417C12.9766 12.2201 12.3323 12.7488 11.5786 13.061C10.8248 13.3732 9.99543 13.4549 9.19526 13.2957C8.39508 13.1366 7.66008 12.7437 7.08319 12.1668C6.5063 11.5899 6.11343 10.8549 5.95427 10.0547C5.7951 9.25458 5.87679 8.42518 6.189 7.67143C6.50121 6.91769 7.02992 6.27345 7.70828 5.82019C8.38663 5.36693 9.18416 5.125 10 5.125C11.0936 5.12624 12.1421 5.56124 12.9154 6.33455C13.6888 7.10787 14.1238 8.15636 14.125 9.25ZM19.75 10C19.75 11.9284 19.1782 13.8134 18.1068 15.4168C17.0355 17.0202 15.5127 18.2699 13.7312 19.0078C11.9496 19.7458 9.98919 19.9389 8.09787 19.5627C6.20656 19.1865 4.46928 18.2579 3.10571 16.8943C1.74215 15.5307 0.813554 13.7934 0.437348 11.9021C0.061142 10.0108 0.254225 8.05042 0.992179 6.26884C1.73013 4.48726 2.97982 2.96451 4.58319 1.89317C6.18657 0.821828 8.07164 0.25 10 0.25C12.585 0.25273 15.0634 1.28084 16.8913 3.10872C18.7192 4.93661 19.7473 7.41498 19.75 10ZM18.25 10C18.2488 8.88956 18.0237 7.79077 17.5881 6.76934C17.1525 5.7479 16.5154 4.82481 15.7148 4.05525C14.9143 3.2857 13.9668 2.68549 12.929 2.29053C11.8911 1.89556 10.7843 1.71395 9.67469 1.75656C5.25907 1.92719 1.73782 5.605 1.75 10.0234C1.75424 12.0349 2.49609 13.9749 3.835 15.4759C4.38028 14.6851 5.07292 14.0068 5.875 13.4781C5.94339 13.433 6.02469 13.4114 6.10646 13.4169C6.18824 13.4223 6.26599 13.4543 6.32782 13.5081C7.34705 14.3897 8.6496 14.8749 9.99719 14.8749C11.3448 14.8749 12.6473 14.3897 13.6666 13.5081C13.7284 13.4543 13.8061 13.4223 13.8879 13.4169C13.9697 13.4114 14.051 13.433 14.1194 13.4781C14.9225 14.0065 15.6161 14.6848 16.1622 15.4759C17.5077 13.9694 18.251 12.0199 18.25 10Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

3
src/icons/sidebar.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 0.5H1.5C1.23478 0.5 0.98043 0.605357 0.792893 0.792893C0.605357 0.98043 0.5 1.23478 0.5 1.5V10.5C0.5 10.7652 0.605357 11.0196 0.792893 11.2071C0.98043 11.3946 1.23478 11.5 1.5 11.5H12.5C12.7652 11.5 13.0196 11.3946 13.2071 11.2071C13.3946 11.0196 13.5 10.7652 13.5 10.5V1.5C13.5 1.23478 13.3946 0.98043 13.2071 0.792893C13.0196 0.605357 12.7652 0.5 12.5 0.5ZM1.5 1.5H4V10.5H1.5V1.5ZM12.5 10.5H5V1.5H12.5V10.5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 551 B

View File

@@ -2,3 +2,7 @@ export const LS_TOKENS = {
isMainMenuOpen: "isMainMenuOpen",
userId: "userId",
};
export const LINKS = {
discord: "https://discord.gg/cytonic",
};

View File

@@ -2,7 +2,7 @@
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { useAccountStore } from "@/contexts/AccountContext";
import { MainMenu } from "@/components/main-menu";
import { MainMenu } from "@/components/main-menu/main-menu";
export const Route = createRootRoute({
component: RootLayout,
@@ -21,7 +21,7 @@ function RootLayout() {
</div>
</div>
<TanStackRouterDevtools />
{/* <TanStackRouterDevtools /> */}
</div>
);
}

View File

@@ -2,22 +2,27 @@ import { Chat } from "@/components/interaction-panel/chat";
import { createFileRoute, Link } from "@tanstack/react-router";
import LogoWithText from "@/icons/logo-with-text.svg?react";
import { Button } from "@/components/ui/button";
import { useAccountStore } from "@/contexts/AccountContext";
export const Route = createFileRoute("/")({
component: App,
});
function App() {
const { userId } = useAccountStore();
return (
<div className="h-full pt-5">
<header className="px-10 h-[52px] flex items-center justify-between absolute w-full">
<LogoWithText />
<Link to="/auth/mail" viewTransition={{ types: ["warp"] }}>
<Button className=" text-[18px] w-[106px] cursor-pointer">
Sign in
</Button>
</Link>
</header>
{!userId && (
<header className="px-10 h-[52px] flex items-center justify-between absolute w-full">
<LogoWithText />
<Link to="/auth/mail" viewTransition={{ types: ["warp"] }}>
<Button className=" text-[18px] w-[106px] cursor-pointer">
Sign in
</Button>
</Link>
</header>
)}
<Chat />
</div>
);

View File

@@ -115,6 +115,7 @@ function RouteComponent() {
};
const handleConfirm = (fullCode: string) => {
console.log(fullCode);
navigate({
to: "/auth/username",
viewTransition: { types: ["warp"] },