@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,289 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { useComponentBackgroundColor } from "../common/adaptive-theme-provider";
4
+
5
+ const TextField = React.forwardRef(
6
+ (
7
+ {
8
+ className,
9
+ fullWidth = false,
10
+ variant = "outlined",
11
+ size = "small",
12
+ InputProps,
13
+ label,
14
+ error = false,
15
+ helperText,
16
+ placeholder,
17
+ multiline = false,
18
+ rows,
19
+ maxRows,
20
+ disabled = false,
21
+ value,
22
+ floatingLabel = false,
23
+ required = false,
24
+ ...props
25
+ },
26
+ ref
27
+ ) => {
28
+ const getComponentBgColor = useComponentBackgroundColor();
29
+ const [focused, setFocused] = React.useState(false);
30
+ const inputRef = React.useRef(null);
31
+ const combinedRef = React.useCallback(
32
+ (node) => {
33
+ inputRef.current = node;
34
+ if (typeof ref === "function") {
35
+ ref(node);
36
+ } else if (ref) {
37
+ ref.current = node;
38
+ }
39
+ },
40
+ [ref]
41
+ );
42
+
43
+ const hasValue = value !== undefined && value !== null && value !== "";
44
+ const shouldShrinkLabel = floatingLabel && (focused || hasValue);
45
+
46
+ const sizeClasses = {
47
+ small: "text-sm",
48
+ medium: "text-base",
49
+ };
50
+
51
+ const sizePadding = {
52
+ small: "px-2.5 py-1.5",
53
+ medium: "px-3 py-2",
54
+ };
55
+
56
+ // Get background color based on variant
57
+ const getVariantBackgroundColor = () => {
58
+ if (InputProps?.style?.backgroundColor !== undefined) {
59
+ return InputProps.style.backgroundColor;
60
+ }
61
+ if (props.style?.backgroundColor !== undefined) {
62
+ return props.style.backgroundColor;
63
+ }
64
+
65
+ // Variant-specific background colors
66
+ if (variant === "filled") {
67
+ return focused
68
+ ? getComponentBgColor("hsl(var(--input))")
69
+ : "hsl(var(--muted) / 0.3)"; // Light gray background for filled
70
+ } else if (variant === "standard") {
71
+ return focused
72
+ ? getComponentBgColor("hsl(var(--input))")
73
+ : "hsl(var(--muted) / 0.5)"; // More prominent gray for standard
74
+ } else {
75
+ // outlined (default)
76
+ return focused
77
+ ? getComponentBgColor("hsl(var(--input))")
78
+ : "transparent";
79
+ }
80
+ };
81
+
82
+ const inputBackgroundColor = getVariantBackgroundColor();
83
+
84
+ const {
85
+ backgroundColor: propsBgColor,
86
+ height: propsHeight,
87
+ ...restPropsStyle
88
+ } = props.style || {};
89
+
90
+ const finalBackgroundColor =
91
+ InputProps?.style?.backgroundColor !== undefined
92
+ ? InputProps.style.backgroundColor
93
+ : inputBackgroundColor;
94
+
95
+ const inputHeight = InputProps?.style?.height || propsHeight;
96
+
97
+ // Remove style and InputProps from props to avoid conflicts
98
+ const {
99
+ style: propsStyle,
100
+ InputProps: propsInputProps,
101
+ ...restProps
102
+ } = props;
103
+
104
+ // Get border style based on variant
105
+ const getVariantBorderStyle = () => {
106
+ if (variant === "standard") {
107
+ // Standard: only bottom border
108
+ return {
109
+ borderTop: "none",
110
+ borderLeft: "none",
111
+ borderRight: "none",
112
+ borderBottom: error
113
+ ? "2px solid hsl(var(--destructive))"
114
+ : focused
115
+ ? "2px solid hsl(var(--ring))"
116
+ : "2px solid hsl(var(--input))",
117
+ borderRadius: "0",
118
+ };
119
+ } else {
120
+ // outlined and filled: full border
121
+ return {
122
+ borderColor: error
123
+ ? "hsl(var(--destructive))"
124
+ : focused
125
+ ? "hsl(var(--ring))"
126
+ : "hsl(var(--input))",
127
+ };
128
+ }
129
+ };
130
+
131
+ // Build final style object - backgroundColor must come from finalBackgroundColor
132
+ // backgroundColor must be set explicitly to ensure it's not overridden
133
+ const finalStyle = {
134
+ ...getVariantBorderStyle(),
135
+ color: "hsl(var(--foreground))",
136
+ ...(inputHeight && { height: inputHeight }),
137
+ ...restPropsStyle,
138
+ // Ensure backgroundColor is set last to override anything from restPropsStyle
139
+ backgroundColor: finalBackgroundColor,
140
+ };
141
+
142
+ const InputComponent = multiline ? "textarea" : "input";
143
+ // Only show placeholder when there's no value
144
+ const shouldShowPlaceholder =
145
+ !hasValue && (placeholder || restProps.placeholder);
146
+
147
+ // Get type from props, default to "text" for input, undefined for textarea
148
+ const inputType = multiline ? undefined : (restProps.type || "text");
149
+
150
+ const inputProps = {
151
+ ...restProps,
152
+ type: inputType,
153
+ value,
154
+ placeholder: shouldShowPlaceholder
155
+ ? placeholder || restProps.placeholder
156
+ : undefined,
157
+ disabled,
158
+ rows: multiline ? rows : undefined,
159
+ };
160
+
161
+ if (floatingLabel) {
162
+ // Floating label mode: NO label displayed - user should use InputLabel component separately
163
+ return (
164
+ <div className={cn(fullWidth && "w-full", className)}>
165
+ <div className="relative">
166
+ <InputComponent
167
+ ref={combinedRef}
168
+ type={inputType}
169
+ className={cn(
170
+ "w-full transition-all",
171
+ variant === "standard" ? "" : "rounded-2xl border",
172
+ sizeClasses[size],
173
+ sizePadding[size],
174
+ "focus-visible:outline-none",
175
+ "disabled:cursor-not-allowed disabled:opacity-50",
176
+ InputProps?.startAdornment && "pl-10",
177
+ InputProps?.endAdornment && "pr-10",
178
+ multiline && "resize-none"
179
+ )}
180
+ style={{
181
+ ...restPropsStyle,
182
+ ...getVariantBorderStyle(),
183
+ color: "hsl(var(--foreground))",
184
+ backgroundColor: finalBackgroundColor,
185
+ ...(inputHeight && {
186
+ height: inputHeight,
187
+ lineHeight: inputHeight,
188
+ }),
189
+ }}
190
+ onFocus={(e) => {
191
+ setFocused(true);
192
+ if (props.onFocus) props.onFocus(e);
193
+ }}
194
+ onBlur={(e) => {
195
+ setFocused(false);
196
+ if (props.onBlur) props.onBlur(e);
197
+ }}
198
+ {...inputProps}
199
+ />
200
+ {InputProps?.startAdornment && (
201
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 flex items-center justify-center z-10">
202
+ {InputProps.startAdornment}
203
+ </div>
204
+ )}
205
+ {InputProps?.endAdornment && (
206
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center justify-center z-10">
207
+ {InputProps.endAdornment}
208
+ </div>
209
+ )}
210
+ </div>
211
+ {helperText && (
212
+ <div
213
+ className="text-xs mt-1"
214
+ style={{
215
+ color: error
216
+ ? "hsl(var(--destructive))"
217
+ : "hsl(var(--muted-foreground))",
218
+ }}
219
+ >
220
+ {helperText}
221
+ </div>
222
+ )}
223
+ </div>
224
+ );
225
+ }
226
+
227
+ // Non-floating label version
228
+ return (
229
+ <div className={cn(fullWidth && "w-full", className)}>
230
+ {label && (
231
+ <label className="block text-sm font-medium mb-2 text-foreground">
232
+ {label}
233
+ {required && <span className="ml-1 text-destructive">*</span>}
234
+ </label>
235
+ )}
236
+ <div className="relative w-full">
237
+ <InputComponent
238
+ ref={combinedRef}
239
+ type={inputType}
240
+ className={cn(
241
+ "w-full transition-colors",
242
+ variant === "standard" ? "" : "rounded-2xl border",
243
+ sizeClasses[size],
244
+ sizePadding[size],
245
+ "focus-visible:outline-none",
246
+ "disabled:cursor-not-allowed disabled:opacity-50",
247
+ InputProps?.startAdornment && "pl-10",
248
+ InputProps?.endAdornment && "pr-10",
249
+ multiline && "resize-none"
250
+ )}
251
+ style={finalStyle}
252
+ onFocus={(e) => {
253
+ setFocused(true);
254
+ if (props.onFocus) props.onFocus(e);
255
+ }}
256
+ onBlur={(e) => {
257
+ setFocused(false);
258
+ if (props.onBlur) props.onBlur(e);
259
+ }}
260
+ {...inputProps}
261
+ />
262
+ {InputProps?.startAdornment && (
263
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 flex items-center justify-center">
264
+ {InputProps.startAdornment}
265
+ </div>
266
+ )}
267
+ {InputProps?.endAdornment && (
268
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center justify-center">
269
+ {InputProps.endAdornment}
270
+ </div>
271
+ )}
272
+ </div>
273
+ {helperText && (
274
+ <div
275
+ className={cn(
276
+ "text-xs mt-1",
277
+ error ? "text-destructive" : "text-muted-foreground"
278
+ )}
279
+ >
280
+ {helperText}
281
+ </div>
282
+ )}
283
+ </div>
284
+ );
285
+ }
286
+ );
287
+ TextField.displayName = "TextField";
288
+
289
+ export { TextField };
@@ -0,0 +1,209 @@
1
+ import * as React from "react";
2
+ import { cn, useIsDarkMode } from "../lib/utils";
3
+
4
+ const ToggleButton = React.forwardRef(
5
+ (
6
+ {
7
+ className,
8
+ value,
9
+ selected = false,
10
+ onChange,
11
+ disabled = false,
12
+ size = "medium",
13
+ sx,
14
+ style,
15
+ children,
16
+ color,
17
+ ...props
18
+ },
19
+ ref
20
+ ) => {
21
+ const actualMode = useIsDarkMode();
22
+ const isDark = actualMode === "dark";
23
+
24
+ const mergedSx = React.useMemo(() => {
25
+ if (!sx) return {};
26
+ return typeof sx === "function" ? sx({}) : sx;
27
+ }, [sx]);
28
+
29
+ const sizeStyles = {
30
+ small: {
31
+ padding: "4px 6px",
32
+ fontSize: "0.75rem",
33
+ minHeight: "28px",
34
+ },
35
+ medium: {
36
+ padding: "6px 16px",
37
+ fontSize: "0.9375rem",
38
+ minHeight: "36px",
39
+ },
40
+ large: {
41
+ padding: "8px 22px",
42
+ fontSize: "1rem",
43
+ minHeight: "40px",
44
+ },
45
+ };
46
+
47
+ const handleClick = (e) => {
48
+ if (disabled) return;
49
+ if (onChange) {
50
+ onChange(e, value);
51
+ }
52
+ };
53
+
54
+ const baseStyle = {
55
+ ...sizeStyles[size],
56
+ border: "1px solid hsl(var(--border))",
57
+ borderRadius: "8px",
58
+ textTransform: "none",
59
+ fontWeight: 400,
60
+ };
61
+
62
+ const {
63
+ backgroundColor: styleBg,
64
+ color: styleColor,
65
+ borderColor: styleBorder,
66
+ ...restStyle
67
+ } = style || {};
68
+
69
+ const selectedStyle = selected
70
+ ? color
71
+ ? {
72
+ backgroundColor: color,
73
+ color: "white",
74
+ borderColor: color,
75
+ }
76
+ : {
77
+ backgroundColor: "hsl(var(--primary))",
78
+ color: "hsl(var(--primary-foreground))",
79
+ borderColor: "hsl(var(--primary))",
80
+ }
81
+ : {
82
+ backgroundColor: "transparent",
83
+ color: "hsl(var(--foreground))",
84
+ borderColor: "hsl(var(--border))",
85
+ };
86
+
87
+ const buttonStyle = {
88
+ ...baseStyle,
89
+ ...mergedSx,
90
+ ...restStyle,
91
+ ...selectedStyle,
92
+ };
93
+
94
+ return (
95
+ <button
96
+ ref={ref}
97
+ type="button"
98
+ className={cn(
99
+ "inline-flex items-center justify-center transition-colors",
100
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
101
+ "disabled:opacity-50 disabled:pointer-events-none",
102
+ className
103
+ )}
104
+ style={buttonStyle}
105
+ onClick={handleClick}
106
+ disabled={disabled}
107
+ aria-pressed={selected}
108
+ onMouseEnter={(e) => {
109
+ if (!disabled && !selected) {
110
+ e.currentTarget.style.backgroundColor = "hsl(var(--accent) / 0.3)";
111
+ }
112
+ }}
113
+ onMouseLeave={(e) => {
114
+ if (!disabled && !selected) {
115
+ e.currentTarget.style.backgroundColor = "transparent";
116
+ }
117
+ }}
118
+ {...props}
119
+ >
120
+ {children}
121
+ </button>
122
+ );
123
+ }
124
+ );
125
+ ToggleButton.displayName = "ToggleButton";
126
+
127
+ const ToggleButtonGroup = React.forwardRef(
128
+ (
129
+ {
130
+ className,
131
+ value,
132
+ exclusive = false,
133
+ onChange,
134
+ size = "medium",
135
+ sx,
136
+ style,
137
+ children,
138
+ ...props
139
+ },
140
+ ref
141
+ ) => {
142
+ const mergedSx = React.useMemo(() => {
143
+ if (!sx) return {};
144
+ return typeof sx === "function" ? sx({}) : sx;
145
+ }, [sx]);
146
+
147
+ const handleToggleButtonChange = (event, buttonValue) => {
148
+ if (!onChange) return;
149
+
150
+ if (exclusive) {
151
+ // Exclusive mode: single selection
152
+ onChange(event, buttonValue === value ? null : buttonValue);
153
+ } else {
154
+ // Multiple selection mode
155
+ const currentValues = Array.isArray(value) ? value : [];
156
+ const index = currentValues.indexOf(buttonValue);
157
+ const newValues = [...currentValues];
158
+
159
+ if (index === -1) {
160
+ newValues.push(buttonValue);
161
+ } else {
162
+ newValues.splice(index, 1);
163
+ }
164
+
165
+ onChange(event, newValues);
166
+ }
167
+ };
168
+
169
+ // Clone children and inject props
170
+ const childrenArray = React.Children.toArray(children);
171
+ const childrenWithProps = childrenArray.map((child, index) => {
172
+ if (React.isValidElement(child) && child.type === ToggleButton) {
173
+ const buttonValue = child.props.value;
174
+ const isSelected = exclusive
175
+ ? value === buttonValue
176
+ : Array.isArray(value) && value.includes(buttonValue);
177
+
178
+ return React.cloneElement(child, {
179
+ selected: isSelected,
180
+ onChange: handleToggleButtonChange,
181
+ size,
182
+ style: {
183
+ borderRadius: "8px",
184
+ ...child.props.style,
185
+ },
186
+ });
187
+ }
188
+ return child;
189
+ });
190
+
191
+ return (
192
+ <div
193
+ ref={ref}
194
+ className={cn("inline-flex gap-2", className)}
195
+ style={{
196
+ ...mergedSx,
197
+ ...style,
198
+ }}
199
+ role="group"
200
+ {...props}
201
+ >
202
+ {childrenWithProps}
203
+ </div>
204
+ );
205
+ }
206
+ );
207
+ ToggleButtonGroup.displayName = "ToggleButtonGroup";
208
+
209
+ export { ToggleButton, ToggleButtonGroup };
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const Toolbar = React.forwardRef(
5
+ ({ className, sx, style, variant = "regular", children, ...props }, ref) => {
6
+ // Convert sx prop to style if provided
7
+ const sxStyles = React.useMemo(() => {
8
+ if (!sx) return {};
9
+ const sxObj = typeof sx === "function" ? sx({}) : sx;
10
+ return sxObj;
11
+ }, [sx]);
12
+
13
+ // Get variant styles
14
+ const getVariantStyles = () => {
15
+ if (variant === "dense") {
16
+ return {
17
+ minHeight: "48px",
18
+ paddingLeft: "16px",
19
+ paddingRight: "16px",
20
+ };
21
+ } else {
22
+ return {
23
+ minHeight: "64px",
24
+ paddingLeft: "16px",
25
+ paddingRight: "16px",
26
+ };
27
+ }
28
+ };
29
+
30
+ const mergedStyle = {
31
+ display: "flex",
32
+ alignItems: "center",
33
+ ...getVariantStyles(),
34
+ ...sxStyles,
35
+ ...style,
36
+ };
37
+
38
+ return (
39
+ <div ref={ref} className={cn(className)} style={mergedStyle} {...props}>
40
+ {children}
41
+ </div>
42
+ );
43
+ }
44
+ );
45
+
46
+ Toolbar.displayName = "Toolbar";
47
+
48
+ export { Toolbar };
@@ -0,0 +1,127 @@
1
+ import * as React from "react";
2
+ import tippy, { roundArrow } from "tippy.js";
3
+ import "tippy.js/dist/tippy.css";
4
+ import "tippy.js/themes/material.css";
5
+
6
+ // Custom styles to match MUI Tooltip
7
+ const tooltipStyles = `
8
+ .tippy-box[data-theme~='material'] {
9
+ font-size: 0.75rem;
10
+ font-weight: 400;
11
+ padding: 2px 6px;
12
+ line-height: 1.4;
13
+ }
14
+ .tippy-box[data-theme~='material'] .tippy-content {
15
+ padding: 0;
16
+ }
17
+ `;
18
+
19
+ const Tooltip = React.forwardRef(
20
+ (
21
+ {
22
+ title,
23
+ children,
24
+ placement = "top",
25
+ arrow = true,
26
+ delay = [100, 0],
27
+ className,
28
+ style,
29
+ ...props
30
+ },
31
+ ref
32
+ ) => {
33
+ const childRef = React.useRef(null);
34
+ const tippyInstanceRef = React.useRef(null);
35
+
36
+ // Combine refs
37
+ React.useImperativeHandle(ref, () => childRef.current);
38
+
39
+ React.useEffect(() => {
40
+ if (!childRef.current) {
41
+ return;
42
+ }
43
+
44
+ // If no title, don't create tooltip
45
+ if (!title) {
46
+ // Clean up existing tooltip if title is removed
47
+ if (tippyInstanceRef.current) {
48
+ tippyInstanceRef.current.destroy();
49
+ tippyInstanceRef.current = null;
50
+ }
51
+ return;
52
+ }
53
+
54
+ // Inject custom styles if not already injected
55
+ if (!document.getElementById("mui-tooltip-styles")) {
56
+ const style = document.createElement("style");
57
+ style.id = "mui-tooltip-styles";
58
+ style.textContent = tooltipStyles;
59
+ document.head.appendChild(style);
60
+ }
61
+
62
+ // Create tippy instance
63
+ tippyInstanceRef.current = tippy(childRef.current, {
64
+ content: title,
65
+ placement: placement,
66
+ arrow: arrow ? roundArrow : false,
67
+ delay: delay,
68
+ theme: "material",
69
+ animation: "fade",
70
+ duration: [200, 150],
71
+ // Use theme colors
72
+ popperOptions: {
73
+ modifiers: [
74
+ {
75
+ name: "offset",
76
+ options: {
77
+ offset: [0, 8],
78
+ },
79
+ },
80
+ ],
81
+ },
82
+ });
83
+
84
+ return () => {
85
+ if (tippyInstanceRef.current) {
86
+ tippyInstanceRef.current.destroy();
87
+ tippyInstanceRef.current = null;
88
+ }
89
+ };
90
+ }, [title, placement, arrow, delay]);
91
+
92
+ // Update content when title changes
93
+ React.useEffect(() => {
94
+ if (tippyInstanceRef.current && title) {
95
+ tippyInstanceRef.current.setContent(title);
96
+ }
97
+ }, [title]);
98
+
99
+ // Clone child element and attach ref
100
+ const childElement = React.Children.only(children);
101
+
102
+ if (!React.isValidElement(childElement)) {
103
+ return children;
104
+ }
105
+
106
+ return React.cloneElement(childElement, {
107
+ ref: (node) => {
108
+ childRef.current = node;
109
+ // Handle original ref if it exists
110
+ if (typeof childElement.ref === "function") {
111
+ childElement.ref(node);
112
+ } else if (childElement.ref) {
113
+ childElement.ref.current = node;
114
+ }
115
+ },
116
+ className: className
117
+ ? `${childElement.props.className || ""} ${className}`.trim()
118
+ : childElement.props.className,
119
+ style: { ...childElement.props.style, ...style },
120
+ ...props,
121
+ });
122
+ }
123
+ );
124
+
125
+ Tooltip.displayName = "Tooltip";
126
+
127
+ export { Tooltip };