@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,192 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const Divider = React.forwardRef(
5
+ (
6
+ {
7
+ className,
8
+ style = {},
9
+ orientation = "horizontal",
10
+ variant = "fullWidth",
11
+ flexItem = false,
12
+ light = false,
13
+ textAlign = "center",
14
+ children,
15
+ ...props
16
+ },
17
+ ref
18
+ ) => {
19
+ const isVertical = orientation === "vertical";
20
+ const isInset = variant === "inset";
21
+ const isMiddle = variant === "middle";
22
+
23
+ // Extract margin values from style prop
24
+ const styleMarginLeft = style?.marginLeft;
25
+ const styleMarginRight = style?.marginRight;
26
+
27
+ // Get divider styles
28
+ const getDividerStyles = () => {
29
+ const baseStyles = {
30
+ borderColor: light ? "rgba(0, 0, 0, 0.08)" : "hsl(var(--border))",
31
+ };
32
+
33
+ if (isVertical) {
34
+ // For vertical divider, use marginLeft and marginRight for horizontal spacing
35
+ // Default spacing: 8px on both sides, unless variant or style specifies otherwise
36
+ const defaultMargin = "8px";
37
+ const marginLeft = styleMarginLeft || (isInset ? "16px" : defaultMargin);
38
+ const marginRight = styleMarginRight || (isInset ? "16px" : defaultMargin);
39
+
40
+ return {
41
+ ...baseStyles,
42
+ borderLeftWidth: "1px",
43
+ borderLeftStyle: "solid",
44
+ height: "100%",
45
+ width: "1px",
46
+ marginLeft: marginLeft,
47
+ marginRight: marginRight,
48
+ };
49
+ }
50
+
51
+ // Horizontal divider
52
+ // Calculate width based on margins to prevent overflow
53
+ // inset: only left margin, middle: both margins, fullWidth: no margins
54
+ const marginLeft =
55
+ styleMarginLeft || (isInset ? "16px" : isMiddle ? "16px" : "0px");
56
+ const marginRight =
57
+ styleMarginRight || (isInset ? "0px" : isMiddle ? "16px" : "0px");
58
+
59
+ // Parse margin values to calculate total margin
60
+ const parseMargin = (margin) => {
61
+ if (!margin || margin === "0px" || margin === "0") return 0;
62
+ if (typeof margin === "number") return margin;
63
+ const match = margin.toString().match(/([\d.]+)/);
64
+ return match ? parseFloat(match[1]) : 0;
65
+ };
66
+
67
+ const marginLeftNum = parseMargin(marginLeft);
68
+ const marginRightNum = parseMargin(marginRight);
69
+ const totalMargin = marginLeftNum + marginRightNum;
70
+
71
+ const styles = {
72
+ ...baseStyles,
73
+ borderTopWidth: "1px",
74
+ borderTopStyle: "solid",
75
+ boxSizing: "border-box",
76
+ };
77
+
78
+ // Set width and margins based on variant
79
+ if (flexItem) {
80
+ styles.width = "1px";
81
+ } else if (variant === "fullWidth") {
82
+ // fullWidth: 100% width, no horizontal margins
83
+ styles.width = "100%";
84
+ styles.marginLeft = "0px";
85
+ styles.marginRight = "0px";
86
+ } else {
87
+ // inset or middle: calculate width based on margins
88
+ styles.width = totalMargin > 0 ? `calc(100% - ${totalMargin}px)` : "100%";
89
+ if (marginLeft !== "0px" && marginLeft !== "0") {
90
+ styles.marginLeft = marginLeft;
91
+ }
92
+ if (marginRight !== "0px" && marginRight !== "0") {
93
+ styles.marginRight = marginRight;
94
+ }
95
+ }
96
+
97
+ return styles;
98
+ };
99
+
100
+ // If has children, render as wrapper with text
101
+ if (children) {
102
+ // Calculate margin for variant
103
+ // inset: only left margin, middle: both margins, fullWidth: no margins
104
+ const leftMargin = styleMarginLeft || (isInset ? "16px" : isMiddle ? "16px" : "0px");
105
+ const rightMargin = styleMarginRight || (isInset ? "0px" : isMiddle ? "16px" : "0px");
106
+
107
+ return (
108
+ <div
109
+ ref={ref}
110
+ className={cn("flex items-center", className)}
111
+ style={{
112
+ width: "100%",
113
+ margin: "8px 0",
114
+ ...style,
115
+ }}
116
+ {...props}
117
+ >
118
+ <div
119
+ style={{
120
+ flex: 1,
121
+ borderTopWidth: "1px",
122
+ borderTopStyle: "solid",
123
+ borderColor: light ? "rgba(0, 0, 0, 0.08)" : "hsl(var(--border))",
124
+ marginLeft: leftMargin !== "0px" ? leftMargin : undefined,
125
+ }}
126
+ />
127
+ <div
128
+ style={{
129
+ paddingLeft: "16px",
130
+ paddingRight: "16px",
131
+ textAlign: textAlign,
132
+ color: "hsl(var(--muted-foreground))",
133
+ fontSize: "0.875rem",
134
+ }}
135
+ >
136
+ {children}
137
+ </div>
138
+ <div
139
+ style={{
140
+ flex: 1,
141
+ borderTopWidth: "1px",
142
+ borderTopStyle: "solid",
143
+ borderColor: light ? "rgba(0, 0, 0, 0.08)" : "hsl(var(--border))",
144
+ marginRight: rightMargin !== "0px" ? rightMargin : undefined,
145
+ }}
146
+ />
147
+ </div>
148
+ );
149
+ }
150
+
151
+ // Simple divider without text
152
+ const dividerStyles = getDividerStyles();
153
+ // Add default vertical spacing for horizontal dividers if not specified
154
+ if (!isVertical && !flexItem) {
155
+ if (style.marginTop === undefined && dividerStyles.marginTop === undefined) {
156
+ dividerStyles.marginTop = "8px";
157
+ }
158
+ if (style.marginBottom === undefined && dividerStyles.marginBottom === undefined) {
159
+ dividerStyles.marginBottom = "8px";
160
+ }
161
+ }
162
+
163
+ // For fullWidth, ensure no horizontal margins override the divider styles
164
+ const finalStyle = { ...dividerStyles, ...style };
165
+ if (!isVertical && variant === "fullWidth" && !flexItem) {
166
+ finalStyle.width = "100%";
167
+ finalStyle.marginLeft = style.marginLeft ?? "0px";
168
+ finalStyle.marginRight = style.marginRight ?? "0px";
169
+ }
170
+
171
+ return (
172
+ <div
173
+ ref={ref}
174
+ className={cn(
175
+ isVertical ? "inline-block" : "block",
176
+ flexItem && "self-stretch",
177
+ className
178
+ )}
179
+ style={{
180
+ padding: 0,
181
+ display: isVertical ? "inline-block" : "block",
182
+ ...finalStyle,
183
+ }}
184
+ {...props}
185
+ />
186
+ );
187
+ }
188
+ );
189
+
190
+ Divider.displayName = "Divider";
191
+
192
+ export { Divider };
@@ -0,0 +1,255 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const Drawer = React.forwardRef(
5
+ (
6
+ {
7
+ anchor = "left",
8
+ open = false,
9
+ onClose,
10
+ onOpenChange,
11
+ children,
12
+ className,
13
+ sx,
14
+ PaperProps,
15
+ transitionDuration = 300,
16
+ keepMounted = false,
17
+ variant = "temporary",
18
+ ModalProps,
19
+ SlideProps,
20
+ ...props
21
+ },
22
+ ref
23
+ ) => {
24
+ const drawerRef = React.useRef(null);
25
+
26
+ React.useImperativeHandle(ref, () => drawerRef.current);
27
+
28
+ // Handle backdrop click - only for temporary variant
29
+ const handleBackdropClick = (e) => {
30
+ if (variant === "temporary" && e.target === e.currentTarget && onClose) {
31
+ onClose(e, "backdropClick");
32
+ if (onOpenChange) {
33
+ onOpenChange(false);
34
+ }
35
+ }
36
+ };
37
+
38
+ // Handle open state changes
39
+ React.useEffect(() => {
40
+ if (onOpenChange) {
41
+ onOpenChange(open);
42
+ }
43
+ }, [open, onOpenChange]);
44
+
45
+ // Handle escape key
46
+ React.useEffect(() => {
47
+ if (!open) return;
48
+
49
+ const handleEscape = (e) => {
50
+ if (e.key === "Escape" && onClose) {
51
+ onClose(e, "escapeKeyDown");
52
+ if (onOpenChange) {
53
+ onOpenChange(false);
54
+ }
55
+ }
56
+ };
57
+
58
+ document.addEventListener("keydown", handleEscape);
59
+ return () => {
60
+ document.removeEventListener("keydown", handleEscape);
61
+ };
62
+ }, [open, onClose]);
63
+
64
+ // Merge sx styles - handle root and paper selectors
65
+ const { rootStyles, paperStyles: sxPaperStyles } = React.useMemo(() => {
66
+ if (!sx) return { rootStyles: {}, paperStyles: {} };
67
+ const sxObj = typeof sx === "function" ? sx({}) : sx;
68
+
69
+ // Extract root-level styles (direct properties)
70
+ const rootStyles = {};
71
+ // Extract paper styles from "& .MuiDrawer-paper" selector
72
+ const paperStyles = {};
73
+
74
+ Object.keys(sxObj).forEach((key) => {
75
+ if (key === "& .MuiDrawer-paper" || key === "&.MuiDrawer-paper") {
76
+ Object.assign(paperStyles, sxObj[key]);
77
+ } else if (key.startsWith("&")) {
78
+ // Other selectors - could be handled here if needed
79
+ } else {
80
+ // Direct style properties go to root
81
+ rootStyles[key] = sxObj[key];
82
+ }
83
+ });
84
+
85
+ return { rootStyles, paperStyles };
86
+ }, [sx]);
87
+
88
+ // Get PaperProps styles
89
+ const paperPropsStyles = React.useMemo(() => {
90
+ const paperSx = PaperProps?.sx;
91
+ if (!paperSx) return {};
92
+ return typeof paperSx === "function" ? paperSx({}) : paperSx;
93
+ }, [PaperProps]);
94
+
95
+ // Get anchor styles
96
+ const getAnchorStyles = () => {
97
+ const baseStyles = {
98
+ position: "fixed",
99
+ zIndex: parseInt(
100
+ getComputedStyle(document.documentElement).getPropertyValue(
101
+ "--z-index-drawer"
102
+ ) || "1200",
103
+ 10
104
+ ),
105
+ transition: `transform ${transitionDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`,
106
+ };
107
+
108
+ switch (anchor) {
109
+ case "left":
110
+ return {
111
+ ...baseStyles,
112
+ top: 0,
113
+ left: 0,
114
+ bottom: 0,
115
+ transform: open ? "translateX(0)" : "translateX(-100%)",
116
+ };
117
+ case "right":
118
+ return {
119
+ ...baseStyles,
120
+ top: 0,
121
+ right: 0,
122
+ bottom: 0,
123
+ transform: open ? "translateX(0)" : "translateX(100%)",
124
+ };
125
+ case "top":
126
+ return {
127
+ ...baseStyles,
128
+ top: 0,
129
+ left: 0,
130
+ right: 0,
131
+ width: "100%",
132
+ transform: open ? "translateY(0)" : "translateY(-100%)",
133
+ };
134
+ case "bottom":
135
+ return {
136
+ ...baseStyles,
137
+ bottom: 0,
138
+ left: 0,
139
+ right: 0,
140
+ width: "100%",
141
+ transform: open ? "translateY(0)" : "translateY(100%)",
142
+ };
143
+ default:
144
+ return baseStyles;
145
+ }
146
+ };
147
+
148
+ const anchorStyles = getAnchorStyles();
149
+
150
+ // Early return after all hooks
151
+ // For permanent variant, always render if keepMounted is true
152
+ if (!open && !keepMounted && variant !== "permanent") return null;
153
+
154
+ // For permanent variant, always show
155
+ const isPermanent = variant === "permanent";
156
+ const shouldShowBackdrop = variant === "temporary" || variant === "persistent";
157
+ const shouldShowDrawer = isPermanent || open || keepMounted;
158
+
159
+ return (
160
+ <>
161
+ {/* Backdrop - only for temporary and persistent variants */}
162
+ {shouldShowBackdrop && shouldShowDrawer && (
163
+ <div
164
+ className="fixed inset-0 bg-black/50"
165
+ style={{
166
+ zIndex:
167
+ parseInt(
168
+ getComputedStyle(document.documentElement).getPropertyValue(
169
+ "--z-index-drawer"
170
+ ) || "1200",
171
+ 10
172
+ ) - 1,
173
+ opacity: open ? 1 : 0,
174
+ transition: `opacity ${transitionDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`,
175
+ pointerEvents: open ? "auto" : "none",
176
+ }}
177
+ onClick={handleBackdropClick}
178
+ aria-hidden="true"
179
+ />
180
+ )}
181
+
182
+ {/* Drawer */}
183
+ <div
184
+ ref={drawerRef}
185
+ className={cn(
186
+ "MuiDrawer-root",
187
+ "fixed",
188
+ anchor === "left" || anchor === "right" ? "h-full" : "w-full",
189
+ anchor === "top" || anchor === "bottom" ? "h-auto" : "w-auto",
190
+ !open && "pointer-events-none",
191
+ className
192
+ )}
193
+ style={{
194
+ ...anchorStyles,
195
+ ...rootStyles,
196
+ display: shouldShowDrawer ? "block" : "none",
197
+ // For permanent variant, always visible
198
+ transform: isPermanent ? "translateX(0)" : anchorStyles.transform,
199
+ ...props.style,
200
+ }}
201
+ role="presentation"
202
+ {...Object.fromEntries(
203
+ Object.entries(props).filter(([key]) => key !== "style")
204
+ )}
205
+ >
206
+ {/* Paper (drawer content) */}
207
+ <div
208
+ className={cn(
209
+ "MuiDrawer-paper",
210
+ "MuiPaper-root",
211
+ anchor === "left" || anchor === "right" ? "h-full" : "",
212
+ "w-full",
213
+ anchor === "left" || anchor === "right"
214
+ ? "overflow-y-auto"
215
+ : anchor === "top" || anchor === "bottom"
216
+ ? "overflow-y-auto"
217
+ : "",
218
+ PaperProps?.className
219
+ )}
220
+ style={{
221
+ backgroundColor: "hsl(var(--card))",
222
+ boxShadow:
223
+ sxPaperStyles.boxShadow !== undefined
224
+ ? sxPaperStyles.boxShadow
225
+ : paperPropsStyles.boxShadow !== undefined
226
+ ? paperPropsStyles.boxShadow
227
+ : PaperProps?.style?.boxShadow !== undefined
228
+ ? PaperProps.style.boxShadow
229
+ : open
230
+ ? "0px 8px 10px -5px rgba(0,0,0,0.2), 0px 16px 24px 2px rgba(0,0,0,0.14), 0px 6px 30px 5px rgba(0,0,0,0.12)"
231
+ : "none", // No shadow when closed
232
+ ...(anchor === "left" || anchor === "right"
233
+ ? {
234
+ height: "100%",
235
+ width: PaperProps?.style?.width || sxPaperStyles.width || paperPropsStyles.width || "300px",
236
+ minWidth: PaperProps?.style?.minWidth || sxPaperStyles.minWidth || paperPropsStyles.minWidth || "300px",
237
+ maxWidth: PaperProps?.style?.maxWidth || sxPaperStyles.maxWidth || paperPropsStyles.maxWidth || "90vw"
238
+ }
239
+ : { width: "100%" }),
240
+ ...sxPaperStyles,
241
+ ...paperPropsStyles,
242
+ ...PaperProps?.style,
243
+ }}
244
+ >
245
+ {children}
246
+ </div>
247
+ </div>
248
+ </>
249
+ );
250
+ }
251
+ );
252
+
253
+ Drawer.displayName = "Drawer";
254
+
255
+ export { Drawer };
@@ -0,0 +1,89 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const FormControlLabel = React.forwardRef(
5
+ (
6
+ {
7
+ control,
8
+ label,
9
+ labelPlacement = "end",
10
+ disabled = false,
11
+ value,
12
+ className,
13
+ sx,
14
+ ...props
15
+ },
16
+ ref
17
+ ) => {
18
+ // Merge sx styles
19
+ const mergedSx = React.useMemo(() => {
20
+ if (!sx) return {};
21
+ return typeof sx === "function" ? sx({}) : sx;
22
+ }, [sx]);
23
+
24
+ const isLabelStart = labelPlacement === "start";
25
+ const isLabelTop = labelPlacement === "top";
26
+ const isLabelBottom = labelPlacement === "bottom";
27
+
28
+ const controlWithValue = React.useMemo(() => {
29
+ if (value !== undefined && React.isValidElement(control)) {
30
+ return React.cloneElement(control, {
31
+ ...control.props,
32
+ value,
33
+ });
34
+ }
35
+ return control;
36
+ }, [control, value]);
37
+
38
+ return (
39
+ <label
40
+ ref={ref}
41
+ className={cn(
42
+ "inline-flex items-center cursor-pointer",
43
+ disabled && "opacity-50 cursor-not-allowed",
44
+ isLabelTop && "flex-col items-start",
45
+ isLabelBottom && "flex-col-reverse items-start",
46
+ isLabelStart && "flex-row-reverse items-center",
47
+ !isLabelTop && !isLabelBottom && !isLabelStart && "items-center",
48
+ className
49
+ )}
50
+ style={mergedSx}
51
+ {...props}
52
+ >
53
+ {/* Control */}
54
+ <span
55
+ className={cn(
56
+ "inline-flex items-center justify-center shrink-0",
57
+ isLabelTop && "mb-1",
58
+ isLabelBottom && "mt-1",
59
+ isLabelStart && "ml-2",
60
+ !isLabelStart && !isLabelTop && !isLabelBottom && "mr-2"
61
+ )}
62
+ style={{
63
+ minWidth: "20px",
64
+ minHeight: "20px",
65
+ }}
66
+ >
67
+ {controlWithValue}
68
+ </span>
69
+
70
+ {/* Label */}
71
+ {label && (
72
+ <span
73
+ className={cn(
74
+ "text-sm text-foreground",
75
+ disabled && "text-muted-foreground opacity-50",
76
+ isLabelStart && "mr-2"
77
+ )}
78
+ >
79
+ {label}
80
+ </span>
81
+ )}
82
+ </label>
83
+ );
84
+ }
85
+ );
86
+
87
+ FormControlLabel.displayName = "FormControlLabel";
88
+
89
+ export { FormControlLabel };
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const FormGroup = React.forwardRef(
5
+ ({ children, className, sx, style, row = false, ...props }, ref) => {
6
+ const mergedSx = React.useMemo(() => {
7
+ if (!sx) return {};
8
+ return typeof sx === "function" ? sx({}) : sx;
9
+ }, [sx]);
10
+
11
+ return (
12
+ <div
13
+ ref={ref}
14
+ className={cn(
15
+ "flex",
16
+ row ? "flex-row flex-wrap gap-4" : "flex-col gap-3",
17
+ className
18
+ )}
19
+ style={{
20
+ ...mergedSx,
21
+ ...style,
22
+ }}
23
+ {...props}
24
+ >
25
+ {children}
26
+ </div>
27
+ );
28
+ }
29
+ );
30
+ FormGroup.displayName = "FormGroup";
31
+
32
+ export { FormGroup };
@@ -0,0 +1,54 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const FormLabel = React.forwardRef(
5
+ (
6
+ {
7
+ children,
8
+ component = "label",
9
+ className,
10
+ sx,
11
+ style,
12
+ required = false,
13
+ disabled = false,
14
+ error = false,
15
+ focused = false,
16
+ ...props
17
+ },
18
+ ref
19
+ ) => {
20
+ const mergedSx = React.useMemo(() => {
21
+ if (!sx) return {};
22
+ return typeof sx === "function" ? sx({}) : sx;
23
+ }, [sx]);
24
+
25
+ const Component = component === "legend" ? "legend" : "label";
26
+
27
+ return (
28
+ <Component
29
+ ref={ref}
30
+ className={cn(
31
+ "block text-sm font-medium mb-1",
32
+ error
33
+ ? "text-destructive"
34
+ : focused
35
+ ? "text-primary"
36
+ : "text-foreground",
37
+ disabled && "opacity-50 cursor-not-allowed",
38
+ className
39
+ )}
40
+ style={{
41
+ ...mergedSx,
42
+ ...style,
43
+ }}
44
+ {...props}
45
+ >
46
+ {children}
47
+ {required && <span className="ml-1 text-destructive">*</span>}
48
+ </Component>
49
+ );
50
+ }
51
+ );
52
+ FormLabel.displayName = "FormLabel";
53
+
54
+ export { FormLabel };