@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,108 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { RadioGroupContext } from "./radio-group";
4
+
5
+ const Radio = React.forwardRef(
6
+ (
7
+ {
8
+ className,
9
+ checked: checkedProp,
10
+ onChange: onChangeProp,
11
+ disabled: disabledProp,
12
+ value,
13
+ size = "medium",
14
+ sx,
15
+ style,
16
+ ...props
17
+ },
18
+ ref
19
+ ) => {
20
+ const [hovered, setHovered] = React.useState(false);
21
+ const radioGroup = React.useContext(RadioGroupContext);
22
+
23
+ const checked =
24
+ checkedProp !== undefined ? checkedProp : radioGroup?.value === value;
25
+
26
+ const disabled =
27
+ disabledProp !== undefined ? disabledProp : radioGroup?.disabled || false;
28
+
29
+ const handleChange = React.useCallback(
30
+ (event) => {
31
+ if (onChangeProp) {
32
+ onChangeProp(event);
33
+ } else if (radioGroup?.onChange) {
34
+ radioGroup.onChange(event);
35
+ }
36
+ },
37
+ [onChangeProp, radioGroup]
38
+ );
39
+
40
+ const mergedSx = React.useMemo(() => {
41
+ if (!sx) return {};
42
+ return typeof sx === "function" ? sx({}) : sx;
43
+ }, [sx]);
44
+
45
+ const sizeMap = {
46
+ small: 18,
47
+ medium: 20,
48
+ large: 24,
49
+ };
50
+
51
+ const radioSize = sizeMap[size] || sizeMap.medium;
52
+ const dotSize = checked ? radioSize * 0.5 : 0;
53
+
54
+ return (
55
+ <label
56
+ className={cn(
57
+ "relative inline-flex items-center justify-center cursor-pointer",
58
+ disabled && "cursor-not-allowed opacity-50",
59
+ className
60
+ )}
61
+ style={{ ...mergedSx, ...style }}
62
+ >
63
+ <input
64
+ ref={ref}
65
+ type="radio"
66
+ checked={checked}
67
+ onChange={handleChange}
68
+ disabled={disabled}
69
+ value={value}
70
+ name={radioGroup?.name}
71
+ className="sr-only"
72
+ {...props}
73
+ />
74
+ <div
75
+ className={cn(
76
+ "flex items-center justify-center transition-all duration-200 border-2 rounded-full bg-transparent transition-colors",
77
+ disabled && "opacity-50 cursor-not-allowed",
78
+ className,
79
+ checked
80
+ ? "border-primary"
81
+ : hovered && !disabled
82
+ ? "border-primary"
83
+ : "border-input"
84
+ )}
85
+ style={{
86
+ width: `${radioSize}px`,
87
+ height: `${radioSize}px`,
88
+ }}
89
+ onMouseEnter={() => !disabled && setHovered(true)}
90
+ onMouseLeave={() => setHovered(false)}
91
+ >
92
+ {checked && (
93
+ <div
94
+ className="bg-primary rounded-full transition-all"
95
+ style={{
96
+ width: `${dotSize}px`,
97
+ height: `${dotSize}px`,
98
+ }}
99
+ />
100
+ )}
101
+ </div>
102
+ </label>
103
+ );
104
+ }
105
+ );
106
+ Radio.displayName = "Radio";
107
+
108
+ export { Radio };
@@ -0,0 +1,308 @@
1
+ import * as React from "react";
2
+ import {
3
+ cn,
4
+ spacingToPx,
5
+ convertSxToStyle,
6
+ useIsDarkMode,
7
+ } from "../lib/utils";
8
+ import { useComponentBackgroundColor } from "../common/adaptive-theme-provider";
9
+ import { ChevronDown } from "lucide-react";
10
+
11
+ const Select = React.forwardRef(
12
+ (
13
+ {
14
+ className,
15
+ children,
16
+ value,
17
+ onChange,
18
+ size = "small",
19
+ label,
20
+ error = false,
21
+ helperText,
22
+ disabled = false,
23
+ displayEmpty = false,
24
+ native = false,
25
+ renderValue,
26
+ startAdornment,
27
+ endAdornment,
28
+ multiple = false,
29
+ sx,
30
+ style,
31
+ ...props
32
+ },
33
+ ref
34
+ ) => {
35
+ const getComponentBgColor = useComponentBackgroundColor();
36
+ const actualMode = useIsDarkMode();
37
+ const isDark = actualMode === "dark";
38
+ const [focused, setFocused] = React.useState(false);
39
+ const [hovered, setHovered] = React.useState(false);
40
+ const [open, setOpen] = React.useState(false);
41
+ const selectRef = React.useRef(null);
42
+ const menuRef = React.useRef(null);
43
+ const selectId = React.useId();
44
+
45
+ React.useImperativeHandle(ref, () => selectRef.current);
46
+
47
+ // Convert sx prop to style if provided
48
+ const sxStyles = React.useMemo(() => {
49
+ if (!sx) return {};
50
+ const sxObj = typeof sx === "function" ? sx({}) : sx;
51
+ return convertSxToStyle(sxObj, {});
52
+ }, [sx]);
53
+
54
+ const sizeClasses = {
55
+ small: "h-9 text-sm",
56
+ medium: "h-10",
57
+ };
58
+
59
+ const isMultiple = multiple && Array.isArray(value);
60
+ const selectedValues = isMultiple ? value : [value];
61
+
62
+ const selectedOption = React.useMemo(() => {
63
+ if (!children) return "";
64
+ if (isMultiple) {
65
+ return ""; // For multiple, always use renderValue
66
+ }
67
+ const childrenArray = React.Children.toArray(children);
68
+ const selected = childrenArray.find(
69
+ (child) => React.isValidElement(child) && child.props.value === value
70
+ );
71
+ return selected && React.isValidElement(selected)
72
+ ? selected.props.children
73
+ : "";
74
+ }, [children, value, isMultiple]);
75
+
76
+ // Handle click outside
77
+ React.useEffect(() => {
78
+ const handleClickOutside = (event) => {
79
+ if (
80
+ menuRef.current &&
81
+ !menuRef.current.contains(event.target) &&
82
+ selectRef.current &&
83
+ !selectRef.current.contains(event.target)
84
+ ) {
85
+ setOpen(false);
86
+ }
87
+ };
88
+
89
+ if (open) {
90
+ document.addEventListener("mousedown", handleClickOutside);
91
+ return () =>
92
+ document.removeEventListener("mousedown", handleClickOutside);
93
+ }
94
+ }, [open]);
95
+
96
+ const handleOptionClick = (optionValue) => {
97
+ if (onChange) {
98
+ if (isMultiple) {
99
+ const currentValues = Array.isArray(value) ? value : [];
100
+ const newValues = currentValues.includes(optionValue)
101
+ ? currentValues.filter((v) => v !== optionValue)
102
+ : [...currentValues, optionValue];
103
+ onChange({ target: { value: newValues } });
104
+ } else {
105
+ onChange({ target: { value: optionValue } });
106
+ setOpen(false);
107
+ }
108
+ }
109
+ };
110
+
111
+ return (
112
+ <div className="relative w-full" style={{ overflow: "visible" }}>
113
+ {label && (
114
+ <label className="block text-sm font-medium mb-1 text-foreground">
115
+ {label}
116
+ </label>
117
+ )}
118
+ <button
119
+ ref={selectRef}
120
+ type="button"
121
+ role="combobox"
122
+ aria-expanded={open}
123
+ aria-haspopup="listbox"
124
+ aria-label={label || "Select an option"}
125
+ aria-controls={open ? `${selectId}-listbox` : undefined}
126
+ onClick={() => !disabled && setOpen(!open)}
127
+ disabled={disabled}
128
+ className={cn(
129
+ "w-full rounded-3xl border transition-colors flex items-center",
130
+ sizeClasses[size],
131
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
132
+ "disabled:cursor-not-allowed disabled:opacity-50",
133
+ className
134
+ )}
135
+ style={{
136
+ borderColor: error
137
+ ? "hsl(var(--destructive))"
138
+ : focused || open
139
+ ? "hsl(var(--primary))"
140
+ : hovered
141
+ ? "hsl(var(--primary))"
142
+ : "hsl(var(--input))",
143
+ backgroundColor:
144
+ focused || open
145
+ ? getComponentBgColor("hsl(var(--input))")
146
+ : hovered
147
+ ? "hsl(var(--accent))"
148
+ : "transparent",
149
+ color: "hsl(var(--foreground))",
150
+ textAlign: "left",
151
+ fontWeight: "normal",
152
+ // Use consistent padding: 14px (0.875rem) horizontal, 6px (0.375rem) vertical
153
+ paddingLeft: startAdornment ? "8px" : "14px",
154
+ paddingRight: endAdornment ? "8px" : "14px",
155
+ paddingTop: "6px",
156
+ paddingBottom: "6px",
157
+ // If borderRadius is provided in props.style, use it; otherwise let Tailwind's rounded-3xl class handle it
158
+ ...(style?.borderRadius !== undefined
159
+ ? { borderRadius: style.borderRadius }
160
+ : {}),
161
+ ...sxStyles,
162
+ ...Object.fromEntries(
163
+ Object.entries(style || {}).filter(
164
+ ([key]) => key !== "borderRadius"
165
+ )
166
+ ),
167
+ }}
168
+ onFocus={() => !disabled && setFocused(true)}
169
+ onBlur={() => setFocused(false)}
170
+ onMouseEnter={() => !disabled && setHovered(true)}
171
+ onMouseLeave={() => setHovered(false)}
172
+ {...props}
173
+ >
174
+ {startAdornment}
175
+ <span style={{ flex: 1, textAlign: "left" }}>
176
+ {renderValue
177
+ ? renderValue(value)
178
+ : selectedOption || (displayEmpty ? "" : "Select...")}
179
+ </span>
180
+ {endAdornment || (
181
+ <ChevronDown
182
+ size={16}
183
+ className={cn(
184
+ "transition-transform text-foreground",
185
+ open && "rotate-180"
186
+ )}
187
+ />
188
+ )}
189
+ </button>
190
+
191
+ {open && (
192
+ <div
193
+ ref={menuRef}
194
+ id={`${selectId}-listbox`}
195
+ role="listbox"
196
+ aria-label={label || "Options"}
197
+ className="absolute z-50 w-full mt-1 rounded-lg shadow-lg"
198
+ style={{
199
+ backgroundColor: "hsl(var(--card))",
200
+ boxShadow: isDark
201
+ ? "0px 4px 12px rgba(0, 0, 0, 0.4), 0px 0px 0px 1px rgba(255, 255, 255, 0.1)"
202
+ : "0px 4px 12px rgba(0, 0, 0, 0.15)",
203
+ border: isDark
204
+ ? "1px solid rgba(255, 255, 255, 0.1)"
205
+ : "1px solid hsl(var(--border))",
206
+ borderRadius: "8px",
207
+ }}
208
+ >
209
+ {React.Children.map(children, (child) => {
210
+ if (!React.isValidElement(child)) return null;
211
+ const isSelected = isMultiple
212
+ ? selectedValues.includes(child.props.value)
213
+ : child.props.value === value;
214
+ const menuBgColor = "hsl(var(--card))";
215
+
216
+ return (
217
+ <div
218
+ key={child.props.value}
219
+ role="option"
220
+ aria-selected={isSelected}
221
+ onClick={() => handleOptionClick(child.props.value)}
222
+ className="cursor-pointer transition-colors focus:outline-none focus:bg-accent"
223
+ style={{
224
+ backgroundColor: isSelected
225
+ ? "hsl(var(--accent))"
226
+ : menuBgColor,
227
+ color: "hsl(var(--foreground))",
228
+ paddingLeft: "0.875rem",
229
+ paddingRight: "0.875rem",
230
+ paddingTop: "0.5rem",
231
+ paddingBottom: "0.5rem",
232
+ fontSize: "0.875rem",
233
+ fontWeight: "normal",
234
+ textAlign: "left",
235
+ }}
236
+ onKeyDown={(e) => {
237
+ if (e.key === "Enter" || e.key === " ") {
238
+ e.preventDefault();
239
+ handleOptionClick(child.props.value);
240
+ }
241
+ }}
242
+ tabIndex={0}
243
+ onMouseEnter={(e) => {
244
+ if (!isSelected) {
245
+ e.currentTarget.style.backgroundColor =
246
+ "hsl(var(--accent))";
247
+ }
248
+ }}
249
+ onMouseLeave={(e) => {
250
+ if (!isSelected) {
251
+ e.currentTarget.style.backgroundColor = menuBgColor;
252
+ } else {
253
+ e.currentTarget.style.backgroundColor =
254
+ "hsl(var(--accent))";
255
+ }
256
+ }}
257
+ >
258
+ {child.props.children}
259
+ </div>
260
+ );
261
+ })}
262
+ </div>
263
+ )}
264
+ {helperText && (
265
+ <div
266
+ className="text-xs mt-1"
267
+ style={{
268
+ color: error
269
+ ? "hsl(var(--destructive))"
270
+ : "hsl(var(--muted-foreground))",
271
+ }}
272
+ >
273
+ {helperText}
274
+ </div>
275
+ )}
276
+ </div>
277
+ );
278
+ }
279
+ );
280
+ Select.displayName = "Select";
281
+
282
+ const MenuItem = React.forwardRef(
283
+ ({ className, value, children, ...props }, ref) => {
284
+ return null;
285
+ }
286
+ );
287
+ MenuItem.displayName = "MenuItem";
288
+
289
+ const FormControl = React.forwardRef(
290
+ ({ className, fullWidth = false, size, children, style, ...props }, ref) => {
291
+ return (
292
+ <div
293
+ ref={ref}
294
+ className={cn(fullWidth && "w-full", className)}
295
+ style={{
296
+ position: "relative",
297
+ ...style,
298
+ }}
299
+ {...props}
300
+ >
301
+ {children}
302
+ </div>
303
+ );
304
+ }
305
+ );
306
+ FormControl.displayName = "FormControl";
307
+
308
+ export { Select, MenuItem, FormControl };