@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,347 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { TextField } from "./text-field";
|
|
4
|
+
|
|
5
|
+
const Autocomplete = React.forwardRef(
|
|
6
|
+
(
|
|
7
|
+
{
|
|
8
|
+
options = [],
|
|
9
|
+
getOptionLabel,
|
|
10
|
+
value,
|
|
11
|
+
onChange,
|
|
12
|
+
inputValue,
|
|
13
|
+
onInputChange,
|
|
14
|
+
filterOptions,
|
|
15
|
+
renderInput,
|
|
16
|
+
className,
|
|
17
|
+
style,
|
|
18
|
+
...props
|
|
19
|
+
},
|
|
20
|
+
ref
|
|
21
|
+
) => {
|
|
22
|
+
const [open, setOpen] = React.useState(false);
|
|
23
|
+
const [filteredOptions, setFilteredOptions] = React.useState(options);
|
|
24
|
+
const inputRef = React.useRef(null);
|
|
25
|
+
const listRef = React.useRef(null);
|
|
26
|
+
|
|
27
|
+
// Combine refs
|
|
28
|
+
React.useImperativeHandle(ref, () => inputRef.current);
|
|
29
|
+
|
|
30
|
+
// Filter options when inputValue or options change
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
// Ensure inputValue is always a string
|
|
33
|
+
const searchValue = typeof inputValue === "string" ? inputValue : (inputValue ? String(inputValue) : "");
|
|
34
|
+
|
|
35
|
+
if (filterOptions) {
|
|
36
|
+
const filtered = filterOptions(options, {
|
|
37
|
+
inputValue: searchValue,
|
|
38
|
+
});
|
|
39
|
+
setFilteredOptions(filtered);
|
|
40
|
+
} else {
|
|
41
|
+
// Default filtering: filter options based on inputValue
|
|
42
|
+
if (searchValue) {
|
|
43
|
+
const filtered = options.filter((option) => {
|
|
44
|
+
const optionLabel = getOptionLabel
|
|
45
|
+
? getOptionLabel(option)
|
|
46
|
+
: option?.name || option?.label || String(option);
|
|
47
|
+
return String(optionLabel).toLowerCase().includes(searchValue.toLowerCase());
|
|
48
|
+
});
|
|
49
|
+
setFilteredOptions(filtered);
|
|
50
|
+
} else {
|
|
51
|
+
// Show all options if input is empty
|
|
52
|
+
setFilteredOptions(options);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, [options, inputValue, filterOptions, getOptionLabel]);
|
|
56
|
+
|
|
57
|
+
const handleInputChange = (event) => {
|
|
58
|
+
// Extract value from event, ensuring it's always a string
|
|
59
|
+
let newValue = "";
|
|
60
|
+
if (event?.target?.value !== undefined) {
|
|
61
|
+
newValue = String(event.target.value);
|
|
62
|
+
} else if (typeof event === "string") {
|
|
63
|
+
newValue = event;
|
|
64
|
+
} else if (event) {
|
|
65
|
+
newValue = String(event);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (onInputChange) {
|
|
69
|
+
onInputChange(event, newValue);
|
|
70
|
+
}
|
|
71
|
+
if (!open) {
|
|
72
|
+
setOpen(true);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleOptionClick = (option) => {
|
|
77
|
+
if (onChange) {
|
|
78
|
+
onChange(null, option);
|
|
79
|
+
}
|
|
80
|
+
// Update inputValue when option is selected
|
|
81
|
+
const optionLabel = getOptionLabel
|
|
82
|
+
? getOptionLabel(option)
|
|
83
|
+
: option?.name || option?.label || String(option);
|
|
84
|
+
// Ensure optionLabel is a string
|
|
85
|
+
const labelString = typeof optionLabel === "string" ? optionLabel : String(optionLabel || "");
|
|
86
|
+
if (onInputChange) {
|
|
87
|
+
onInputChange(null, labelString);
|
|
88
|
+
}
|
|
89
|
+
setOpen(false);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleClickOutside = (event) => {
|
|
93
|
+
if (
|
|
94
|
+
listRef.current &&
|
|
95
|
+
!listRef.current.contains(event.target) &&
|
|
96
|
+
inputRef.current &&
|
|
97
|
+
!inputRef.current.contains(event.target)
|
|
98
|
+
) {
|
|
99
|
+
setOpen(false);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (open) {
|
|
105
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
106
|
+
return () => {
|
|
107
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}, [open]);
|
|
111
|
+
|
|
112
|
+
const displayValue = value
|
|
113
|
+
? getOptionLabel
|
|
114
|
+
? getOptionLabel(value)
|
|
115
|
+
: value?.name || value?.label || String(value)
|
|
116
|
+
: "";
|
|
117
|
+
|
|
118
|
+
// Determine the input value: use inputValue if provided, otherwise use displayValue
|
|
119
|
+
// When value is selected, show displayValue unless user is typing
|
|
120
|
+
const getInputValue = () => {
|
|
121
|
+
// Always ensure we return a string
|
|
122
|
+
if (inputValue !== undefined && inputValue !== null) {
|
|
123
|
+
// If user is typing, show inputValue (ensure it's a string)
|
|
124
|
+
return typeof inputValue === "string" ? inputValue : String(inputValue);
|
|
125
|
+
}
|
|
126
|
+
// If value is selected, show displayValue
|
|
127
|
+
return typeof displayValue === "string" ? displayValue : String(displayValue || "");
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const inputProps = {
|
|
131
|
+
ref: inputRef,
|
|
132
|
+
value: getInputValue(),
|
|
133
|
+
onChange: handleInputChange,
|
|
134
|
+
onFocus: () => setOpen(true),
|
|
135
|
+
...props,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// If renderInput is provided, use it (for MUI compatibility)
|
|
139
|
+
if (renderInput) {
|
|
140
|
+
return (
|
|
141
|
+
<div ref={ref} className={cn("relative", className)} style={style}>
|
|
142
|
+
{renderInput({
|
|
143
|
+
...inputProps,
|
|
144
|
+
InputProps: {
|
|
145
|
+
...(inputProps.InputProps || {}),
|
|
146
|
+
endAdornment: (
|
|
147
|
+
<div
|
|
148
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer"
|
|
149
|
+
onClick={(e) => {
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
setOpen(!open);
|
|
153
|
+
}}
|
|
154
|
+
onMouseDown={(e) => {
|
|
155
|
+
e.stopPropagation();
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<svg
|
|
160
|
+
width="20"
|
|
161
|
+
height="20"
|
|
162
|
+
viewBox="0 0 20 20"
|
|
163
|
+
fill="none"
|
|
164
|
+
style={{
|
|
165
|
+
transform: open ? "rotate(180deg)" : "rotate(0deg)",
|
|
166
|
+
transition: "transform 0.2s",
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<path
|
|
170
|
+
d="M5 7.5L10 12.5L15 7.5"
|
|
171
|
+
stroke="hsl(var(--muted-foreground))"
|
|
172
|
+
strokeWidth="2"
|
|
173
|
+
strokeLinecap="round"
|
|
174
|
+
strokeLinejoin="round"
|
|
175
|
+
/>
|
|
176
|
+
</svg>
|
|
177
|
+
</div>
|
|
178
|
+
),
|
|
179
|
+
},
|
|
180
|
+
})}
|
|
181
|
+
{open && filteredOptions.length > 0 && (
|
|
182
|
+
<div
|
|
183
|
+
ref={listRef}
|
|
184
|
+
className="absolute z-50 w-full mt-1 rounded-2xl border shadow-lg overflow-auto"
|
|
185
|
+
style={{
|
|
186
|
+
maxHeight: "200px", // Ensure at least 4 options are visible (each ~50px)
|
|
187
|
+
minHeight: "160px", // Minimum height for better UX
|
|
188
|
+
backgroundColor: "hsl(var(--card))",
|
|
189
|
+
borderColor: "hsl(var(--border))",
|
|
190
|
+
top: "100%",
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
{filteredOptions.map((option, index) => {
|
|
194
|
+
const optionLabel = getOptionLabel
|
|
195
|
+
? getOptionLabel(option)
|
|
196
|
+
: option?.name || option?.label || String(option);
|
|
197
|
+
const isSelected =
|
|
198
|
+
value &&
|
|
199
|
+
(getOptionLabel
|
|
200
|
+
? getOptionLabel(value) === optionLabel
|
|
201
|
+
: value === option ||
|
|
202
|
+
(value?.id && option?.id && value.id === option.id));
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
key={index}
|
|
207
|
+
className="px-3 py-2 cursor-pointer hover:bg-accent transition-colors"
|
|
208
|
+
style={{
|
|
209
|
+
backgroundColor: isSelected
|
|
210
|
+
? "hsl(var(--accent))"
|
|
211
|
+
: "transparent",
|
|
212
|
+
color: "hsl(var(--foreground))",
|
|
213
|
+
}}
|
|
214
|
+
onClick={() => handleOptionClick(option)}
|
|
215
|
+
onMouseEnter={(e) => {
|
|
216
|
+
if (!isSelected) {
|
|
217
|
+
e.currentTarget.style.backgroundColor =
|
|
218
|
+
"hsl(var(--accent))";
|
|
219
|
+
}
|
|
220
|
+
}}
|
|
221
|
+
onMouseLeave={(e) => {
|
|
222
|
+
e.currentTarget.style.backgroundColor = isSelected
|
|
223
|
+
? "hsl(var(--accent))"
|
|
224
|
+
: "transparent";
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{optionLabel}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
})}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Default render with TextField
|
|
238
|
+
return (
|
|
239
|
+
<div ref={ref} className={cn("relative", className)} style={style}>
|
|
240
|
+
<TextField
|
|
241
|
+
{...inputProps}
|
|
242
|
+
style={{
|
|
243
|
+
...inputProps.style,
|
|
244
|
+
height: inputProps.style?.height || "40px",
|
|
245
|
+
}}
|
|
246
|
+
InputProps={{
|
|
247
|
+
...inputProps.InputProps,
|
|
248
|
+
style: {
|
|
249
|
+
...inputProps.InputProps?.style,
|
|
250
|
+
backgroundColor:
|
|
251
|
+
inputProps.InputProps?.style?.backgroundColor || "transparent",
|
|
252
|
+
height: inputProps.InputProps?.style?.height || "40px",
|
|
253
|
+
},
|
|
254
|
+
endAdornment: (
|
|
255
|
+
<div
|
|
256
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer"
|
|
257
|
+
onClick={(e) => {
|
|
258
|
+
e.stopPropagation();
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
setOpen(!open);
|
|
261
|
+
}}
|
|
262
|
+
onMouseDown={(e) => {
|
|
263
|
+
e.stopPropagation();
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<svg
|
|
268
|
+
width="20"
|
|
269
|
+
height="20"
|
|
270
|
+
viewBox="0 0 20 20"
|
|
271
|
+
fill="none"
|
|
272
|
+
style={{
|
|
273
|
+
transform: open ? "rotate(180deg)" : "rotate(0deg)",
|
|
274
|
+
transition: "transform 0.2s",
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
<path
|
|
278
|
+
d="M5 7.5L10 12.5L15 7.5"
|
|
279
|
+
stroke="hsl(var(--muted-foreground))"
|
|
280
|
+
strokeWidth="2"
|
|
281
|
+
strokeLinecap="round"
|
|
282
|
+
strokeLinejoin="round"
|
|
283
|
+
/>
|
|
284
|
+
</svg>
|
|
285
|
+
</div>
|
|
286
|
+
),
|
|
287
|
+
}}
|
|
288
|
+
/>
|
|
289
|
+
{open && filteredOptions.length > 0 && (
|
|
290
|
+
<div
|
|
291
|
+
ref={listRef}
|
|
292
|
+
className="absolute z-50 w-full mt-1 rounded-2xl border shadow-lg overflow-auto"
|
|
293
|
+
style={{
|
|
294
|
+
maxHeight: "200px", // Ensure at least 4 options are visible (each ~50px)
|
|
295
|
+
minHeight: "160px", // Minimum height for better UX
|
|
296
|
+
backgroundColor: "hsl(var(--card))",
|
|
297
|
+
borderColor: "hsl(var(--border))",
|
|
298
|
+
top: "100%",
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
{filteredOptions.map((option, index) => {
|
|
302
|
+
const optionLabel = getOptionLabel
|
|
303
|
+
? getOptionLabel(option)
|
|
304
|
+
: option?.name || option?.label || String(option);
|
|
305
|
+
const isSelected =
|
|
306
|
+
value &&
|
|
307
|
+
(getOptionLabel
|
|
308
|
+
? getOptionLabel(value) === optionLabel
|
|
309
|
+
: value === option ||
|
|
310
|
+
(value?.id && option?.id && value.id === option.id));
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div
|
|
314
|
+
key={index}
|
|
315
|
+
className="px-3 py-2 cursor-pointer hover:bg-accent transition-colors"
|
|
316
|
+
style={{
|
|
317
|
+
backgroundColor: isSelected
|
|
318
|
+
? "hsl(var(--accent))"
|
|
319
|
+
: "transparent",
|
|
320
|
+
color: "hsl(var(--foreground))",
|
|
321
|
+
}}
|
|
322
|
+
onClick={() => handleOptionClick(option)}
|
|
323
|
+
onMouseEnter={(e) => {
|
|
324
|
+
if (!isSelected) {
|
|
325
|
+
e.currentTarget.style.backgroundColor =
|
|
326
|
+
"hsl(var(--accent))";
|
|
327
|
+
}
|
|
328
|
+
}}
|
|
329
|
+
onMouseLeave={(e) => {
|
|
330
|
+
e.currentTarget.style.backgroundColor = isSelected
|
|
331
|
+
? "hsl(var(--accent))"
|
|
332
|
+
: "transparent";
|
|
333
|
+
}}
|
|
334
|
+
>
|
|
335
|
+
{optionLabel}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
})}
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
Autocomplete.displayName = "Autocomplete";
|
|
346
|
+
|
|
347
|
+
export { Autocomplete };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn, useIsDarkMode } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
const Avatar = React.forwardRef(
|
|
5
|
+
(
|
|
6
|
+
{
|
|
7
|
+
className,
|
|
8
|
+
style,
|
|
9
|
+
src,
|
|
10
|
+
alt,
|
|
11
|
+
children,
|
|
12
|
+
variant = "circular",
|
|
13
|
+
sizes,
|
|
14
|
+
imgProps,
|
|
15
|
+
...props
|
|
16
|
+
},
|
|
17
|
+
ref
|
|
18
|
+
) => {
|
|
19
|
+
// Get variant styles
|
|
20
|
+
const getVariantStyles = () => {
|
|
21
|
+
if (variant === "rounded") {
|
|
22
|
+
return { borderRadius: "8px" };
|
|
23
|
+
}
|
|
24
|
+
if (variant === "square") {
|
|
25
|
+
return { borderRadius: "0px" };
|
|
26
|
+
}
|
|
27
|
+
// circular (default)
|
|
28
|
+
return { borderRadius: "50%" };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const defaultStyles = {
|
|
32
|
+
display: "inline-flex",
|
|
33
|
+
alignItems: "center",
|
|
34
|
+
justifyContent: "center",
|
|
35
|
+
flexShrink: 0,
|
|
36
|
+
position: "relative",
|
|
37
|
+
textAlign: "center",
|
|
38
|
+
fontSize: "1.25rem",
|
|
39
|
+
lineHeight: 1,
|
|
40
|
+
overflow: "hidden",
|
|
41
|
+
userSelect: "none",
|
|
42
|
+
backgroundColor: "hsl(var(--muted))",
|
|
43
|
+
color: "hsl(var(--foreground))",
|
|
44
|
+
fontWeight: 500,
|
|
45
|
+
width: "40px",
|
|
46
|
+
height: "40px",
|
|
47
|
+
...getVariantStyles(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const actualMode = useIsDarkMode();
|
|
51
|
+
const isDark = actualMode === "dark";
|
|
52
|
+
|
|
53
|
+
// If has src, render as image
|
|
54
|
+
if (src) {
|
|
55
|
+
// Get text color based on theme mode if backgroundColor is set but color is not
|
|
56
|
+
const getTextColor = () => {
|
|
57
|
+
// If color is explicitly set in style, use it
|
|
58
|
+
if (style?.color) {
|
|
59
|
+
return style.color;
|
|
60
|
+
}
|
|
61
|
+
// If backgroundColor is set in style, determine text color based on dark mode
|
|
62
|
+
if (style?.backgroundColor) {
|
|
63
|
+
return isDark ? "#000000" : "#ffffff";
|
|
64
|
+
}
|
|
65
|
+
// Otherwise use default color
|
|
66
|
+
return defaultStyles.color;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Merge styles: defaultStyles first, then style to allow overrides
|
|
70
|
+
// But preserve borderRadius and backgroundColor from defaultStyles if not explicitly set in style
|
|
71
|
+
// IMPORTANT: color must be set last to ensure it's not overridden by style spread
|
|
72
|
+
const { color: styleColor, ...styleWithoutColor } = style || {};
|
|
73
|
+
const mergedStylesWithSrc = {
|
|
74
|
+
...defaultStyles,
|
|
75
|
+
...styleWithoutColor, // style without color to avoid override
|
|
76
|
+
// Ensure borderRadius is preserved if not explicitly set in style
|
|
77
|
+
borderRadius: style?.borderRadius ?? defaultStyles.borderRadius,
|
|
78
|
+
// Ensure backgroundColor is preserved if not explicitly set in style
|
|
79
|
+
backgroundColor:
|
|
80
|
+
style?.backgroundColor ?? defaultStyles.backgroundColor,
|
|
81
|
+
// Get text color based on theme mode (set last to ensure it's not overridden)
|
|
82
|
+
color: getTextColor(),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={cn("relative", className)}
|
|
89
|
+
style={mergedStylesWithSrc}
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
<img
|
|
93
|
+
src={src}
|
|
94
|
+
alt={alt}
|
|
95
|
+
sizes={sizes}
|
|
96
|
+
style={{
|
|
97
|
+
width: "100%",
|
|
98
|
+
height: "100%",
|
|
99
|
+
objectFit: "cover",
|
|
100
|
+
}}
|
|
101
|
+
{...imgProps}
|
|
102
|
+
/>
|
|
103
|
+
{children && (
|
|
104
|
+
<div
|
|
105
|
+
style={{
|
|
106
|
+
position: "absolute",
|
|
107
|
+
inset: 0,
|
|
108
|
+
display: "flex",
|
|
109
|
+
alignItems: "center",
|
|
110
|
+
justifyContent: "center",
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Render as div with children
|
|
121
|
+
// Get text color based on theme mode if backgroundColor is set but color is not
|
|
122
|
+
const getTextColor = () => {
|
|
123
|
+
// If color is explicitly set in style, use it
|
|
124
|
+
if (style?.color) {
|
|
125
|
+
return style.color;
|
|
126
|
+
}
|
|
127
|
+
// If backgroundColor is set in style, determine text color based on dark mode
|
|
128
|
+
if (style?.backgroundColor) {
|
|
129
|
+
return isDark ? "#000000" : "#ffffff";
|
|
130
|
+
}
|
|
131
|
+
// Otherwise use default color
|
|
132
|
+
return defaultStyles.color;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Merge styles: defaultStyles first, then style to allow overrides
|
|
136
|
+
// But preserve borderRadius and backgroundColor from defaultStyles if not explicitly set in style
|
|
137
|
+
// IMPORTANT: color must be set last to ensure it's not overridden by style spread
|
|
138
|
+
const { color: styleColor, ...styleWithoutColor } = style || {};
|
|
139
|
+
const mergedStyles = {
|
|
140
|
+
...defaultStyles,
|
|
141
|
+
...styleWithoutColor, // style without color to avoid override
|
|
142
|
+
// Ensure borderRadius is preserved if not explicitly set in style
|
|
143
|
+
borderRadius: style?.borderRadius ?? defaultStyles.borderRadius,
|
|
144
|
+
// Ensure backgroundColor is preserved if not explicitly set in style
|
|
145
|
+
backgroundColor: style?.backgroundColor ?? defaultStyles.backgroundColor,
|
|
146
|
+
// Get text color based on theme mode (set last to ensure it's not overridden)
|
|
147
|
+
color: getTextColor(),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div ref={ref} className={cn(className)} style={mergedStyles} {...props}>
|
|
152
|
+
{children}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
Avatar.displayName = "Avatar";
|
|
159
|
+
|
|
160
|
+
export { Avatar };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn, spacingToPx } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
const Box = React.forwardRef(
|
|
5
|
+
({ className, sx, style, children, component = "div", ...props }, ref) => {
|
|
6
|
+
const Component = component;
|
|
7
|
+
|
|
8
|
+
// Convert sx prop to style if provided
|
|
9
|
+
const sxStyles = React.useMemo(() => {
|
|
10
|
+
if (!sx) return {};
|
|
11
|
+
const sxObj = typeof sx === "function" ? sx({}) : sx;
|
|
12
|
+
const converted = { ...sxObj };
|
|
13
|
+
|
|
14
|
+
// Handle spacing shortcuts
|
|
15
|
+
if (converted.p !== undefined) {
|
|
16
|
+
converted.padding = spacingToPx(converted.p);
|
|
17
|
+
delete converted.p;
|
|
18
|
+
}
|
|
19
|
+
if (converted.px !== undefined) {
|
|
20
|
+
converted.paddingLeft = spacingToPx(converted.px);
|
|
21
|
+
converted.paddingRight = spacingToPx(converted.px);
|
|
22
|
+
delete converted.px;
|
|
23
|
+
}
|
|
24
|
+
if (converted.py !== undefined) {
|
|
25
|
+
converted.paddingTop = spacingToPx(converted.py);
|
|
26
|
+
converted.paddingBottom = spacingToPx(converted.py);
|
|
27
|
+
delete converted.py;
|
|
28
|
+
}
|
|
29
|
+
if (converted.pt !== undefined) {
|
|
30
|
+
converted.paddingTop = spacingToPx(converted.pt);
|
|
31
|
+
delete converted.pt;
|
|
32
|
+
}
|
|
33
|
+
if (converted.pb !== undefined) {
|
|
34
|
+
converted.paddingBottom = spacingToPx(converted.pb);
|
|
35
|
+
delete converted.pb;
|
|
36
|
+
}
|
|
37
|
+
if (converted.pl !== undefined) {
|
|
38
|
+
converted.paddingLeft = spacingToPx(converted.pl);
|
|
39
|
+
delete converted.pl;
|
|
40
|
+
}
|
|
41
|
+
if (converted.pr !== undefined) {
|
|
42
|
+
converted.paddingRight = spacingToPx(converted.pr);
|
|
43
|
+
delete converted.pr;
|
|
44
|
+
}
|
|
45
|
+
if (converted.m !== undefined) {
|
|
46
|
+
converted.margin = spacingToPx(converted.m);
|
|
47
|
+
delete converted.m;
|
|
48
|
+
}
|
|
49
|
+
if (converted.mx !== undefined) {
|
|
50
|
+
converted.marginLeft = spacingToPx(converted.mx);
|
|
51
|
+
converted.marginRight = spacingToPx(converted.mx);
|
|
52
|
+
delete converted.mx;
|
|
53
|
+
}
|
|
54
|
+
if (converted.my !== undefined) {
|
|
55
|
+
converted.marginTop = spacingToPx(converted.my);
|
|
56
|
+
converted.marginBottom = spacingToPx(converted.my);
|
|
57
|
+
delete converted.my;
|
|
58
|
+
}
|
|
59
|
+
if (converted.mt !== undefined) {
|
|
60
|
+
converted.marginTop = spacingToPx(converted.mt);
|
|
61
|
+
delete converted.mt;
|
|
62
|
+
}
|
|
63
|
+
if (converted.mb !== undefined) {
|
|
64
|
+
converted.marginBottom = spacingToPx(converted.mb);
|
|
65
|
+
delete converted.mb;
|
|
66
|
+
}
|
|
67
|
+
if (converted.ml !== undefined) {
|
|
68
|
+
converted.marginLeft = spacingToPx(converted.ml);
|
|
69
|
+
delete converted.ml;
|
|
70
|
+
}
|
|
71
|
+
if (converted.mr !== undefined) {
|
|
72
|
+
converted.marginRight = spacingToPx(converted.mr);
|
|
73
|
+
delete converted.mr;
|
|
74
|
+
}
|
|
75
|
+
if (converted.gap !== undefined) {
|
|
76
|
+
converted.gap = spacingToPx(converted.gap);
|
|
77
|
+
delete converted.gap;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle color shortcuts (text.primary, text.secondary, etc.)
|
|
81
|
+
if (converted.color) {
|
|
82
|
+
if (typeof converted.color === "string") {
|
|
83
|
+
if (
|
|
84
|
+
converted.color === "text.primary" ||
|
|
85
|
+
converted.color === "textPrimary"
|
|
86
|
+
) {
|
|
87
|
+
converted.color = "hsl(var(--foreground))";
|
|
88
|
+
} else if (
|
|
89
|
+
converted.color === "text.secondary" ||
|
|
90
|
+
converted.color === "textSecondary"
|
|
91
|
+
) {
|
|
92
|
+
converted.color = "hsl(var(--muted-foreground))";
|
|
93
|
+
} else if (converted.color === "primary") {
|
|
94
|
+
converted.color = "hsl(var(--primary))";
|
|
95
|
+
} else if (converted.color === "secondary") {
|
|
96
|
+
converted.color = "hsl(var(--secondary))";
|
|
97
|
+
} else if (converted.color === "error") {
|
|
98
|
+
converted.color = "hsl(var(--destructive))";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle bgcolor - convert to CSS variable if needed
|
|
104
|
+
if (converted.bgcolor) {
|
|
105
|
+
if (
|
|
106
|
+
typeof converted.bgcolor === "string" &&
|
|
107
|
+
converted.bgcolor.includes(".")
|
|
108
|
+
) {
|
|
109
|
+
// Map common theme paths to CSS variables
|
|
110
|
+
const bgcolorMap = {
|
|
111
|
+
"background.paper": "hsl(var(--card))",
|
|
112
|
+
"background.default": "hsl(var(--background))",
|
|
113
|
+
"background.card": "hsl(var(--card))",
|
|
114
|
+
};
|
|
115
|
+
converted.backgroundColor =
|
|
116
|
+
bgcolorMap[converted.bgcolor] || converted.bgcolor;
|
|
117
|
+
} else {
|
|
118
|
+
converted.backgroundColor = converted.bgcolor;
|
|
119
|
+
}
|
|
120
|
+
delete converted.bgcolor;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return converted;
|
|
124
|
+
}, [sx]);
|
|
125
|
+
|
|
126
|
+
const mergedStyle = {
|
|
127
|
+
...sxStyles,
|
|
128
|
+
...style,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Convert display, flexDirection, etc. to Tailwind classes where possible
|
|
132
|
+
const displayClass = sxStyles.display === "flex" ? "flex" : "";
|
|
133
|
+
const flexDirectionClass =
|
|
134
|
+
sxStyles.flexDirection === "column" ? "flex-col" : "";
|
|
135
|
+
const alignItemsClass =
|
|
136
|
+
sxStyles.alignItems === "center" ? "items-center" : "";
|
|
137
|
+
const justifyContentClass =
|
|
138
|
+
sxStyles.justifyContent === "space-between"
|
|
139
|
+
? "justify-between"
|
|
140
|
+
: sxStyles.justifyContent === "center"
|
|
141
|
+
? "justify-center"
|
|
142
|
+
: "";
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Component
|
|
146
|
+
ref={ref}
|
|
147
|
+
className={cn(
|
|
148
|
+
displayClass,
|
|
149
|
+
flexDirectionClass,
|
|
150
|
+
alignItemsClass,
|
|
151
|
+
justifyContentClass,
|
|
152
|
+
className
|
|
153
|
+
)}
|
|
154
|
+
style={mergedStyle}
|
|
155
|
+
{...props}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</Component>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
Box.displayName = "Box";
|
|
164
|
+
|
|
165
|
+
export { Box };
|