@mhome/ui 0.1.2 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhome/ui",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "mHome UI Component Library",
6
6
  "main": "dist/index.cjs.js",
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { cn, spacingToPx, useIsDarkMode } from "../lib/utils";
4
4
  import { getBackground } from "../common/adaptive-theme-provider";
5
- import { useRecoilValue, backgroundTypeState } from "../global-state";
5
+ import { backgroundTypeState, useRecoilValue } from "../global-state";
6
6
 
7
7
  const Dialog = ({
8
8
  open,
@@ -330,14 +330,23 @@ const DialogContent = React.forwardRef(
330
330
  );
331
331
  DialogContent.displayName = "DialogContent";
332
332
 
333
+ // Context to track if DialogTitle is inside DialogHeader
334
+ const DialogHeaderContext = React.createContext(false);
335
+
333
336
  const DialogHeader = ({ className, ...props }) => (
334
- <div
335
- className={cn(
336
- "flex flex-col space-y-1.5 text-center sm:text-left px-6 pt-6",
337
- className
338
- )}
339
- {...props}
340
- />
337
+ <DialogHeaderContext.Provider value={true}>
338
+ <div
339
+ className={cn(
340
+ "flex flex-col space-y-1.5 px-6 pt-6",
341
+ className
342
+ )}
343
+ style={{
344
+ textAlign: "left", // Ensure left alignment for all children
345
+ ...props.style,
346
+ }}
347
+ {...props}
348
+ />
349
+ </DialogHeaderContext.Provider>
341
350
  );
342
351
  DialogHeader.displayName = "DialogHeader";
343
352
 
@@ -364,6 +373,9 @@ const DialogTitle = React.forwardRef(
364
373
  bgColor.startsWith("radial-gradient") ||
365
374
  bgColor.startsWith("conic-gradient"));
366
375
 
376
+ // Check if DialogTitle is inside DialogHeader
377
+ const isInsideDialogHeader = React.useContext(DialogHeaderContext);
378
+
367
379
  // Convert sx prop to style if provided, handling MUI spacing
368
380
  const sxStyles = React.useMemo(() => {
369
381
  if (!sx) return {};
@@ -455,14 +467,30 @@ const DialogTitle = React.forwardRef(
455
467
  ...restStyle
456
468
  } = style || {};
457
469
 
458
- // Check if padding is explicitly set (if padding is set, individual paddings should be undefined)
459
- // If no padding is set at all, use defaults
470
+ // Check if padding is explicitly set in style, sxStyles, or className
471
+ // Check for padding-related Tailwind classes in className
472
+ const hasPaddingInClassName = className && (
473
+ /\b(p|px|py|pt|pb|pl|pr)-\d+/.test(className) ||
474
+ /\bpadding/.test(className)
475
+ );
476
+
460
477
  const hasAnyPadding =
461
478
  padding !== undefined ||
462
479
  paddingTop !== undefined ||
463
480
  paddingBottom !== undefined ||
464
481
  paddingLeft !== undefined ||
465
- paddingRight !== undefined;
482
+ paddingRight !== undefined ||
483
+ sxStyles.padding !== undefined ||
484
+ sxStyles.paddingTop !== undefined ||
485
+ sxStyles.paddingBottom !== undefined ||
486
+ sxStyles.paddingLeft !== undefined ||
487
+ sxStyles.paddingRight !== undefined ||
488
+ hasPaddingInClassName;
489
+
490
+ // DialogTitle should have default padding when used standalone (not inside DialogHeader)
491
+ // When inside DialogHeader, DialogHeader provides px-6 pt-6, so DialogTitle doesn't need padding
492
+ // When used standalone, DialogTitle should have px-6 pt-6 to match DialogHeader's padding
493
+ const defaultPaddingClass = !isInsideDialogHeader && !hasAnyPadding ? "px-6 pt-6" : "";
466
494
 
467
495
  return (
468
496
  <h2
@@ -471,9 +499,8 @@ const DialogTitle = React.forwardRef(
471
499
  "rounded-t-[inherit]",
472
500
  "text-lg font-semibold leading-none tracking-tight",
473
501
  "text-foreground",
474
- // DialogTitle is typically used inside DialogHeader which has padding
475
- // Only add padding if explicitly needed (when used standalone)
476
- // For normal use, padding is handled by DialogHeader
502
+ "text-left", // Always left align
503
+ defaultPaddingClass,
477
504
  className
478
505
  )}
479
506
  style={{
@@ -485,12 +512,13 @@ const DialogTitle = React.forwardRef(
485
512
  ? { background: backgroundColor }
486
513
  : { backgroundColor }
487
514
  : {}),
488
- // Allow padding override via style prop
515
+ // Allow padding override via style prop (for standalone use)
489
516
  padding: padding,
490
517
  paddingLeft: paddingLeft,
491
518
  paddingRight: paddingRight,
492
519
  paddingTop: paddingTop,
493
520
  paddingBottom: paddingBottom,
521
+ textAlign: "left", // Ensure left alignment
494
522
  ...sxStyles,
495
523
  ...restStyle,
496
524
  }}
@@ -17,6 +17,7 @@ const Drawer = React.forwardRef(
17
17
  variant = "temporary",
18
18
  ModalProps,
19
19
  SlideProps,
20
+ BackdropProps,
20
21
  ...props
21
22
  },
22
23
  ref
@@ -173,9 +174,13 @@ const Drawer = React.forwardRef(
173
174
  opacity: open ? 1 : 0,
174
175
  transition: `opacity ${transitionDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`,
175
176
  pointerEvents: open ? "auto" : "none",
177
+ ...BackdropProps?.style,
176
178
  }}
177
179
  onClick={handleBackdropClick}
178
180
  aria-hidden="true"
181
+ {...Object.fromEntries(
182
+ Object.entries(BackdropProps || {}).filter(([key]) => key !== "style")
183
+ )}
179
184
  />
180
185
  )}
181
186
 
@@ -200,7 +205,7 @@ const Drawer = React.forwardRef(
200
205
  }}
201
206
  role="presentation"
202
207
  {...Object.fromEntries(
203
- Object.entries(props).filter(([key]) => key !== "style")
208
+ Object.entries(props).filter(([key]) => key !== "style" && key !== "BackdropProps")
204
209
  )}
205
210
  >
206
211
  {/* Paper (drawer content) */}
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { createPortal } from "react-dom";
2
3
  import { cn } from "../lib/utils";
3
4
 
4
5
  const Menu = React.forwardRef(
@@ -11,13 +12,68 @@ const Menu = React.forwardRef(
11
12
  PaperProps,
12
13
  anchorOrigin = { vertical: "bottom", horizontal: "left" },
13
14
  transformOrigin = { vertical: "top", horizontal: "left" },
15
+ position: positionProp = "fixed",
16
+ disablePortal = false,
17
+ border = false,
14
18
  children,
15
19
  ...props
16
20
  },
17
21
  ref
18
22
  ) => {
23
+ // If using Portal, position must be fixed to work correctly
24
+ const positionType = disablePortal ? positionProp : "fixed";
19
25
  const menuRef = React.useRef(null);
20
26
  const [position, setPosition] = React.useState({ top: 0, left: 0 });
27
+ const previousPositionRef = React.useRef({ top: 0, left: 0 });
28
+
29
+ // Extract borderRadius to a stable value to prevent infinite loops
30
+ // Use a ref to track the last borderRadius value to avoid dependency issues
31
+ // MUST be called before any early returns (React Hooks rule)
32
+ const borderRadiusRef = React.useRef(PaperProps?.style?.borderRadius || "8px");
33
+ if (PaperProps?.style?.borderRadius !== undefined) {
34
+ borderRadiusRef.current = PaperProps.style.borderRadius;
35
+ }
36
+
37
+ // Process children to add borderRadius info to MenuItems
38
+ // We'll do this in the render phase instead of useMemo to avoid issues
39
+ const processChildren = (childrenToProcess) => {
40
+ if (!childrenToProcess) return childrenToProcess;
41
+
42
+ const borderRadius = borderRadiusRef.current;
43
+
44
+ // Get all MenuItem children first
45
+ const childrenArray = React.Children.toArray(childrenToProcess).filter(
46
+ (c) => {
47
+ if (!React.isValidElement(c)) return false;
48
+ const type = c.type;
49
+ return type?.displayName === "MenuItem" ||
50
+ (typeof type === 'function' && type.name === 'MenuItem');
51
+ }
52
+ );
53
+
54
+ return React.Children.map(childrenToProcess, (child) => {
55
+ if (!React.isValidElement(child)) return child;
56
+
57
+ // Check if child is a MenuItem
58
+ const type = child.type;
59
+ const isMenuItem = type?.displayName === "MenuItem" ||
60
+ (typeof type === 'function' && type.name === 'MenuItem');
61
+
62
+ if (!isMenuItem) return child;
63
+
64
+ const childIndex = childrenArray.findIndex((c) => c === child);
65
+ const isFirst = childIndex === 0;
66
+ const isLast = childIndex === childrenArray.length - 1;
67
+
68
+ // Clone child and pass borderRadius info
69
+ return React.cloneElement(child, {
70
+ ...child.props,
71
+ _isFirst: isFirst,
72
+ _isLast: isLast,
73
+ _menuBorderRadius: borderRadius,
74
+ });
75
+ });
76
+ };
21
77
 
22
78
  React.useImperativeHandle(ref, () => menuRef.current);
23
79
 
@@ -54,50 +110,126 @@ const Menu = React.forwardRef(
54
110
  }
55
111
  return;
56
112
  }
57
-
58
- // Find the nearest positioned ancestor (position: relative, absolute, or fixed)
59
- let container = anchorEl.parentElement;
60
- while (container && container !== document.body && container.isConnected) {
61
- try {
62
- const style = window.getComputedStyle(container);
63
- if (style.position === 'relative' || style.position === 'absolute' || style.position === 'fixed') {
113
+
114
+ if (positionType === "fixed") {
115
+ // Fixed positioning: use viewport coordinates
116
+ const viewportWidth = window.innerWidth;
117
+ const viewportHeight = window.innerHeight;
118
+
119
+ // Get menu dimensions (estimate if not yet rendered)
120
+ const menuWidth = menuRef.current?.offsetWidth || PaperProps?.style?.minWidth || 180;
121
+ const menuHeight = menuRef.current?.offsetHeight || 100;
122
+
123
+ // Calculate initial position based on anchorOrigin
124
+ let top = 0;
125
+ let left = 0;
126
+ const gap = 8;
127
+
128
+ // Vertical positioning
129
+ if (anchorOrigin.vertical === 'bottom') {
130
+ top = anchorRect.bottom + gap;
131
+ } else if (anchorOrigin.vertical === 'top') {
132
+ top = anchorRect.top - menuHeight - gap;
133
+ } else {
134
+ // center
135
+ top = anchorRect.top + (anchorRect.height - menuHeight) / 2;
136
+ }
137
+
138
+ // Horizontal positioning
139
+ if (anchorOrigin.horizontal === 'left') {
140
+ left = anchorRect.left;
141
+ } else if (anchorOrigin.horizontal === 'right') {
142
+ left = anchorRect.right - menuWidth;
143
+ } else {
144
+ // center
145
+ left = anchorRect.left + (anchorRect.width - menuWidth) / 2;
146
+ }
147
+
148
+ // Boundary detection and adjustment
149
+ const padding = 16;
150
+
151
+ // Adjust horizontal position if menu would overflow
152
+ if (left + menuWidth > viewportWidth - padding) {
153
+ left = viewportWidth - menuWidth - padding;
154
+ }
155
+ if (left < padding) {
156
+ left = padding;
157
+ }
158
+
159
+ // Adjust vertical position if menu would overflow
160
+ if (top + menuHeight > viewportHeight - padding) {
161
+ if (anchorOrigin.vertical === 'bottom') {
162
+ // Try to show above anchor
163
+ top = anchorRect.top - menuHeight - gap;
164
+ }
165
+ // Clamp to viewport
166
+ if (top + menuHeight > viewportHeight - padding) {
167
+ top = viewportHeight - menuHeight - padding;
168
+ }
169
+ }
170
+ if (top < padding) {
171
+ top = padding;
172
+ }
173
+
174
+ if (isMounted) {
175
+ setPosition({ top, left });
176
+ }
177
+ } else {
178
+ // Absolute positioning: use relative coordinates
179
+ // Find the nearest positioned ancestor
180
+ let container = anchorEl.parentElement;
181
+ while (container && container !== document.body && container.isConnected) {
182
+ try {
183
+ const style = window.getComputedStyle(container);
184
+ if (style.position === 'relative' || style.position === 'absolute' || style.position === 'fixed') {
185
+ break;
186
+ }
187
+ container = container.parentElement;
188
+ } catch (e) {
64
189
  break;
65
190
  }
66
- container = container.parentElement;
67
- } catch (e) {
68
- // If getComputedStyle fails, break the loop
69
- break;
70
191
  }
71
- }
72
-
73
- let containerRect = { top: 0, left: 0 };
74
- if (container && container !== document.body && container.isConnected && container.getBoundingClientRect) {
75
- try {
76
- containerRect = container.getBoundingClientRect();
77
- // Validate containerRect
78
- if (!containerRect || isNaN(containerRect.top) || isNaN(containerRect.left)) {
192
+
193
+ let containerRect = { top: 0, left: 0 };
194
+ if (container && container !== document.body && container.isConnected && container.getBoundingClientRect) {
195
+ try {
196
+ containerRect = container.getBoundingClientRect();
197
+ if (!containerRect || isNaN(containerRect.top) || isNaN(containerRect.left)) {
198
+ containerRect = { top: 0, left: 0 };
199
+ }
200
+ } catch (e) {
79
201
  containerRect = { top: 0, left: 0 };
80
202
  }
81
- } catch (e) {
82
- containerRect = { top: 0, left: 0 };
83
203
  }
84
- }
85
-
86
- // Calculate relative position - menu appears below the button
87
- const relativeTop = anchorRect.bottom - containerRect.top + 8;
88
- const relativeLeft = anchorRect.left - containerRect.left;
89
-
90
- // Validate calculated values
91
- if (isNaN(relativeTop) || isNaN(relativeLeft)) {
204
+
205
+ // Calculate relative position
206
+ const gap = 8;
207
+ let relativeTop = 0;
208
+ let relativeLeft = 0;
209
+
210
+ if (anchorOrigin.vertical === 'bottom') {
211
+ relativeTop = anchorRect.bottom - containerRect.top + gap;
212
+ } else if (anchorOrigin.vertical === 'top') {
213
+ const menuHeight = menuRef.current?.offsetHeight || 100;
214
+ relativeTop = anchorRect.top - containerRect.top - menuHeight - gap;
215
+ } else {
216
+ const menuHeight = menuRef.current?.offsetHeight || 100;
217
+ relativeTop = anchorRect.top - containerRect.top + (anchorRect.height - menuHeight) / 2;
218
+ }
219
+
220
+ if (anchorOrigin.horizontal === 'left') {
221
+ relativeLeft = anchorRect.left - containerRect.left;
222
+ } else if (anchorOrigin.horizontal === 'right') {
223
+ const menuWidth = menuRef.current?.offsetWidth || PaperProps?.style?.minWidth || 180;
224
+ relativeLeft = anchorRect.right - containerRect.left - menuWidth;
225
+ } else {
226
+ const menuWidth = menuRef.current?.offsetWidth || PaperProps?.style?.minWidth || 180;
227
+ relativeLeft = anchorRect.left - containerRect.left + (anchorRect.width - menuWidth) / 2;
228
+ }
229
+
92
230
  if (isMounted) {
93
- setPosition({ top: 50, left: 0 });
231
+ setPosition({ top: relativeTop, left: relativeLeft });
94
232
  }
95
- return;
96
- }
97
-
98
- // Only update state if component is still mounted
99
- if (isMounted) {
100
- setPosition({ top: relativeTop, left: relativeLeft });
101
233
  }
102
234
  } catch (e) {
103
235
  // Fallback position - only update if mounted
@@ -107,13 +239,180 @@ const Menu = React.forwardRef(
107
239
  }
108
240
  });
109
241
 
242
+ // Update position on scroll and resize (only for fixed positioning)
243
+ if (positionType === "fixed") {
244
+ const handleUpdate = () => {
245
+ rafId = requestAnimationFrame(() => {
246
+ if (isMounted) {
247
+ // Re-run position calculation
248
+ const anchorRect = anchorEl?.getBoundingClientRect();
249
+ if (anchorRect) {
250
+ const viewportWidth = window.innerWidth;
251
+ const viewportHeight = window.innerHeight;
252
+ const menuWidth = menuRef.current?.offsetWidth || PaperProps?.style?.minWidth || 180;
253
+ const menuHeight = menuRef.current?.offsetHeight || 100;
254
+ const gap = 8;
255
+ const padding = 16;
256
+
257
+ let top = anchorOrigin.vertical === 'bottom'
258
+ ? anchorRect.bottom + gap
259
+ : anchorOrigin.vertical === 'top'
260
+ ? anchorRect.top - menuHeight - gap
261
+ : anchorRect.top + (anchorRect.height - menuHeight) / 2;
262
+
263
+ let left = anchorOrigin.horizontal === 'left'
264
+ ? anchorRect.left
265
+ : anchorOrigin.horizontal === 'right'
266
+ ? anchorRect.right - menuWidth
267
+ : anchorRect.left + (anchorRect.width - menuWidth) / 2;
268
+
269
+ if (left + menuWidth > viewportWidth - padding) {
270
+ left = viewportWidth - menuWidth - padding;
271
+ }
272
+ if (left < padding) left = padding;
273
+ if (top + menuHeight > viewportHeight - padding) {
274
+ if (anchorOrigin.vertical === 'bottom') {
275
+ top = anchorRect.top - menuHeight - gap;
276
+ }
277
+ if (top + menuHeight > viewportHeight - padding) {
278
+ top = viewportHeight - menuHeight - padding;
279
+ }
280
+ }
281
+ if (top < padding) top = padding;
282
+
283
+ setPosition({ top, left });
284
+ }
285
+ }
286
+ });
287
+ };
288
+
289
+ window.addEventListener('scroll', handleUpdate, true);
290
+ window.addEventListener('resize', handleUpdate);
291
+
292
+ return () => {
293
+ isMounted = false;
294
+ if (rafId !== null) {
295
+ cancelAnimationFrame(rafId);
296
+ }
297
+ window.removeEventListener('scroll', handleUpdate, true);
298
+ window.removeEventListener('resize', handleUpdate);
299
+ };
300
+ } else {
301
+ return () => {
302
+ isMounted = false;
303
+ if (rafId !== null) {
304
+ cancelAnimationFrame(rafId);
305
+ }
306
+ };
307
+ }
308
+ }, [open, anchorEl, anchorOrigin.vertical, anchorOrigin.horizontal, positionType, PaperProps?.style?.minWidth]);
309
+
310
+ // Recalculate position after menu is rendered (to get actual dimensions)
311
+ React.useLayoutEffect(() => {
312
+ if (!open || !anchorEl || !menuRef.current || positionType !== "fixed") return;
313
+
314
+ const updatePositionWithRealSize = () => {
315
+ try {
316
+ const anchorRect = anchorEl.getBoundingClientRect();
317
+ if (!anchorRect) return;
318
+
319
+ const viewportWidth = window.innerWidth;
320
+ const viewportHeight = window.innerHeight;
321
+
322
+ // Get actual menu dimensions
323
+ const menuWidth = menuRef.current.offsetWidth;
324
+ const menuHeight = menuRef.current.offsetHeight;
325
+
326
+ if (!menuWidth || !menuHeight) return; // Menu not fully rendered yet
327
+
328
+ const gap = 8;
329
+ const padding = 16;
330
+
331
+ // Calculate initial position based on anchorOrigin
332
+ let top = 0;
333
+ let left = 0;
334
+
335
+ // Vertical positioning
336
+ if (anchorOrigin.vertical === 'bottom') {
337
+ top = anchorRect.bottom + gap;
338
+ } else if (anchorOrigin.vertical === 'top') {
339
+ top = anchorRect.top - menuHeight - gap;
340
+ } else {
341
+ top = anchorRect.top + (anchorRect.height - menuHeight) / 2;
342
+ }
343
+
344
+ // Horizontal positioning
345
+ if (anchorOrigin.horizontal === 'left') {
346
+ left = anchorRect.left;
347
+ } else if (anchorOrigin.horizontal === 'right') {
348
+ left = anchorRect.right - menuWidth;
349
+ } else {
350
+ left = anchorRect.left + (anchorRect.width - menuWidth) / 2;
351
+ }
352
+
353
+ // Strict boundary detection - ensure menu is fully within viewport
354
+ // Adjust horizontal position
355
+ if (left + menuWidth > viewportWidth - padding) {
356
+ // Menu would overflow on the right, align to right edge
357
+ left = viewportWidth - menuWidth - padding;
358
+ }
359
+ if (left < padding) {
360
+ // Menu would overflow on the left
361
+ left = padding;
362
+ }
363
+
364
+ // Adjust vertical position
365
+ if (top + menuHeight > viewportHeight - padding) {
366
+ // Menu would overflow on the bottom
367
+ if (anchorOrigin.vertical === 'bottom') {
368
+ // Try to show above anchor
369
+ top = anchorRect.top - menuHeight - gap;
370
+ }
371
+ // If still overflowing, clamp to viewport
372
+ if (top + menuHeight > viewportHeight - padding) {
373
+ top = viewportHeight - menuHeight - padding;
374
+ }
375
+ }
376
+ if (top < padding) {
377
+ // Menu would overflow on the top
378
+ top = padding;
379
+ }
380
+
381
+ // Ensure menu doesn't exceed viewport in any direction
382
+ if (left < 0) left = padding;
383
+ if (top < 0) top = padding;
384
+ if (left + menuWidth > viewportWidth) {
385
+ left = Math.max(padding, viewportWidth - menuWidth - padding);
386
+ }
387
+ if (top + menuHeight > viewportHeight) {
388
+ top = Math.max(padding, viewportHeight - menuHeight - padding);
389
+ }
390
+
391
+ // Only update position if it actually changed to prevent infinite loops
392
+ const newPosition = { top, left };
393
+ if (
394
+ Math.abs(previousPositionRef.current.top - newPosition.top) > 0.5 ||
395
+ Math.abs(previousPositionRef.current.left - newPosition.left) > 0.5
396
+ ) {
397
+ previousPositionRef.current = newPosition;
398
+ setPosition(newPosition);
399
+ }
400
+ } catch (e) {
401
+ // Silently fail if calculation error
402
+ }
403
+ };
404
+
405
+ // Use requestAnimationFrame to ensure DOM is updated
406
+ const rafId = requestAnimationFrame(() => {
407
+ updatePositionWithRealSize();
408
+ });
409
+
110
410
  return () => {
111
- isMounted = false;
112
- if (rafId !== null) {
411
+ if (rafId) {
113
412
  cancelAnimationFrame(rafId);
114
413
  }
115
414
  };
116
- }, [open, anchorEl]);
415
+ }, [open, anchorEl, anchorOrigin.vertical, anchorOrigin.horizontal, positionType]); // Re-run when menu opens or anchor changes
117
416
 
118
417
  // Handle backdrop click
119
418
  React.useEffect(() => {
@@ -154,12 +453,12 @@ const Menu = React.forwardRef(
154
453
  const isDark = document.documentElement.classList.contains('dark') ||
155
454
  window.matchMedia('(prefers-color-scheme: dark)').matches;
156
455
 
157
- return (
456
+ const menuElement = (
158
457
  <div
159
458
  ref={menuRef}
160
459
  className={cn("rounded-lg shadow-lg", className)}
161
460
  style={{
162
- position: "absolute",
461
+ position: positionType,
163
462
  top: `${position.top}px`,
164
463
  left: `${position.left}px`,
165
464
  zIndex: 1300,
@@ -171,15 +470,19 @@ const Menu = React.forwardRef(
171
470
  boxShadow: isDark
172
471
  ? "0px 4px 12px rgba(0, 0, 0, 0.4), 0px 0px 0px 1px rgba(255, 255, 255, 0.1)"
173
472
  : "0px 4px 12px rgba(0, 0, 0, 0.15)",
174
- border: isDark
175
- ? "1px solid rgba(255, 255, 255, 0.1)"
176
- : "1px solid hsl(var(--border))",
473
+ ...(border || PaperProps?.style?.border ? {
474
+ border: PaperProps?.style?.border || (isDark
475
+ ? "1px solid rgba(255, 255, 255, 0.1)"
476
+ : "1px solid hsl(var(--border))")
477
+ } : {}),
177
478
  borderRadius: PaperProps?.style?.borderRadius || "8px",
178
479
  minWidth: PaperProps?.style?.minWidth || 180,
179
480
  maxWidth: "calc(100vw - 32px)",
180
- padding: "4px 0",
481
+ padding: "4px 0", // Vertical padding only, no horizontal padding so MenuItem hover extends to border
181
482
  height: "auto",
182
- overflow: "visible", // Ensure content is visible
483
+ overflow: "hidden", // Ensure children don't overflow rounded corners, but allow MenuItem hover to extend to edges
484
+ // Ensure MenuItem can extend to edges
485
+ boxSizing: "border-box",
183
486
  ...PaperProps?.style,
184
487
  ...props.style,
185
488
  }}
@@ -187,9 +490,19 @@ const Menu = React.forwardRef(
187
490
  Object.entries(props || {}).filter(([key]) => key !== "style")
188
491
  )}
189
492
  >
190
- {children}
493
+ {processChildren(children)}
191
494
  </div>
192
495
  );
496
+
497
+ // Use Portal to render menu at body level to avoid positioning constraints
498
+ // Only use Portal if not disabled
499
+ if (disablePortal) {
500
+ return menuElement;
501
+ }
502
+
503
+ return typeof document !== "undefined"
504
+ ? createPortal(menuElement, document.body)
505
+ : null;
193
506
  }
194
507
  );
195
508
 
@@ -197,7 +510,7 @@ Menu.displayName = "Menu";
197
510
 
198
511
  const MenuItem = React.forwardRef(
199
512
  (
200
- { className, onClick, disabled = false, sx, style, children, ...props },
513
+ { className, onClick, disabled = false, sx, style, children, _isFirst, _isLast, _menuBorderRadius, ...props },
201
514
  ref
202
515
  ) => {
203
516
  const [hovered, setHovered] = React.useState(false);
@@ -222,13 +535,13 @@ const MenuItem = React.forwardRef(
222
535
  paddingRight: `${menuItemPadding.x}px`,
223
536
  paddingTop: `${menuItemPadding.y}px`,
224
537
  paddingBottom: `${menuItemPadding.y}px`,
538
+ margin: 0, // Ensure no margin that could cause overflow
225
539
  cursor: disabled ? "not-allowed" : "pointer",
226
540
  fontSize: menuItemFontSize,
227
541
  color: disabled
228
542
  ? "#666666"
229
543
  : "#000000", // Use fixed color for testing
230
- backgroundColor:
231
- hovered && !disabled ? "hsl(var(--accent))" : "transparent",
544
+ backgroundColor: hovered && !disabled ? "hsl(var(--accent))" : "transparent",
232
545
  opacity: disabled
233
546
  ? 0.5
234
547
  : sxStyles.opacity !== undefined
@@ -240,7 +553,16 @@ const MenuItem = React.forwardRef(
240
553
  textAlign: "left",
241
554
  fontWeight: "normal",
242
555
  width: "100%",
556
+ maxWidth: "100%", // Ensure it doesn't exceed parent width
243
557
  boxSizing: "border-box",
558
+ position: "relative",
559
+ // Ensure MenuItem doesn't overflow Menu boundaries
560
+ overflow: "hidden",
561
+ // Apply border radius to match Menu's borderRadius
562
+ borderTopLeftRadius: _isFirst ? _menuBorderRadius : 0,
563
+ borderTopRightRadius: _isFirst ? _menuBorderRadius : 0,
564
+ borderBottomLeftRadius: _isLast ? _menuBorderRadius : 0,
565
+ borderBottomRightRadius: _isLast ? _menuBorderRadius : 0,
244
566
  ...sxStyles,
245
567
  ...style,
246
568
  };
@@ -280,7 +602,11 @@ const MenuItem = React.forwardRef(
280
602
  "focus:outline-none focus:bg-accent focus:text-accent-foreground",
281
603
  className
282
604
  )}
283
- style={mergedStyle}
605
+ style={{
606
+ ...mergedStyle,
607
+ // Use pseudo-element approach: extend hover background to edges
608
+ position: "relative",
609
+ }}
284
610
  onClick={handleClick}
285
611
  onMouseDown={handleMouseDown}
286
612
  onKeyDown={handleKeyDown}