@mhome/ui 0.1.0

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.
Files changed (53) hide show
  1. package/README.md +188 -0
  2. package/dist/index.cjs.js +9 -0
  3. package/dist/index.cjs.js.map +1 -0
  4. package/dist/index.css +2 -0
  5. package/dist/index.esm.js +9 -0
  6. package/dist/index.esm.js.map +1 -0
  7. package/package.json +54 -0
  8. package/src/common/adaptive-theme-provider.js +19 -0
  9. package/src/components/accordion.jsx +306 -0
  10. package/src/components/alert.jsx +137 -0
  11. package/src/components/app-bar.jsx +105 -0
  12. package/src/components/autocomplete.jsx +347 -0
  13. package/src/components/avatar.jsx +160 -0
  14. package/src/components/box.jsx +165 -0
  15. package/src/components/button.jsx +104 -0
  16. package/src/components/card.jsx +156 -0
  17. package/src/components/checkbox.jsx +63 -0
  18. package/src/components/chip.jsx +137 -0
  19. package/src/components/collapse.jsx +188 -0
  20. package/src/components/container.jsx +67 -0
  21. package/src/components/date-picker.jsx +528 -0
  22. package/src/components/dialog-content-text.jsx +27 -0
  23. package/src/components/dialog.jsx +584 -0
  24. package/src/components/divider.jsx +192 -0
  25. package/src/components/drawer.jsx +255 -0
  26. package/src/components/form-control-label.jsx +89 -0
  27. package/src/components/form-group.jsx +32 -0
  28. package/src/components/form-label.jsx +54 -0
  29. package/src/components/grid.jsx +135 -0
  30. package/src/components/icon-button.jsx +101 -0
  31. package/src/components/index.js +78 -0
  32. package/src/components/input-adornment.jsx +43 -0
  33. package/src/components/input-label.jsx +55 -0
  34. package/src/components/list.jsx +239 -0
  35. package/src/components/menu.jsx +370 -0
  36. package/src/components/paper.jsx +173 -0
  37. package/src/components/radio-group.jsx +76 -0
  38. package/src/components/radio.jsx +108 -0
  39. package/src/components/select.jsx +308 -0
  40. package/src/components/slider.jsx +382 -0
  41. package/src/components/stack.jsx +110 -0
  42. package/src/components/table.jsx +243 -0
  43. package/src/components/tabs.jsx +363 -0
  44. package/src/components/text-field.jsx +289 -0
  45. package/src/components/toggle-button.jsx +209 -0
  46. package/src/components/toolbar.jsx +48 -0
  47. package/src/components/tooltip.jsx +127 -0
  48. package/src/components/typography.jsx +77 -0
  49. package/src/global-state.js +29 -0
  50. package/src/index.css +110 -0
  51. package/src/index.js +6 -0
  52. package/src/lib/useMediaQuery.js +37 -0
  53. package/src/lib/utils.js +113 -0
@@ -0,0 +1,135 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { useMediaQuery } from "../lib/useMediaQuery";
4
+
5
+ const Grid = React.forwardRef(
6
+ (
7
+ {
8
+ className,
9
+ container = false,
10
+ item = false,
11
+ xs,
12
+ sm,
13
+ md,
14
+ lg,
15
+ xl,
16
+ spacing,
17
+ sx,
18
+ style,
19
+ children,
20
+ ...props
21
+ },
22
+ ref
23
+ ) => {
24
+ // Use media query strings directly
25
+ const isXs = useMediaQuery("@media (max-width:599px)");
26
+ const isSm = useMediaQuery(
27
+ "@media (min-width:600px) and (max-width:959px)"
28
+ );
29
+ const isMd = useMediaQuery(
30
+ "@media (min-width:960px) and (max-width:1279px)"
31
+ );
32
+ const isLg = useMediaQuery(
33
+ "@media (min-width:1280px) and (max-width:1919px)"
34
+ );
35
+ const isXl = useMediaQuery("@media (min-width:1920px)");
36
+
37
+ // Convert sx prop to style if provided
38
+ const sxStyles = React.useMemo(() => {
39
+ if (!sx) return {};
40
+ const sxObj = typeof sx === "function" ? sx({}) : sx;
41
+ return sxObj;
42
+ }, [sx]);
43
+
44
+ // Convert MUI spacing to pixels (MUI spacing unit = 8px)
45
+ const spacingToPx = (value) => {
46
+ if (typeof value === "number") {
47
+ return `${value * 8}px`;
48
+ }
49
+ return "0px";
50
+ };
51
+
52
+ // Get responsive breakpoint value
53
+ const getBreakpointValue = () => {
54
+ if (isXl && xl !== undefined) return xl;
55
+ if (isLg && lg !== undefined) return lg;
56
+ if (isMd && md !== undefined) return md;
57
+ if (isSm && sm !== undefined) return sm;
58
+ if (isXs && xs !== undefined) return xs;
59
+ // Fallback to xs if provided, otherwise undefined
60
+ return xs;
61
+ };
62
+
63
+ const breakpointValue = getBreakpointValue();
64
+
65
+ // Calculate flex basis and max width for item
66
+ const getItemStyles = () => {
67
+ if (!item) return {};
68
+ if (breakpointValue === undefined) return {};
69
+
70
+ // MUI Grid uses 12 columns
71
+ const columns = 12;
72
+ const percentage = (breakpointValue / columns) * 100;
73
+
74
+ return {
75
+ flexBasis: `${percentage}%`,
76
+ maxWidth: `${percentage}%`,
77
+ flexGrow: 0,
78
+ flexShrink: 0,
79
+ boxSizing: "border-box",
80
+ };
81
+ };
82
+
83
+ // Get container styles
84
+ // MUI Grid spacing: container has negative margins only on left and top
85
+ // This offsets item padding to align items with container edges on left/top
86
+ // Right and bottom edges maintain spacing through item padding
87
+ const getContainerStyles = () => {
88
+ if (!container) return {};
89
+
90
+ const spacingPx = spacingToPx(spacing || 0);
91
+
92
+ return {
93
+ display: "flex",
94
+ flexWrap: "wrap",
95
+ width: "100%",
96
+ marginLeft: spacing ? `-${spacingPx}` : undefined,
97
+ marginTop: spacing ? `-${spacingPx}` : undefined,
98
+ };
99
+ };
100
+
101
+ // Get item styles with padding for spacing
102
+ // MUI Grid spacing: items have paddingLeft and paddingTop
103
+ // This creates spacing between items and from left/top edges
104
+ // Right and bottom spacing is maintained by the last items' padding
105
+ const getItemPaddingStyles = () => {
106
+ if (!item) return {};
107
+ if (!spacing) return {};
108
+
109
+ const spacingPx = spacingToPx(spacing);
110
+
111
+ return {
112
+ paddingLeft: spacingPx,
113
+ paddingTop: spacingPx,
114
+ };
115
+ };
116
+
117
+ const mergedStyle = {
118
+ ...getContainerStyles(),
119
+ ...getItemStyles(),
120
+ ...getItemPaddingStyles(),
121
+ ...sxStyles,
122
+ ...style,
123
+ };
124
+
125
+ return (
126
+ <div ref={ref} className={cn(className)} style={mergedStyle} {...props}>
127
+ {children}
128
+ </div>
129
+ );
130
+ }
131
+ );
132
+
133
+ Grid.displayName = "Grid";
134
+
135
+ export { Grid };
@@ -0,0 +1,101 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { cva } from "class-variance-authority";
4
+
5
+ const iconButtonVariants = cva(
6
+ "inline-flex items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "hover:bg-accent hover:text-accent-foreground",
11
+ destructive: "hover:bg-destructive hover:text-destructive-foreground",
12
+ outline:
13
+ "border border-input hover:bg-accent hover:text-accent-foreground",
14
+ ghost: "hover:bg-accent hover:text-accent-foreground",
15
+ },
16
+ size: {
17
+ default: "h-10 w-10",
18
+ sm: "h-8 w-8",
19
+ lg: "h-12 w-12",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ size: "default",
25
+ },
26
+ }
27
+ );
28
+
29
+ const IconButton = React.forwardRef(
30
+ (
31
+ {
32
+ className,
33
+ variant,
34
+ size = "default",
35
+ style,
36
+ color,
37
+ "aria-label": ariaLabel,
38
+ children,
39
+ ...props
40
+ },
41
+ ref
42
+ ) => {
43
+ // Get color class based on color prop
44
+ const getColorClass = () => {
45
+ if (color === "primary") return "text-primary";
46
+ if (color === "secondary") return "text-secondary-foreground";
47
+ if (color === "error") return "text-destructive";
48
+ if (color === "inherit") return "text-inherit";
49
+ return "text-muted-foreground"; // Default
50
+ };
51
+
52
+ // Get icon size based on button size
53
+ const getIconSize = () => {
54
+ if (size === "sm") return 16;
55
+ if (size === "lg") return 24;
56
+ return 20; // default
57
+ };
58
+
59
+ // Clone children to apply size if it's a React element (like lucide-react icons)
60
+ const childrenWithSize = React.useMemo(() => {
61
+ if (React.isValidElement(children) && typeof children.type !== "string") {
62
+ // Check if the element has a size prop (like lucide-react icons)
63
+ if (children.props.size === undefined) {
64
+ return React.cloneElement(children, {
65
+ size: getIconSize(),
66
+ });
67
+ }
68
+ }
69
+ return children;
70
+ }, [children, size]);
71
+
72
+ // Warn if no aria-label is provided (accessibility best practice)
73
+ React.useEffect(() => {
74
+ if (!ariaLabel && !props["aria-labelledby"]) {
75
+ console.warn(
76
+ "IconButton: Missing aria-label or aria-labelledby. Icon-only buttons should have accessible labels."
77
+ );
78
+ }
79
+ }, [ariaLabel, props["aria-labelledby"]]);
80
+
81
+ return (
82
+ <button
83
+ className={cn(
84
+ iconButtonVariants({ variant, size }),
85
+ "rounded-2xl",
86
+ getColorClass(),
87
+ className
88
+ )}
89
+ ref={ref}
90
+ style={style}
91
+ aria-label={ariaLabel}
92
+ {...props}
93
+ >
94
+ {childrenWithSize}
95
+ </button>
96
+ );
97
+ }
98
+ );
99
+ IconButton.displayName = "IconButton";
100
+
101
+ export { IconButton, iconButtonVariants };
@@ -0,0 +1,78 @@
1
+ export {
2
+ Card,
3
+ CardHeader,
4
+ CardTitle,
5
+ CardDescription,
6
+ CardContent,
7
+ CardFooter,
8
+ CardActionArea,
9
+ } from "./card";
10
+ export { Button } from "./button";
11
+ // buttonVariants is exported separately if needed, but not as a component
12
+ export {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogHeader,
16
+ DialogFooter,
17
+ DialogTitle,
18
+ DialogDescription,
19
+ DialogActions,
20
+ } from "./dialog";
21
+ export { IconButton } from "./icon-button";
22
+ // iconButtonVariants is exported separately if needed, but not as a component
23
+ export { Typography } from "./typography";
24
+ export { Chip } from "./chip";
25
+ export { TextField } from "./text-field";
26
+ export { Select, FormControl } from "./select";
27
+ export { Checkbox } from "./checkbox";
28
+ export {
29
+ List,
30
+ ListItem,
31
+ ListItemIcon,
32
+ ListItemText,
33
+ ListItemButton,
34
+ ListItemAvatar,
35
+ } from "./list";
36
+ export { Accordion, AccordionSummary, AccordionDetails } from "./accordion";
37
+ export { Alert } from "./alert";
38
+ export { Tabs, Tab } from "./tabs";
39
+ export { Tooltip } from "./tooltip";
40
+ export { Autocomplete } from "./autocomplete";
41
+ export { DatePicker } from "./date-picker";
42
+ export { Drawer } from "./drawer";
43
+ export { FormControlLabel } from "./form-control-label";
44
+ export { FormLabel } from "./form-label";
45
+ export { FormGroup } from "./form-group";
46
+ export { Radio } from "./radio";
47
+ export { RadioGroup } from "./radio-group";
48
+ export { Collapse } from "./collapse";
49
+ export { DialogContentText } from "./dialog-content-text";
50
+ export { Container } from "./container";
51
+ export { Divider } from "./divider";
52
+ export { Avatar } from "./avatar";
53
+ export { Stack } from "./stack";
54
+ export { InputAdornment } from "./input-adornment";
55
+ export { Paper } from "./paper";
56
+ export { AppBar } from "./app-bar";
57
+ export { Toolbar } from "./toolbar";
58
+ export {
59
+ Menu,
60
+ MenuItem,
61
+ MenuItemIcon,
62
+ MenuItemText,
63
+ } from "./menu";
64
+ // Export Select's MenuItem as SelectMenuItem to avoid naming conflict
65
+ export { MenuItem as SelectMenuItem } from "./select";
66
+ export {
67
+ Table,
68
+ TableContainer,
69
+ TableHead,
70
+ TableBody,
71
+ TableRow,
72
+ TableCell,
73
+ } from "./table";
74
+ export { ToggleButton, ToggleButtonGroup } from "./toggle-button";
75
+ export { Grid } from "./grid";
76
+ export { InputLabel } from "./input-label";
77
+ export { Slider } from "./slider";
78
+ export { Box } from "./box";
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const InputAdornment = React.forwardRef(
5
+ (
6
+ {
7
+ className,
8
+ position = "end",
9
+ disablePointerEvents = false,
10
+ disableTypography = false,
11
+ children,
12
+ ...props
13
+ },
14
+ ref
15
+ ) => {
16
+ return (
17
+ <div
18
+ ref={ref}
19
+ className={cn(
20
+ "flex items-center",
21
+ position === "start" ? "mr-2" : "ml-2",
22
+ disablePointerEvents && "pointer-events-none",
23
+ className
24
+ )}
25
+ style={{
26
+ pointerEvents: disablePointerEvents ? "none" : "auto",
27
+ cursor: disablePointerEvents ? "default" : "inherit",
28
+ }}
29
+ {...props}
30
+ >
31
+ {disableTypography ? (
32
+ children
33
+ ) : (
34
+ <span className="text-sm text-muted-foreground">{children}</span>
35
+ )}
36
+ </div>
37
+ );
38
+ }
39
+ );
40
+
41
+ InputAdornment.displayName = "InputAdornment";
42
+
43
+ export { InputAdornment };
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const InputLabel = React.forwardRef(
5
+ (
6
+ {
7
+ children,
8
+ className,
9
+ required = false,
10
+ disabled = false,
11
+ error = false,
12
+ focused = false,
13
+ shrink = false,
14
+ sx,
15
+ style,
16
+ ...props
17
+ },
18
+ ref
19
+ ) => {
20
+ // Convert sx prop to style if provided
21
+ const sxStyles = React.useMemo(() => {
22
+ if (!sx) return {};
23
+ const sxObj = typeof sx === "function" ? sx({}) : sx;
24
+ return sxObj;
25
+ }, [sx]);
26
+
27
+ return (
28
+ <label
29
+ ref={ref}
30
+ className={cn(
31
+ "block text-sm font-medium mb-2 text-foreground",
32
+ error
33
+ ? "text-destructive"
34
+ : focused
35
+ ? "text-primary"
36
+ : "text-foreground",
37
+ disabled && "opacity-50",
38
+ className
39
+ )}
40
+ style={{
41
+ ...sxStyles,
42
+ ...style,
43
+ }}
44
+ {...props}
45
+ >
46
+ {children}
47
+ {required && <span className="ml-1 text-destructive">*</span>}
48
+ </label>
49
+ );
50
+ }
51
+ );
52
+
53
+ InputLabel.displayName = "InputLabel";
54
+
55
+ export { InputLabel };
@@ -0,0 +1,239 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { Typography } from "./typography";
4
+
5
+ const List = React.forwardRef(
6
+ ({ className, style, children, ...props }, ref) => {
7
+ return (
8
+ <div
9
+ ref={ref}
10
+ className={cn("w-full", className)}
11
+ style={{
12
+ padding: 0,
13
+ margin: 0,
14
+ ...style,
15
+ }}
16
+ {...props}
17
+ >
18
+ {children}
19
+ </div>
20
+ );
21
+ }
22
+ );
23
+ List.displayName = "List";
24
+
25
+ const ListItem = React.forwardRef(
26
+ (
27
+ {
28
+ className,
29
+ style,
30
+ alignItems = "flex-start",
31
+ disablePadding,
32
+ dense,
33
+ children,
34
+ ...props
35
+ },
36
+ ref
37
+ ) => {
38
+ // Extract padding from style to allow override
39
+ const {
40
+ padding,
41
+ paddingTop,
42
+ paddingBottom,
43
+ paddingLeft,
44
+ paddingRight,
45
+ ...restStyle
46
+ } = style || {};
47
+
48
+ const hasPaddingInStyle =
49
+ padding !== undefined ||
50
+ paddingTop !== undefined ||
51
+ paddingBottom !== undefined ||
52
+ paddingLeft !== undefined ||
53
+ paddingRight !== undefined;
54
+
55
+ // If disablePadding is true, don't apply default padding
56
+ // If dense is true, use smaller padding
57
+ let defaultPaddingClass = "";
58
+ if (!disablePadding && !hasPaddingInStyle) {
59
+ defaultPaddingClass = dense ? "py-2 px-8" : "py-4 px-8";
60
+ }
61
+
62
+ return (
63
+ <div
64
+ ref={ref}
65
+ className={cn("flex", defaultPaddingClass, className)}
66
+ style={{
67
+ alignItems: alignItems,
68
+ padding: disablePadding ? 0 : padding,
69
+ paddingTop: disablePadding ? 0 : paddingTop,
70
+ paddingBottom: disablePadding ? 0 : paddingBottom,
71
+ paddingLeft: disablePadding ? 0 : paddingLeft,
72
+ paddingRight: disablePadding ? 0 : paddingRight,
73
+ ...restStyle,
74
+ }}
75
+ {...props}
76
+ >
77
+ {children}
78
+ </div>
79
+ );
80
+ }
81
+ );
82
+ ListItem.displayName = "ListItem";
83
+
84
+ const ListItemIcon = React.forwardRef(
85
+ ({ className, style, children, ...props }, ref) => {
86
+ // Extract margin and color from style to allow override
87
+ const { marginRight, minWidth, color, ...restStyle } = style || {};
88
+
89
+ return (
90
+ <div
91
+ ref={ref}
92
+ className={cn("flex items-center justify-center", className)}
93
+ style={{
94
+ minWidth: minWidth !== undefined ? minWidth : "auto",
95
+ marginRight: marginRight !== undefined ? marginRight : "1rem",
96
+ ...(color !== undefined && { color }),
97
+ ...restStyle,
98
+ }}
99
+ {...props}
100
+ >
101
+ {children}
102
+ </div>
103
+ );
104
+ }
105
+ );
106
+ ListItemIcon.displayName = "ListItemIcon";
107
+
108
+ const ListItemText = React.forwardRef(
109
+ ({ className, style, primary, secondary, children, ...props }, ref) => {
110
+ return (
111
+ <div
112
+ ref={ref}
113
+ className={cn("flex flex-col", className)}
114
+ style={{
115
+ flex: 1,
116
+ margin: 0,
117
+ ...style,
118
+ }}
119
+ {...props}
120
+ >
121
+ {primary && (
122
+ <div className={cn(secondary && "mb-2")}>
123
+ {React.isValidElement(primary) ? (
124
+ primary
125
+ ) : (
126
+ <Typography variant="body1" color="default">
127
+ {primary}
128
+ </Typography>
129
+ )}
130
+ </div>
131
+ )}
132
+ {secondary && (
133
+ <div>
134
+ {React.isValidElement(secondary) ? (
135
+ secondary
136
+ ) : (
137
+ <Typography variant="body2" color="muted">
138
+ {secondary}
139
+ </Typography>
140
+ )}
141
+ </div>
142
+ )}
143
+ {children}
144
+ </div>
145
+ );
146
+ }
147
+ );
148
+ ListItemText.displayName = "ListItemText";
149
+
150
+ const ListItemButton = React.forwardRef(
151
+ (
152
+ { className, style, selected, onClick, disabled, children, dense, disablePadding, ...props },
153
+ ref
154
+ ) => {
155
+ const [hovered, setHovered] = React.useState(false);
156
+
157
+ const handleKeyDown = (e) => {
158
+ if (disabled) return;
159
+ // Support Enter and Space keys for keyboard navigation
160
+ if (e.key === "Enter" || e.key === " ") {
161
+ e.preventDefault();
162
+ if (onClick) {
163
+ onClick(e);
164
+ }
165
+ }
166
+ };
167
+
168
+ // Calculate padding for ListItemButton
169
+ let paddingClass = "";
170
+ if (!disablePadding) {
171
+ paddingClass = dense ? "py-2 px-8" : "py-4 px-8";
172
+ }
173
+
174
+ return (
175
+ <div
176
+ ref={ref}
177
+ role="button"
178
+ tabIndex={disabled ? -1 : 0}
179
+ aria-disabled={disabled}
180
+ aria-selected={selected}
181
+ className={cn(
182
+ "flex cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
183
+ paddingClass,
184
+ selected
185
+ ? "bg-primary text-primary-foreground hover:bg-primary/90"
186
+ : hovered
187
+ ? "bg-accent text-accent-foreground"
188
+ : "hover:bg-accent hover:text-accent-foreground",
189
+ disabled && "opacity-50 cursor-not-allowed",
190
+ className
191
+ )}
192
+ style={{
193
+ alignItems: "center",
194
+ ...style,
195
+ }}
196
+ onClick={disabled ? undefined : onClick}
197
+ onKeyDown={handleKeyDown}
198
+ onMouseEnter={() => !disabled && setHovered(true)}
199
+ onMouseLeave={() => setHovered(false)}
200
+ {...props}
201
+ >
202
+ {children}
203
+ </div>
204
+ );
205
+ }
206
+ );
207
+ ListItemButton.displayName = "ListItemButton";
208
+
209
+ const ListItemAvatar = React.forwardRef(
210
+ ({ className, style, children, ...props }, ref) => {
211
+ // Extract margin and minWidth from style to allow override
212
+ const { marginRight, minWidth, ...restStyle } = style || {};
213
+
214
+ return (
215
+ <div
216
+ ref={ref}
217
+ className={cn("flex items-center justify-center", className)}
218
+ style={{
219
+ minWidth: minWidth !== undefined ? minWidth : "40px",
220
+ marginRight: marginRight !== undefined ? marginRight : "1rem",
221
+ ...restStyle,
222
+ }}
223
+ {...props}
224
+ >
225
+ {children}
226
+ </div>
227
+ );
228
+ }
229
+ );
230
+ ListItemAvatar.displayName = "ListItemAvatar";
231
+
232
+ export {
233
+ List,
234
+ ListItem,
235
+ ListItemIcon,
236
+ ListItemText,
237
+ ListItemButton,
238
+ ListItemAvatar,
239
+ };