@mhome/ui 0.1.3 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhome/ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "mHome UI Component Library",
6
6
  "main": "dist/index.cjs.js",
@@ -26,6 +26,8 @@ const DatePicker = React.forwardRef(
26
26
  const inputRef = React.useRef(null);
27
27
  const yearPickerRef = React.useRef(null);
28
28
  const currentYearButtonRef = React.useRef(null);
29
+ const [viewMode, setViewMode] = React.useState("month"); // "month" or "year"
30
+ const [showYearPicker, setShowYearPicker] = React.useState(false);
29
31
 
30
32
  React.useImperativeHandle(ref, () => inputRef.current);
31
33
 
@@ -52,7 +54,7 @@ const DatePicker = React.forwardRef(
52
54
  }
53
55
  };
54
56
 
55
- const handleClickOutside = (event) => {
57
+ const handleClickOutside = React.useCallback((event) => {
56
58
  if (
57
59
  calendarRef.current &&
58
60
  !calendarRef.current.contains(event.target) &&
@@ -65,7 +67,7 @@ const DatePicker = React.forwardRef(
65
67
  onOpenChange(false);
66
68
  }
67
69
  }
68
- };
70
+ }, [onOpenChange]);
69
71
 
70
72
  React.useEffect(() => {
71
73
  if (open) {
@@ -74,12 +76,10 @@ const DatePicker = React.forwardRef(
74
76
  document.removeEventListener("mousedown", handleClickOutside);
75
77
  };
76
78
  }
77
- }, [open]);
79
+ }, [open, handleClickOutside]);
78
80
 
79
81
  const textFieldProps = slotProps?.textField || {};
80
82
  const displayValue = selectedDate ? selectedDate.format("YYYY-MM-DD") : "";
81
- const [viewMode, setViewMode] = React.useState("month"); // "month" or "year"
82
- const [showYearPicker, setShowYearPicker] = React.useState(false);
83
83
 
84
84
  // Scroll to current year when year picker opens
85
85
  React.useEffect(() => {
@@ -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
 
@@ -354,15 +363,8 @@ DialogFooter.displayName = "DialogFooter";
354
363
 
355
364
  const DialogTitle = React.forwardRef(
356
365
  ({ className, style, sx, ...props }, ref) => {
357
- const actualMode = useIsDarkMode();
358
- const backgroundType = useRecoilValue(backgroundTypeState);
359
- const bgColor = getBackground(backgroundType, actualMode === "dark");
360
- const isGradient =
361
- bgColor &&
362
- typeof bgColor === "string" &&
363
- (bgColor.startsWith("linear-gradient") ||
364
- bgColor.startsWith("radial-gradient") ||
365
- bgColor.startsWith("conic-gradient"));
366
+ // Check if DialogTitle is inside DialogHeader (DialogHeader already has padding)
367
+ const isInsideDialogHeader = React.useContext(DialogHeaderContext);
366
368
 
367
369
  // Convert sx prop to style if provided, handling MUI spacing
368
370
  const sxStyles = React.useMemo(() => {
@@ -444,26 +446,6 @@ const DialogTitle = React.forwardRef(
444
446
  return converted;
445
447
  }, [sx]);
446
448
 
447
- // Extract padding from style to allow override
448
- const {
449
- padding,
450
- paddingTop,
451
- paddingBottom,
452
- paddingLeft,
453
- paddingRight,
454
- backgroundColor,
455
- ...restStyle
456
- } = style || {};
457
-
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
460
- const hasAnyPadding =
461
- padding !== undefined ||
462
- paddingTop !== undefined ||
463
- paddingBottom !== undefined ||
464
- paddingLeft !== undefined ||
465
- paddingRight !== undefined;
466
-
467
449
  return (
468
450
  <h2
469
451
  ref={ref}
@@ -471,28 +453,14 @@ const DialogTitle = React.forwardRef(
471
453
  "rounded-t-[inherit]",
472
454
  "text-lg font-semibold leading-none tracking-tight",
473
455
  "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
456
+ "text-left",
457
+ !isInsideDialogHeader && "px-6 pt-6", // Default padding only when not inside DialogHeader
477
458
  className
478
459
  )}
479
460
  style={{
480
- // Only set background if explicitly provided, otherwise inherit from parent
481
- ...(backgroundColor !== undefined
482
- ? backgroundColor.startsWith("linear-gradient") ||
483
- backgroundColor.startsWith("radial-gradient") ||
484
- backgroundColor.startsWith("conic-gradient")
485
- ? { background: backgroundColor }
486
- : { backgroundColor }
487
- : {}),
488
- // Allow padding override via style prop
489
- padding: padding,
490
- paddingLeft: paddingLeft,
491
- paddingRight: paddingRight,
492
- paddingTop: paddingTop,
493
- paddingBottom: paddingBottom,
461
+ textAlign: "left",
494
462
  ...sxStyles,
495
- ...restStyle,
463
+ ...style,
496
464
  }}
497
465
  {...props}
498
466
  />
@@ -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) */}
@@ -70,8 +70,9 @@ const IconButton = React.forwardRef(
70
70
  }, [children, size]);
71
71
 
72
72
  // Warn if no aria-label is provided (accessibility best practice)
73
+ // Only warn in development mode
73
74
  React.useEffect(() => {
74
- if (!ariaLabel && !props["aria-labelledby"]) {
75
+ if (process.env.NODE_ENV === "development" && !ariaLabel && !props["aria-labelledby"]) {
75
76
  console.warn(
76
77
  "IconButton: Missing aria-label or aria-labelledby. Icon-only buttons should have accessible labels."
77
78
  );
@@ -24,6 +24,56 @@ const Menu = React.forwardRef(
24
24
  const positionType = disablePortal ? positionProp : "fixed";
25
25
  const menuRef = React.useRef(null);
26
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
+ };
27
77
 
28
78
  React.useImperativeHandle(ref, () => menuRef.current);
29
79
 
@@ -255,7 +305,7 @@ const Menu = React.forwardRef(
255
305
  }
256
306
  };
257
307
  }
258
- }, [open, anchorEl, anchorOrigin, positionType, PaperProps?.style?.minWidth]);
308
+ }, [open, anchorEl, anchorOrigin.vertical, anchorOrigin.horizontal, positionType, PaperProps?.style?.minWidth]);
259
309
 
260
310
  // Recalculate position after menu is rendered (to get actual dimensions)
261
311
  React.useLayoutEffect(() => {
@@ -338,7 +388,15 @@ const Menu = React.forwardRef(
338
388
  top = Math.max(padding, viewportHeight - menuHeight - padding);
339
389
  }
340
390
 
341
- setPosition({ top, left });
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
+ }
342
400
  } catch (e) {
343
401
  // Silently fail if calculation error
344
402
  }
@@ -354,7 +412,7 @@ const Menu = React.forwardRef(
354
412
  cancelAnimationFrame(rafId);
355
413
  }
356
414
  };
357
- }, [open, anchorEl, anchorOrigin, positionType]); // Re-run when menu opens or anchor changes
415
+ }, [open, anchorEl, anchorOrigin.vertical, anchorOrigin.horizontal, positionType]); // Re-run when menu opens or anchor changes
358
416
 
359
417
  // Handle backdrop click
360
418
  React.useEffect(() => {
@@ -420,9 +478,11 @@ const Menu = React.forwardRef(
420
478
  borderRadius: PaperProps?.style?.borderRadius || "8px",
421
479
  minWidth: PaperProps?.style?.minWidth || 180,
422
480
  maxWidth: "calc(100vw - 32px)",
423
- padding: "4px 0",
481
+ padding: "4px 0", // Vertical padding only, no horizontal padding so MenuItem hover extends to border
424
482
  height: "auto",
425
- overflow: "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",
426
486
  ...PaperProps?.style,
427
487
  ...props.style,
428
488
  }}
@@ -430,7 +490,7 @@ const Menu = React.forwardRef(
430
490
  Object.entries(props || {}).filter(([key]) => key !== "style")
431
491
  )}
432
492
  >
433
- {children}
493
+ {processChildren(children)}
434
494
  </div>
435
495
  );
436
496
 
@@ -450,7 +510,7 @@ Menu.displayName = "Menu";
450
510
 
451
511
  const MenuItem = React.forwardRef(
452
512
  (
453
- { className, onClick, disabled = false, sx, style, children, ...props },
513
+ { className, onClick, disabled = false, sx, style, children, _isFirst, _isLast, _menuBorderRadius, ...props },
454
514
  ref
455
515
  ) => {
456
516
  const [hovered, setHovered] = React.useState(false);
@@ -475,13 +535,13 @@ const MenuItem = React.forwardRef(
475
535
  paddingRight: `${menuItemPadding.x}px`,
476
536
  paddingTop: `${menuItemPadding.y}px`,
477
537
  paddingBottom: `${menuItemPadding.y}px`,
538
+ margin: 0, // Ensure no margin that could cause overflow
478
539
  cursor: disabled ? "not-allowed" : "pointer",
479
540
  fontSize: menuItemFontSize,
480
541
  color: disabled
481
542
  ? "#666666"
482
543
  : "#000000", // Use fixed color for testing
483
- backgroundColor:
484
- hovered && !disabled ? "hsl(var(--accent))" : "transparent",
544
+ backgroundColor: hovered && !disabled ? "hsl(var(--accent))" : "transparent",
485
545
  opacity: disabled
486
546
  ? 0.5
487
547
  : sxStyles.opacity !== undefined
@@ -493,7 +553,16 @@ const MenuItem = React.forwardRef(
493
553
  textAlign: "left",
494
554
  fontWeight: "normal",
495
555
  width: "100%",
556
+ maxWidth: "100%", // Ensure it doesn't exceed parent width
496
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,
497
566
  ...sxStyles,
498
567
  ...style,
499
568
  };
@@ -533,7 +602,11 @@ const MenuItem = React.forwardRef(
533
602
  "focus:outline-none focus:bg-accent focus:text-accent-foreground",
534
603
  className
535
604
  )}
536
- style={mergedStyle}
605
+ style={{
606
+ ...mergedStyle,
607
+ // Use pseudo-element approach: extend hover background to edges
608
+ position: "relative",
609
+ }}
537
610
  onClick={handleClick}
538
611
  onMouseDown={handleMouseDown}
539
612
  onKeyDown={handleKeyDown}
@@ -28,6 +28,8 @@ const Select = React.forwardRef(
28
28
  multiple = false,
29
29
  sx,
30
30
  style,
31
+ menuStyle, // Style for the dropdown menu (the open listbox)
32
+ menuClassName, // ClassName for the dropdown menu
31
33
  ...props
32
34
  },
33
35
  ref
@@ -154,7 +156,7 @@ const Select = React.forwardRef(
154
156
  paddingRight: endAdornment ? "8px" : "14px",
155
157
  paddingTop: "6px",
156
158
  paddingBottom: "6px",
157
- // If borderRadius is provided in props.style, use it; otherwise let Tailwind's rounded-3xl class handle it
159
+ // If borderRadius is provided in props.style, use it; otherwise let Tailwind classes handle it
158
160
  ...(style?.borderRadius !== undefined
159
161
  ? { borderRadius: style.borderRadius }
160
162
  : {}),
@@ -194,16 +196,21 @@ const Select = React.forwardRef(
194
196
  id={`${selectId}-listbox`}
195
197
  role="listbox"
196
198
  aria-label={label || "Options"}
197
- className="absolute z-50 w-full mt-1 rounded-lg shadow-lg"
199
+ className={cn(
200
+ "absolute z-50 w-full mt-1 shadow-lg",
201
+ menuClassName
202
+ )}
198
203
  style={{
199
204
  backgroundColor: "hsl(var(--card))",
200
205
  boxShadow: isDark
201
- ? "0px 4px 12px rgba(0, 0, 0, 0.4), 0px 0px 0px 1px rgba(255, 255, 255, 0.1)"
206
+ ? "0px 4px 12px rgba(0, 0, 0, 0.4)"
202
207
  : "0px 4px 12px rgba(0, 0, 0, 0.15)",
203
208
  border: isDark
204
209
  ? "1px solid rgba(255, 255, 255, 0.1)"
205
210
  : "1px solid hsl(var(--border))",
206
211
  borderRadius: "8px",
212
+ overflow: "hidden",
213
+ ...menuStyle,
207
214
  }}
208
215
  >
209
216
  {React.Children.map(children, (child) => {
@@ -218,7 +218,7 @@ const Tab = React.forwardRef(
218
218
  }
219
219
  }}
220
220
  className={cn(
221
- "cursor-pointer transition-all duration-200 text-sm border-none bg-transparent focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
221
+ "cursor-pointer transition-all duration-200 text-sm border-none bg-transparent focus:outline-none",
222
222
  defaultPaddingClass,
223
223
  "min-w-[72px] min-h-[48px] flex-1",
224
224
  selected ? "font-semibold" : "font-medium",
@@ -320,7 +320,7 @@ const Tab = React.forwardRef(
320
320
  }
321
321
  }}
322
322
  className={cn(
323
- "cursor-pointer transition-all duration-200 min-h-[32px] min-w-[100px] rounded-2xl text-sm border-none focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
323
+ "cursor-pointer transition-all duration-200 min-h-[32px] min-w-[100px] rounded-2xl text-sm border-none focus:outline-none",
324
324
  defaultPaddingClass,
325
325
  defaultMarginClass,
326
326
  selected ? "font-semibold" : "font-medium",
@@ -74,7 +74,7 @@ const TextField = React.forwardRef(
74
74
  } else {
75
75
  // outlined (default)
76
76
  return focused
77
- ? getComponentBgColor("hsl(var(--input))")
77
+ ? getComponentBgColor("hsl(var(--background))")
78
78
  : "transparent";
79
79
  }
80
80
  };
@@ -46,7 +46,7 @@ const typographyVariants = cva("", {
46
46
 
47
47
  const Typography = React.forwardRef(
48
48
  (
49
- { className, variant = "body1", component, style, color, ...props },
49
+ { className, variant = "body1", component, style, color, noWrap, gutterBottom, ...props },
50
50
  ref
51
51
  ) => {
52
52
  // Map MUI color props to shadcn colors
@@ -66,7 +66,11 @@ const Typography = React.forwardRef(
66
66
  typographyVariants({ variant, color: mappedColor }),
67
67
  className
68
68
  )}
69
- style={style}
69
+ style={{
70
+ ...(noWrap && { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }),
71
+ ...(gutterBottom && { marginBottom: "0.35em" }),
72
+ ...style,
73
+ }}
70
74
  {...props}
71
75
  />
72
76
  );