@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.
- package/README.md +188 -0
- package/dist/index.cjs.js +9 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.css +2 -0
- package/dist/index.esm.js +9 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +54 -0
- package/src/common/adaptive-theme-provider.js +19 -0
- package/src/components/accordion.jsx +306 -0
- package/src/components/alert.jsx +137 -0
- package/src/components/app-bar.jsx +105 -0
- package/src/components/autocomplete.jsx +347 -0
- package/src/components/avatar.jsx +160 -0
- package/src/components/box.jsx +165 -0
- package/src/components/button.jsx +104 -0
- package/src/components/card.jsx +156 -0
- package/src/components/checkbox.jsx +63 -0
- package/src/components/chip.jsx +137 -0
- package/src/components/collapse.jsx +188 -0
- package/src/components/container.jsx +67 -0
- package/src/components/date-picker.jsx +528 -0
- package/src/components/dialog-content-text.jsx +27 -0
- package/src/components/dialog.jsx +584 -0
- package/src/components/divider.jsx +192 -0
- package/src/components/drawer.jsx +255 -0
- package/src/components/form-control-label.jsx +89 -0
- package/src/components/form-group.jsx +32 -0
- package/src/components/form-label.jsx +54 -0
- package/src/components/grid.jsx +135 -0
- package/src/components/icon-button.jsx +101 -0
- package/src/components/index.js +78 -0
- package/src/components/input-adornment.jsx +43 -0
- package/src/components/input-label.jsx +55 -0
- package/src/components/list.jsx +239 -0
- package/src/components/menu.jsx +370 -0
- package/src/components/paper.jsx +173 -0
- package/src/components/radio-group.jsx +76 -0
- package/src/components/radio.jsx +108 -0
- package/src/components/select.jsx +308 -0
- package/src/components/slider.jsx +382 -0
- package/src/components/stack.jsx +110 -0
- package/src/components/table.jsx +243 -0
- package/src/components/tabs.jsx +363 -0
- package/src/components/text-field.jsx +289 -0
- package/src/components/toggle-button.jsx +209 -0
- package/src/components/toolbar.jsx +48 -0
- package/src/components/tooltip.jsx +127 -0
- package/src/components/typography.jsx +77 -0
- package/src/global-state.js +29 -0
- package/src/index.css +110 -0
- package/src/index.js +6 -0
- package/src/lib/useMediaQuery.js +37 -0
- 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 };
|