@checkstack/ui 1.3.5 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @checkstack/ui
2
2
 
3
+ ## 1.3.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 4b0934d: Refactored UserMenu to use a responsive grid layout, improved menu item alignment, and implemented a full-screen scrollable portal for mobile devices. Fixed an issue where the UserMenu would instantly close and reopen when clicking the trigger while the menu was open.
8
+
3
9
  ## 1.3.5
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "dependencies": {
@@ -21,18 +21,20 @@
21
21
  "monaco-editor": "^0.55.1",
22
22
  "react": "^18.2.0",
23
23
  "react-day-picker": "^9.13.0",
24
+ "react-dom": "^19.2.5",
24
25
  "react-markdown": "^10.1.0",
25
26
  "react-router-dom": "^6.20.0",
26
27
  "recharts": "^3.6.0",
27
28
  "tailwind-merge": "^2.2.0"
28
29
  },
29
30
  "devDependencies": {
30
- "typescript": "^5.0.0",
31
- "@types/react": "^18.2.0",
32
- "@testing-library/react": "^16.0.0",
31
+ "@checkstack/scripts": "0.1.2",
33
32
  "@checkstack/test-utils-frontend": "0.0.4",
34
33
  "@checkstack/tsconfig": "0.0.5",
35
- "@checkstack/scripts": "0.1.2"
34
+ "@testing-library/react": "^16.0.0",
35
+ "@types/react": "^18.2.0",
36
+ "@types/react-dom": "^19.2.3",
37
+ "typescript": "^5.0.0"
36
38
  },
37
39
  "scripts": {
38
40
  "typecheck": "tsc --noEmit",
@@ -1,11 +1,43 @@
1
- import React, { useRef, useEffect } from "react";
1
+ import React, { useRef, useEffect, createContext, useContext, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { X } from "lucide-react";
2
4
  import { cn } from "../utils";
3
5
  import { usePerformance } from "./PerformanceProvider";
4
6
 
7
+ export const DropdownMenuContext = createContext<{ onClose?: () => void }>({});
8
+
9
+ /**
10
+ * Custom hook to detect mobile viewport.
11
+ */
12
+ export function useIsMobile() {
13
+ const [isMobile, setIsMobile] = useState(false);
14
+
15
+ useEffect(() => {
16
+ // Initial check
17
+ const mql = globalThis.matchMedia("(max-width: 640px)");
18
+ setIsMobile(mql.matches);
19
+
20
+ // Listener for changes
21
+ const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
22
+ mql.addEventListener("change", handler);
23
+ return () => mql.removeEventListener("change", handler);
24
+ }, []);
25
+
26
+ return isMobile;
27
+ }
28
+
29
+
30
+ export const DropdownMenuRootContext = createContext<{ rootRef?: React.RefObject<HTMLDivElement> }>({});
31
+
5
32
  export const DropdownMenu: React.FC<{ children: React.ReactNode }> = ({
6
33
  children,
7
34
  }) => {
8
- return <div className="relative inline-block text-left">{children}</div>;
35
+ const rootRef = useRef<HTMLDivElement>(null);
36
+ return (
37
+ <DropdownMenuRootContext.Provider value={{ rootRef }}>
38
+ <div ref={rootRef} className="relative inline-block text-left">{children}</div>
39
+ </DropdownMenuRootContext.Provider>
40
+ );
9
41
  };
10
42
 
11
43
  export const DropdownMenuTrigger: React.FC<{
@@ -25,44 +57,83 @@ export const DropdownMenuContent: React.FC<{
25
57
  isOpen: boolean;
26
58
  onClose: () => void;
27
59
  className?: string;
28
- }> = ({ children, isOpen, onClose, className }) => {
60
+ innerClassName?: string;
61
+ }> = ({ children, isOpen, onClose, className, innerClassName }) => {
29
62
  const { isLowPower } = usePerformance();
30
63
  const contentRef = useRef<HTMLDivElement>(null);
64
+ const { rootRef } = useContext(DropdownMenuRootContext);
65
+ const isMobile = useIsMobile();
31
66
 
32
67
  useEffect(() => {
33
68
  const handleClickOutside = (event: MouseEvent) => {
34
- if (
35
- contentRef.current &&
36
- !contentRef.current.contains(event.target as Node)
37
- ) {
38
- onClose();
69
+ // Don't auto-close if clicking outside on mobile (full screen modal style)
70
+ if (isMobile) return;
71
+
72
+ const target = event.target as Node;
73
+
74
+ // Don't close if clicking inside the menu content
75
+ if (contentRef.current && contentRef.current.contains(target)) {
76
+ return;
77
+ }
78
+
79
+ // Don't close if clicking inside the wrapper (e.g. the trigger button)
80
+ // This allows the trigger's onClick to handle toggling the menu without conflict.
81
+ if (rootRef?.current && rootRef.current.contains(target)) {
82
+ return;
39
83
  }
84
+
85
+ onClose();
40
86
  };
41
87
 
42
88
  if (isOpen) {
43
89
  document.addEventListener("mousedown", handleClickOutside);
90
+ // Lock body scroll on mobile
91
+ if (isMobile) {
92
+ document.body.style.overflow = "hidden";
93
+ }
44
94
  }
45
95
  return () => {
46
96
  document.removeEventListener("mousedown", handleClickOutside);
97
+ if (isMobile) {
98
+ document.body.style.overflow = "";
99
+ }
47
100
  };
48
- }, [isOpen, onClose]);
101
+ }, [isOpen, onClose, isMobile, rootRef]);
49
102
 
50
103
  if (!isOpen) return <React.Fragment />;
51
104
 
52
- return (
105
+ const content = (
53
106
  <div
54
107
  ref={contentRef}
55
108
  className={cn(
56
- "absolute right-0 mt-2 w-56 origin-top-right rounded-md bg-popover shadow-lg ring-1 ring-border focus:outline-none z-[100]",
109
+ isMobile
110
+ ? "fixed inset-0 z-[100] bg-background w-full h-full p-4 overflow-y-auto"
111
+ : "absolute right-0 mt-2 w-56 origin-top-right rounded-md bg-popover shadow-lg ring-1 ring-border focus:outline-none z-[100] max-h-[calc(100vh-4rem)] overflow-y-auto",
57
112
  !isLowPower && "animate-in fade-in zoom-in-95 duration-100",
58
113
  className
59
114
  )}
60
115
  >
61
- <div className="py-1" role="none">
62
- {children}
63
- </div>
116
+ <DropdownMenuContext.Provider value={{ onClose }}>
117
+ <div className={cn(isMobile ? "flex flex-col pt-4 pb-12" : "py-1", innerClassName)} role="none">
118
+ {isMobile && (
119
+ <div className="flex justify-between items-center mb-6 col-span-full px-2">
120
+ <h2 className="text-xl font-bold text-foreground">Menu</h2>
121
+ <button
122
+ onClick={onClose}
123
+ className="p-2 rounded-full hover:bg-accent text-muted-foreground transition-colors"
124
+ aria-label="Close menu"
125
+ >
126
+ <X className="w-6 h-6" />
127
+ </button>
128
+ </div>
129
+ )}
130
+ {children}
131
+ </div>
132
+ </DropdownMenuContext.Provider>
64
133
  </div>
65
134
  );
135
+
136
+ return isMobile ? createPortal(content, document.body) : content;
66
137
  };
67
138
 
68
139
  export const DropdownMenuItem: React.FC<{
@@ -70,30 +141,47 @@ export const DropdownMenuItem: React.FC<{
70
141
  onClick?: () => void;
71
142
  className?: string;
72
143
  icon?: React.ReactNode;
73
- }> = ({ children, onClick, className, icon }) => {
144
+ description?: React.ReactNode;
145
+ closeOnClick?: boolean;
146
+ }> = ({ children, onClick, className, icon, description, closeOnClick = true }) => {
147
+ const { onClose } = useContext(DropdownMenuContext);
148
+
149
+ const handleClick = () => {
150
+ if (onClick) onClick();
151
+ if (closeOnClick && onClose) onClose();
152
+ };
153
+
74
154
  return (
75
155
  <button
76
- onClick={onClick}
156
+ onClick={handleClick}
77
157
  className={cn(
78
- "flex items-center w-full px-4 py-2 text-sm text-popover-foreground hover:bg-accent hover:text-accent-foreground transition-colors",
158
+ "flex flex-col items-start w-full px-4 py-2 text-sm text-popover-foreground hover:bg-accent hover:text-accent-foreground transition-colors rounded-sm overflow-hidden",
79
159
  className
80
160
  )}
81
161
  role="menuitem"
82
162
  >
83
- {icon && <span className="mr-3 text-muted-foreground">{icon}</span>}
84
- {children}
163
+ <div className="flex items-center w-full overflow-hidden">
164
+ {icon && <span className="mr-3 text-muted-foreground shrink-0">{icon}</span>}
165
+ <span className="flex-1 text-left truncate">{children}</span>
166
+ </div>
167
+ {description && (
168
+ <span className={cn("text-[10px] text-muted-foreground mt-1 leading-tight text-left truncate w-full", icon ? "pl-7" : "")}>
169
+ {description}
170
+ </span>
171
+ )}
85
172
  </button>
86
173
  );
87
174
  };
88
175
 
89
- export const DropdownMenuSeparator: React.FC = () => (
90
- <div className="my-1 h-px bg-border" />
176
+ export const DropdownMenuSeparator: React.FC<{ className?: string }> = ({ className }) => (
177
+ <div className={cn("my-1 h-px bg-border col-span-full", className)} />
91
178
  );
92
179
 
93
- export const DropdownMenuLabel: React.FC<{ children: React.ReactNode }> = ({
180
+ export const DropdownMenuLabel: React.FC<{ children: React.ReactNode; className?: string }> = ({
94
181
  children,
182
+ className
95
183
  }) => (
96
- <div className="px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
184
+ <div className={cn("px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider col-span-full", className)}>
97
185
  {children}
98
186
  </div>
99
187
  );
@@ -60,7 +60,12 @@ export const UserMenu: React.FC<UserMenuProps> = ({
60
60
  </button>
61
61
  </DropdownMenuTrigger>
62
62
 
63
- <DropdownMenuContent isOpen={isOpen} onClose={() => setIsOpen(false)}>
63
+ <DropdownMenuContent
64
+ isOpen={isOpen}
65
+ onClose={() => setIsOpen(false)}
66
+ className="w-full sm:w-[400px] md:w-[460px]"
67
+ innerClassName="grid grid-cols-1 sm:grid-cols-2 p-2 gap-2 sm:gap-1"
68
+ >
64
69
  <DropdownMenuLabel>
65
70
  <div className="flex flex-col">
66
71
  <span className="text-sm font-bold text-foreground truncate">