Skip to content
This repository was archived by the owner on Jul 20, 2025. It is now read-only.

Commit 01b0207

Browse files
feat: mobile view
1 parent 0ec961c commit 01b0207

File tree

6 files changed

+172
-106
lines changed

6 files changed

+172
-106
lines changed

src/components/Menu/Menu.stories.tsx

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { MenuItem } from "./MenuItem";
55
import { SubMenu } from "./SubMenu";
66
import { SubMenuItem } from "./SubMenuItem";
77
import { Button } from "../Button";
8+
import { Text } from "../Text";
89

910
const meta: Meta<typeof Menu> = {
1011
title: "Components/Menu",
@@ -62,9 +63,9 @@ export const Default: Story = {
6263
render: () => {
6364
const [selectedTheme, setSelectedTheme] = useState("auto");
6465
const themes = [
65-
{ id: "auto", label: "Auto", description: "Follow system preference" },
66-
{ id: "light", label: "Light", description: "Always use light theme" },
67-
{ id: "dark", label: "Dark", description: "Always use dark theme" },
66+
{ id: "auto", label: "Auto", description: undefined },
67+
{ id: "light", label: "Light", description: undefined },
68+
{ id: "dark", label: "Dark", description: undefined },
6869
];
6970
return (
7071
<div className="p-4">
@@ -75,25 +76,49 @@ export const Default: Story = {
7576
</button>
7677
}
7778
>
78-
<SubMenu name="Theme" description={selectedTheme} icon={<ThemeIcon />} suffix={<ChevronRightIcon />}>
79-
<div className="flex flex-col">
80-
{themes.map((theme) => (
81-
<SubMenuItem
82-
key={theme.id}
83-
label={theme.label}
84-
description={theme.description}
85-
selected={selectedTheme === theme.id}
86-
onClick={() => {
87-
setSelectedTheme(theme.id);
88-
console.log(`Theme selected: ${theme.id}`);
89-
}}
90-
/>
91-
))}
92-
</div>
93-
</SubMenu>
94-
<MenuItem name="Report a bug" icon={<ReportABugIcon />} suffix={<ChevronRightIcon />} />
95-
<MenuItem name="Terms of Use" suffix={<ChevronRightIcon />} />
96-
<MenuItem name="Privacy Policy" suffix={<ChevronRightIcon />} />
79+
<Text variant="body1" className="px-7 pb-6 text-accent-primary md:hidden">
80+
Settings
81+
</Text>
82+
<div className="mx-[21px] rounded-lg md:mx-0">
83+
<SubMenu
84+
name="Theme"
85+
description={selectedTheme}
86+
icon={<ThemeIcon />}
87+
suffix={<ChevronRightIcon />}
88+
className="bg-secondary-highlight md:bg-transparent"
89+
>
90+
<div className="flex flex-col">
91+
{themes.map((theme) => (
92+
<SubMenuItem
93+
key={theme.id}
94+
label={theme.label}
95+
description={theme?.description ?? ""}
96+
selected={selectedTheme === theme.id}
97+
onClick={() => {
98+
setSelectedTheme(theme.id);
99+
console.log(`Theme selected: ${theme.id}`);
100+
}}
101+
/>
102+
))}
103+
</div>
104+
</SubMenu>
105+
<MenuItem
106+
name="Report a bug"
107+
icon={<ReportABugIcon />}
108+
suffix={<ChevronRightIcon />}
109+
className="mb-4 bg-secondary-highlight md:mb-0 md:bg-transparent"
110+
/>
111+
<MenuItem
112+
name="Terms of Use"
113+
suffix={<ChevronRightIcon />}
114+
className="bg-secondary-highlight md:bg-transparent"
115+
/>
116+
<MenuItem
117+
name="Privacy Policy"
118+
suffix={<ChevronRightIcon />}
119+
className="bg-secondary-highlight md:bg-transparent"
120+
/>
121+
</div>
97122
<div className="my-4 flex justify-center">
98123
<Button className="!bg-[#D5FCE8] !text-black" variant="contained">
99124
Switch to BABY Staking

src/components/Menu/Menu.tsx

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, { useState, useRef, useEffect } from "react";
1+
import React, { useState, useRef } from "react";
22
import { twJoin } from "tailwind-merge";
33

4-
import { MobileDialog } from "../Dialog";
54
import { Popover } from "../Popover";
65
import { MenuProvider } from "./MenuContext";
6+
import { MenuDrawer } from "./MenuDrawer";
7+
import { useIsMobile } from "@/hooks/useIsMobile";
78
import { Placement } from "@popperjs/core";
89

910
interface MenuProps {
@@ -27,7 +28,7 @@ export const Menu: React.FC<MenuProps> = ({
2728
}) => {
2829
const [internalOpen, setInternalOpen] = useState(false);
2930
const triggerRef = useRef<HTMLDivElement>(null);
30-
const [isMobile, setIsMobile] = useState(false);
31+
const isMobile = useIsMobile();
3132

3233
const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
3334
const setIsOpen = (open: boolean) => {
@@ -39,31 +40,6 @@ export const Menu: React.FC<MenuProps> = ({
3940

4041
const onClose = () => setIsOpen(false);
4142

42-
// Check if mobile view - we'll need to use the same breakpoint logic as simple-staking
43-
useEffect(() => {
44-
const checkMobile = () => {
45-
setIsMobile(window.innerWidth < 768);
46-
};
47-
48-
checkMobile();
49-
window.addEventListener("resize", checkMobile);
50-
return () => window.removeEventListener("resize", checkMobile);
51-
}, []);
52-
53-
// Handle escape key
54-
useEffect(() => {
55-
const handleEscape = (e: KeyboardEvent) => {
56-
if (e.key === "Escape" && isOpen) {
57-
onClose();
58-
}
59-
};
60-
61-
if (isOpen) {
62-
document.addEventListener("keydown", handleEscape);
63-
return () => document.removeEventListener("keydown", handleEscape);
64-
}
65-
}, [isOpen]);
66-
6743
const menuContent = <div className="relative w-full overflow-hidden">{children}</div>;
6844

6945
const triggerElement = trigger || (
@@ -84,13 +60,17 @@ export const Menu: React.FC<MenuProps> = ({
8460
</div>
8561

8662
{isMobile ? (
87-
<MobileDialog
88-
open={isOpen}
63+
<MenuDrawer
64+
isOpen={isOpen}
8965
onClose={onClose}
90-
className="bg-[#FFFFFF] p-0 text-accent-primary dark:bg-[#252525]"
66+
onBackdropClick={onClose}
67+
fullHeight={true}
68+
fullWidth={true}
69+
showBackButton={true}
70+
showDivider={false}
9171
>
9272
{menuContent}
93-
</MobileDialog>
73+
</MenuDrawer>
9474
) : (
9575
<Popover
9676
anchorEl={triggerRef.current}

src/components/Menu/MenuDrawer.tsx

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import { twJoin } from "tailwind-merge";
44
interface MenuDrawerProps {
55
isOpen: boolean;
66
onClose: () => void;
7-
title: string;
7+
title?: string;
88
children: React.ReactNode;
99
className?: string;
1010
titleAlign?: "left" | "center" | "right";
1111
backIcon?: React.ReactNode;
12+
fullHeight?: boolean;
13+
fullWidth?: boolean;
14+
showBackButton?: boolean;
15+
showDivider?: boolean;
16+
onBackdropClick?: () => void;
1217
}
1318

1419
export const MenuDrawer: React.FC<MenuDrawerProps> = ({
@@ -19,57 +24,85 @@ export const MenuDrawer: React.FC<MenuDrawerProps> = ({
1924
className = "",
2025
titleAlign = "left",
2126
backIcon,
27+
fullHeight = false,
28+
fullWidth = false,
29+
showBackButton = true,
30+
showDivider = true,
31+
onBackdropClick,
2232
}) => {
2333
const titleAlignment = {
24-
left: "justify-start",
25-
center: "justify-center",
26-
right: "justify-end",
34+
left: "text-left",
35+
center: "text-center",
36+
right: "text-right",
2737
};
2838

2939
return (
30-
<div
31-
className={twJoin(
32-
"absolute inset-0 transform rounded-lg transition-transform duration-300 ease-in-out",
33-
"bg-[#FFFFFF] dark:bg-[#252525]",
34-
isOpen ? "translate-x-0" : "translate-x-full",
35-
className,
40+
<>
41+
{fullHeight && isOpen && onBackdropClick && (
42+
<div className="fixed inset-0 z-40 bg-black/50 transition-opacity duration-300" onClick={onBackdropClick} />
3643
)}
37-
>
38-
{/* Header */}
44+
3945
<div
4046
className={twJoin(
41-
"flex items-center gap-3 border-b border-[#38708533] p-4 dark:border-[#404040]",
42-
titleAlignment[titleAlign],
47+
"transform transition-transform duration-300 ease-in-out",
48+
"bg-[#FFFFFF] dark:bg-[#252525]",
49+
fullHeight
50+
? twJoin("fixed inset-y-0 right-0 z-50", fullWidth ? "w-full" : "w-full max-w-sm")
51+
: "absolute inset-0 rounded-lg",
52+
isOpen ? "translate-x-0" : "translate-x-full",
53+
className,
4354
)}
4455
>
45-
<button
46-
onClick={onClose}
47-
className="flex h-8 w-8 items-center justify-center rounded transition-colors hover:bg-accent-secondary/10"
48-
aria-label="Go back"
49-
>
50-
{backIcon || (
51-
<svg
52-
width="16"
53-
height="16"
54-
viewBox="0 0 16 16"
55-
fill="none"
56-
xmlns="http://www.w3.org/2000/svg"
57-
className="text-accent-primary"
58-
>
59-
<path
60-
d="M10 12L6 8L10 4"
61-
stroke="currentColor"
62-
strokeWidth="2"
63-
strokeLinecap="round"
64-
strokeLinejoin="round"
65-
/>
66-
</svg>
67-
)}
68-
</button>
69-
<h3 className="text-sm font-medium text-accent-primary">{title}</h3>
70-
</div>
56+
{/* Header */}
57+
{(showBackButton || title) && (
58+
<div
59+
className={twJoin(
60+
"flex items-center p-4",
61+
showDivider && "border-b border-[#38708533] dark:border-[#404040]",
62+
)}
63+
>
64+
{showBackButton && (
65+
<button
66+
onClick={onClose}
67+
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded transition-colors hover:bg-accent-secondary/10"
68+
aria-label="Go back"
69+
>
70+
{backIcon || (
71+
<svg
72+
width="16"
73+
height="16"
74+
viewBox="0 0 16 16"
75+
fill="none"
76+
xmlns="http://www.w3.org/2000/svg"
77+
className="text-accent-primary"
78+
>
79+
<path
80+
d="M10 12L6 8L10 4"
81+
stroke="currentColor"
82+
strokeWidth="2"
83+
strokeLinecap="round"
84+
strokeLinejoin="round"
85+
/>
86+
</svg>
87+
)}
88+
</button>
89+
)}
90+
{title && (
91+
<h3
92+
className={twJoin(
93+
"flex-1 text-sm font-medium text-accent-primary",
94+
showBackButton ? "ml-3" : "",
95+
titleAlignment[titleAlign],
96+
)}
97+
>
98+
{title}
99+
</h3>
100+
)}
101+
</div>
102+
)}
71103

72-
<div className="flex-1 overflow-y-auto">{children}</div>
73-
</div>
104+
<div className="flex-1 overflow-y-auto">{children}</div>
105+
</div>
106+
</>
74107
);
75108
};

src/components/Menu/MenuItem.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from "react";
22
import { twJoin } from "tailwind-merge";
33

4-
import { Text } from "../Text";
54
import { useMenuContext } from "./MenuContext";
65

76
interface MenuItemProps {
@@ -83,14 +82,8 @@ export const MenuItem: React.FC<MenuItemProps> = ({
8382
<div className="flex items-center gap-3">
8483
{icon && <div className="flex-shrink-0">{icon}</div>}
8584
<div className="flex flex-col">
86-
<Text variant="body1" className="text-sm font-medium text-accent-primary">
87-
{name}
88-
</Text>
89-
{description && (
90-
<Text variant="body2" className="text-xs text-accent-secondary">
91-
{description}
92-
</Text>
93-
)}
85+
<div className="text-sm font-medium text-accent-primary">{name}</div>
86+
{description && <div className="text-xs text-accent-secondary">{description}</div>}
9487
</div>
9588
</div>
9689
{suffix}

src/components/Menu/SubMenu.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState } from "react";
22
import { twJoin } from "tailwind-merge";
33
import { MenuDrawer } from "./MenuDrawer";
4+
import { useMenuContext } from "./MenuContext";
45

56
interface SubMenuProps {
67
/** Nested submenu content */
@@ -35,6 +36,7 @@ export const SubMenu: React.FC<SubMenuProps> = ({
3536
titleAlign = "left",
3637
}) => {
3738
const [isOpen, setIsOpen] = useState(false);
39+
const menuContext = useMenuContext();
3840

3941
const defaultBackIcon = (
4042
<svg
@@ -92,7 +94,10 @@ export const SubMenu: React.FC<SubMenuProps> = ({
9294
title={name || ""}
9395
titleAlign={titleAlign}
9496
backIcon={backIcon || defaultBackIcon}
95-
className={twJoin("z-50", contentClassName)}
97+
fullHeight={menuContext?.isMobile}
98+
fullWidth={menuContext?.isMobile}
99+
onBackdropClick={menuContext?.isMobile ? undefined : () => setIsOpen(false)}
100+
className={twJoin(menuContext?.isMobile ? "z-60" : "z-50", contentClassName)}
96101
>
97102
{children}
98103
</MenuDrawer>

src/hooks/useIsMobile.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useState, useEffect } from "react";
2+
3+
/**
4+
* Hook to detect if the viewport is in mobile size
5+
* @param breakpoint - The breakpoint in pixels (default: 768)
6+
* @returns boolean indicating if viewport is mobile size
7+
*/
8+
export function useIsMobile(breakpoint: number = 768): boolean {
9+
const [isMobile, setIsMobile] = useState(() => {
10+
// Initial state based on current window width
11+
if (typeof window !== "undefined") {
12+
return window.innerWidth < breakpoint;
13+
}
14+
return false;
15+
});
16+
17+
useEffect(() => {
18+
const checkMobile = () => {
19+
setIsMobile(window.innerWidth < breakpoint);
20+
};
21+
22+
checkMobile();
23+
24+
window.addEventListener("resize", checkMobile);
25+
26+
return () => window.removeEventListener("resize", checkMobile);
27+
}, [breakpoint]);
28+
29+
return isMobile;
30+
}

0 commit comments

Comments
 (0)