@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,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 };
|