@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,135 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { useMediaQuery } from "../lib/useMediaQuery";
|
|
4
|
+
|
|
5
|
+
const Grid = React.forwardRef(
|
|
6
|
+
(
|
|
7
|
+
{
|
|
8
|
+
className,
|
|
9
|
+
container = false,
|
|
10
|
+
item = false,
|
|
11
|
+
xs,
|
|
12
|
+
sm,
|
|
13
|
+
md,
|
|
14
|
+
lg,
|
|
15
|
+
xl,
|
|
16
|
+
spacing,
|
|
17
|
+
sx,
|
|
18
|
+
style,
|
|
19
|
+
children,
|
|
20
|
+
...props
|
|
21
|
+
},
|
|
22
|
+
ref
|
|
23
|
+
) => {
|
|
24
|
+
// Use media query strings directly
|
|
25
|
+
const isXs = useMediaQuery("@media (max-width:599px)");
|
|
26
|
+
const isSm = useMediaQuery(
|
|
27
|
+
"@media (min-width:600px) and (max-width:959px)"
|
|
28
|
+
);
|
|
29
|
+
const isMd = useMediaQuery(
|
|
30
|
+
"@media (min-width:960px) and (max-width:1279px)"
|
|
31
|
+
);
|
|
32
|
+
const isLg = useMediaQuery(
|
|
33
|
+
"@media (min-width:1280px) and (max-width:1919px)"
|
|
34
|
+
);
|
|
35
|
+
const isXl = useMediaQuery("@media (min-width:1920px)");
|
|
36
|
+
|
|
37
|
+
// Convert sx prop to style if provided
|
|
38
|
+
const sxStyles = React.useMemo(() => {
|
|
39
|
+
if (!sx) return {};
|
|
40
|
+
const sxObj = typeof sx === "function" ? sx({}) : sx;
|
|
41
|
+
return sxObj;
|
|
42
|
+
}, [sx]);
|
|
43
|
+
|
|
44
|
+
// Convert MUI spacing to pixels (MUI spacing unit = 8px)
|
|
45
|
+
const spacingToPx = (value) => {
|
|
46
|
+
if (typeof value === "number") {
|
|
47
|
+
return `${value * 8}px`;
|
|
48
|
+
}
|
|
49
|
+
return "0px";
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Get responsive breakpoint value
|
|
53
|
+
const getBreakpointValue = () => {
|
|
54
|
+
if (isXl && xl !== undefined) return xl;
|
|
55
|
+
if (isLg && lg !== undefined) return lg;
|
|
56
|
+
if (isMd && md !== undefined) return md;
|
|
57
|
+
if (isSm && sm !== undefined) return sm;
|
|
58
|
+
if (isXs && xs !== undefined) return xs;
|
|
59
|
+
// Fallback to xs if provided, otherwise undefined
|
|
60
|
+
return xs;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const breakpointValue = getBreakpointValue();
|
|
64
|
+
|
|
65
|
+
// Calculate flex basis and max width for item
|
|
66
|
+
const getItemStyles = () => {
|
|
67
|
+
if (!item) return {};
|
|
68
|
+
if (breakpointValue === undefined) return {};
|
|
69
|
+
|
|
70
|
+
// MUI Grid uses 12 columns
|
|
71
|
+
const columns = 12;
|
|
72
|
+
const percentage = (breakpointValue / columns) * 100;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
flexBasis: `${percentage}%`,
|
|
76
|
+
maxWidth: `${percentage}%`,
|
|
77
|
+
flexGrow: 0,
|
|
78
|
+
flexShrink: 0,
|
|
79
|
+
boxSizing: "border-box",
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Get container styles
|
|
84
|
+
// MUI Grid spacing: container has negative margins only on left and top
|
|
85
|
+
// This offsets item padding to align items with container edges on left/top
|
|
86
|
+
// Right and bottom edges maintain spacing through item padding
|
|
87
|
+
const getContainerStyles = () => {
|
|
88
|
+
if (!container) return {};
|
|
89
|
+
|
|
90
|
+
const spacingPx = spacingToPx(spacing || 0);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
display: "flex",
|
|
94
|
+
flexWrap: "wrap",
|
|
95
|
+
width: "100%",
|
|
96
|
+
marginLeft: spacing ? `-${spacingPx}` : undefined,
|
|
97
|
+
marginTop: spacing ? `-${spacingPx}` : undefined,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Get item styles with padding for spacing
|
|
102
|
+
// MUI Grid spacing: items have paddingLeft and paddingTop
|
|
103
|
+
// This creates spacing between items and from left/top edges
|
|
104
|
+
// Right and bottom spacing is maintained by the last items' padding
|
|
105
|
+
const getItemPaddingStyles = () => {
|
|
106
|
+
if (!item) return {};
|
|
107
|
+
if (!spacing) return {};
|
|
108
|
+
|
|
109
|
+
const spacingPx = spacingToPx(spacing);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
paddingLeft: spacingPx,
|
|
113
|
+
paddingTop: spacingPx,
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const mergedStyle = {
|
|
118
|
+
...getContainerStyles(),
|
|
119
|
+
...getItemStyles(),
|
|
120
|
+
...getItemPaddingStyles(),
|
|
121
|
+
...sxStyles,
|
|
122
|
+
...style,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div ref={ref} className={cn(className)} style={mergedStyle} {...props}>
|
|
127
|
+
{children}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
Grid.displayName = "Grid";
|
|
134
|
+
|
|
135
|
+
export { Grid };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
const iconButtonVariants = cva(
|
|
6
|
+
"inline-flex items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "hover:bg-accent hover:text-accent-foreground",
|
|
11
|
+
destructive: "hover:bg-destructive hover:text-destructive-foreground",
|
|
12
|
+
outline:
|
|
13
|
+
"border border-input hover:bg-accent hover:text-accent-foreground",
|
|
14
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
default: "h-10 w-10",
|
|
18
|
+
sm: "h-8 w-8",
|
|
19
|
+
lg: "h-12 w-12",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
size: "default",
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const IconButton = React.forwardRef(
|
|
30
|
+
(
|
|
31
|
+
{
|
|
32
|
+
className,
|
|
33
|
+
variant,
|
|
34
|
+
size = "default",
|
|
35
|
+
style,
|
|
36
|
+
color,
|
|
37
|
+
"aria-label": ariaLabel,
|
|
38
|
+
children,
|
|
39
|
+
...props
|
|
40
|
+
},
|
|
41
|
+
ref
|
|
42
|
+
) => {
|
|
43
|
+
// Get color class based on color prop
|
|
44
|
+
const getColorClass = () => {
|
|
45
|
+
if (color === "primary") return "text-primary";
|
|
46
|
+
if (color === "secondary") return "text-secondary-foreground";
|
|
47
|
+
if (color === "error") return "text-destructive";
|
|
48
|
+
if (color === "inherit") return "text-inherit";
|
|
49
|
+
return "text-muted-foreground"; // Default
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Get icon size based on button size
|
|
53
|
+
const getIconSize = () => {
|
|
54
|
+
if (size === "sm") return 16;
|
|
55
|
+
if (size === "lg") return 24;
|
|
56
|
+
return 20; // default
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Clone children to apply size if it's a React element (like lucide-react icons)
|
|
60
|
+
const childrenWithSize = React.useMemo(() => {
|
|
61
|
+
if (React.isValidElement(children) && typeof children.type !== "string") {
|
|
62
|
+
// Check if the element has a size prop (like lucide-react icons)
|
|
63
|
+
if (children.props.size === undefined) {
|
|
64
|
+
return React.cloneElement(children, {
|
|
65
|
+
size: getIconSize(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return children;
|
|
70
|
+
}, [children, size]);
|
|
71
|
+
|
|
72
|
+
// Warn if no aria-label is provided (accessibility best practice)
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
if (!ariaLabel && !props["aria-labelledby"]) {
|
|
75
|
+
console.warn(
|
|
76
|
+
"IconButton: Missing aria-label or aria-labelledby. Icon-only buttons should have accessible labels."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}, [ariaLabel, props["aria-labelledby"]]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<button
|
|
83
|
+
className={cn(
|
|
84
|
+
iconButtonVariants({ variant, size }),
|
|
85
|
+
"rounded-2xl",
|
|
86
|
+
getColorClass(),
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
ref={ref}
|
|
90
|
+
style={style}
|
|
91
|
+
aria-label={ariaLabel}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
{childrenWithSize}
|
|
95
|
+
</button>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
IconButton.displayName = "IconButton";
|
|
100
|
+
|
|
101
|
+
export { IconButton, iconButtonVariants };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Card,
|
|
3
|
+
CardHeader,
|
|
4
|
+
CardTitle,
|
|
5
|
+
CardDescription,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardFooter,
|
|
8
|
+
CardActionArea,
|
|
9
|
+
} from "./card";
|
|
10
|
+
export { Button } from "./button";
|
|
11
|
+
// buttonVariants is exported separately if needed, but not as a component
|
|
12
|
+
export {
|
|
13
|
+
Dialog,
|
|
14
|
+
DialogContent,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogFooter,
|
|
17
|
+
DialogTitle,
|
|
18
|
+
DialogDescription,
|
|
19
|
+
DialogActions,
|
|
20
|
+
} from "./dialog";
|
|
21
|
+
export { IconButton } from "./icon-button";
|
|
22
|
+
// iconButtonVariants is exported separately if needed, but not as a component
|
|
23
|
+
export { Typography } from "./typography";
|
|
24
|
+
export { Chip } from "./chip";
|
|
25
|
+
export { TextField } from "./text-field";
|
|
26
|
+
export { Select, FormControl } from "./select";
|
|
27
|
+
export { Checkbox } from "./checkbox";
|
|
28
|
+
export {
|
|
29
|
+
List,
|
|
30
|
+
ListItem,
|
|
31
|
+
ListItemIcon,
|
|
32
|
+
ListItemText,
|
|
33
|
+
ListItemButton,
|
|
34
|
+
ListItemAvatar,
|
|
35
|
+
} from "./list";
|
|
36
|
+
export { Accordion, AccordionSummary, AccordionDetails } from "./accordion";
|
|
37
|
+
export { Alert } from "./alert";
|
|
38
|
+
export { Tabs, Tab } from "./tabs";
|
|
39
|
+
export { Tooltip } from "./tooltip";
|
|
40
|
+
export { Autocomplete } from "./autocomplete";
|
|
41
|
+
export { DatePicker } from "./date-picker";
|
|
42
|
+
export { Drawer } from "./drawer";
|
|
43
|
+
export { FormControlLabel } from "./form-control-label";
|
|
44
|
+
export { FormLabel } from "./form-label";
|
|
45
|
+
export { FormGroup } from "./form-group";
|
|
46
|
+
export { Radio } from "./radio";
|
|
47
|
+
export { RadioGroup } from "./radio-group";
|
|
48
|
+
export { Collapse } from "./collapse";
|
|
49
|
+
export { DialogContentText } from "./dialog-content-text";
|
|
50
|
+
export { Container } from "./container";
|
|
51
|
+
export { Divider } from "./divider";
|
|
52
|
+
export { Avatar } from "./avatar";
|
|
53
|
+
export { Stack } from "./stack";
|
|
54
|
+
export { InputAdornment } from "./input-adornment";
|
|
55
|
+
export { Paper } from "./paper";
|
|
56
|
+
export { AppBar } from "./app-bar";
|
|
57
|
+
export { Toolbar } from "./toolbar";
|
|
58
|
+
export {
|
|
59
|
+
Menu,
|
|
60
|
+
MenuItem,
|
|
61
|
+
MenuItemIcon,
|
|
62
|
+
MenuItemText,
|
|
63
|
+
} from "./menu";
|
|
64
|
+
// Export Select's MenuItem as SelectMenuItem to avoid naming conflict
|
|
65
|
+
export { MenuItem as SelectMenuItem } from "./select";
|
|
66
|
+
export {
|
|
67
|
+
Table,
|
|
68
|
+
TableContainer,
|
|
69
|
+
TableHead,
|
|
70
|
+
TableBody,
|
|
71
|
+
TableRow,
|
|
72
|
+
TableCell,
|
|
73
|
+
} from "./table";
|
|
74
|
+
export { ToggleButton, ToggleButtonGroup } from "./toggle-button";
|
|
75
|
+
export { Grid } from "./grid";
|
|
76
|
+
export { InputLabel } from "./input-label";
|
|
77
|
+
export { Slider } from "./slider";
|
|
78
|
+
export { Box } from "./box";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
const InputAdornment = React.forwardRef(
|
|
5
|
+
(
|
|
6
|
+
{
|
|
7
|
+
className,
|
|
8
|
+
position = "end",
|
|
9
|
+
disablePointerEvents = false,
|
|
10
|
+
disableTypography = false,
|
|
11
|
+
children,
|
|
12
|
+
...props
|
|
13
|
+
},
|
|
14
|
+
ref
|
|
15
|
+
) => {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
ref={ref}
|
|
19
|
+
className={cn(
|
|
20
|
+
"flex items-center",
|
|
21
|
+
position === "start" ? "mr-2" : "ml-2",
|
|
22
|
+
disablePointerEvents && "pointer-events-none",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
style={{
|
|
26
|
+
pointerEvents: disablePointerEvents ? "none" : "auto",
|
|
27
|
+
cursor: disablePointerEvents ? "default" : "inherit",
|
|
28
|
+
}}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{disableTypography ? (
|
|
32
|
+
children
|
|
33
|
+
) : (
|
|
34
|
+
<span className="text-sm text-muted-foreground">{children}</span>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
InputAdornment.displayName = "InputAdornment";
|
|
42
|
+
|
|
43
|
+
export { InputAdornment };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
const InputLabel = React.forwardRef(
|
|
5
|
+
(
|
|
6
|
+
{
|
|
7
|
+
children,
|
|
8
|
+
className,
|
|
9
|
+
required = false,
|
|
10
|
+
disabled = false,
|
|
11
|
+
error = false,
|
|
12
|
+
focused = false,
|
|
13
|
+
shrink = false,
|
|
14
|
+
sx,
|
|
15
|
+
style,
|
|
16
|
+
...props
|
|
17
|
+
},
|
|
18
|
+
ref
|
|
19
|
+
) => {
|
|
20
|
+
// Convert sx prop to style if provided
|
|
21
|
+
const sxStyles = React.useMemo(() => {
|
|
22
|
+
if (!sx) return {};
|
|
23
|
+
const sxObj = typeof sx === "function" ? sx({}) : sx;
|
|
24
|
+
return sxObj;
|
|
25
|
+
}, [sx]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<label
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(
|
|
31
|
+
"block text-sm font-medium mb-2 text-foreground",
|
|
32
|
+
error
|
|
33
|
+
? "text-destructive"
|
|
34
|
+
: focused
|
|
35
|
+
? "text-primary"
|
|
36
|
+
: "text-foreground",
|
|
37
|
+
disabled && "opacity-50",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
style={{
|
|
41
|
+
...sxStyles,
|
|
42
|
+
...style,
|
|
43
|
+
}}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
{required && <span className="ml-1 text-destructive">*</span>}
|
|
48
|
+
</label>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
InputLabel.displayName = "InputLabel";
|
|
54
|
+
|
|
55
|
+
export { InputLabel };
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Typography } from "./typography";
|
|
4
|
+
|
|
5
|
+
const List = React.forwardRef(
|
|
6
|
+
({ className, style, children, ...props }, ref) => {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn("w-full", className)}
|
|
11
|
+
style={{
|
|
12
|
+
padding: 0,
|
|
13
|
+
margin: 0,
|
|
14
|
+
...style,
|
|
15
|
+
}}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
List.displayName = "List";
|
|
24
|
+
|
|
25
|
+
const ListItem = React.forwardRef(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
className,
|
|
29
|
+
style,
|
|
30
|
+
alignItems = "flex-start",
|
|
31
|
+
disablePadding,
|
|
32
|
+
dense,
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
},
|
|
36
|
+
ref
|
|
37
|
+
) => {
|
|
38
|
+
// Extract padding from style to allow override
|
|
39
|
+
const {
|
|
40
|
+
padding,
|
|
41
|
+
paddingTop,
|
|
42
|
+
paddingBottom,
|
|
43
|
+
paddingLeft,
|
|
44
|
+
paddingRight,
|
|
45
|
+
...restStyle
|
|
46
|
+
} = style || {};
|
|
47
|
+
|
|
48
|
+
const hasPaddingInStyle =
|
|
49
|
+
padding !== undefined ||
|
|
50
|
+
paddingTop !== undefined ||
|
|
51
|
+
paddingBottom !== undefined ||
|
|
52
|
+
paddingLeft !== undefined ||
|
|
53
|
+
paddingRight !== undefined;
|
|
54
|
+
|
|
55
|
+
// If disablePadding is true, don't apply default padding
|
|
56
|
+
// If dense is true, use smaller padding
|
|
57
|
+
let defaultPaddingClass = "";
|
|
58
|
+
if (!disablePadding && !hasPaddingInStyle) {
|
|
59
|
+
defaultPaddingClass = dense ? "py-2 px-8" : "py-4 px-8";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
ref={ref}
|
|
65
|
+
className={cn("flex", defaultPaddingClass, className)}
|
|
66
|
+
style={{
|
|
67
|
+
alignItems: alignItems,
|
|
68
|
+
padding: disablePadding ? 0 : padding,
|
|
69
|
+
paddingTop: disablePadding ? 0 : paddingTop,
|
|
70
|
+
paddingBottom: disablePadding ? 0 : paddingBottom,
|
|
71
|
+
paddingLeft: disablePadding ? 0 : paddingLeft,
|
|
72
|
+
paddingRight: disablePadding ? 0 : paddingRight,
|
|
73
|
+
...restStyle,
|
|
74
|
+
}}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
ListItem.displayName = "ListItem";
|
|
83
|
+
|
|
84
|
+
const ListItemIcon = React.forwardRef(
|
|
85
|
+
({ className, style, children, ...props }, ref) => {
|
|
86
|
+
// Extract margin and color from style to allow override
|
|
87
|
+
const { marginRight, minWidth, color, ...restStyle } = style || {};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
ref={ref}
|
|
92
|
+
className={cn("flex items-center justify-center", className)}
|
|
93
|
+
style={{
|
|
94
|
+
minWidth: minWidth !== undefined ? minWidth : "auto",
|
|
95
|
+
marginRight: marginRight !== undefined ? marginRight : "1rem",
|
|
96
|
+
...(color !== undefined && { color }),
|
|
97
|
+
...restStyle,
|
|
98
|
+
}}
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
ListItemIcon.displayName = "ListItemIcon";
|
|
107
|
+
|
|
108
|
+
const ListItemText = React.forwardRef(
|
|
109
|
+
({ className, style, primary, secondary, children, ...props }, ref) => {
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
ref={ref}
|
|
113
|
+
className={cn("flex flex-col", className)}
|
|
114
|
+
style={{
|
|
115
|
+
flex: 1,
|
|
116
|
+
margin: 0,
|
|
117
|
+
...style,
|
|
118
|
+
}}
|
|
119
|
+
{...props}
|
|
120
|
+
>
|
|
121
|
+
{primary && (
|
|
122
|
+
<div className={cn(secondary && "mb-2")}>
|
|
123
|
+
{React.isValidElement(primary) ? (
|
|
124
|
+
primary
|
|
125
|
+
) : (
|
|
126
|
+
<Typography variant="body1" color="default">
|
|
127
|
+
{primary}
|
|
128
|
+
</Typography>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
{secondary && (
|
|
133
|
+
<div>
|
|
134
|
+
{React.isValidElement(secondary) ? (
|
|
135
|
+
secondary
|
|
136
|
+
) : (
|
|
137
|
+
<Typography variant="body2" color="muted">
|
|
138
|
+
{secondary}
|
|
139
|
+
</Typography>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
{children}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
ListItemText.displayName = "ListItemText";
|
|
149
|
+
|
|
150
|
+
const ListItemButton = React.forwardRef(
|
|
151
|
+
(
|
|
152
|
+
{ className, style, selected, onClick, disabled, children, dense, disablePadding, ...props },
|
|
153
|
+
ref
|
|
154
|
+
) => {
|
|
155
|
+
const [hovered, setHovered] = React.useState(false);
|
|
156
|
+
|
|
157
|
+
const handleKeyDown = (e) => {
|
|
158
|
+
if (disabled) return;
|
|
159
|
+
// Support Enter and Space keys for keyboard navigation
|
|
160
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
if (onClick) {
|
|
163
|
+
onClick(e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Calculate padding for ListItemButton
|
|
169
|
+
let paddingClass = "";
|
|
170
|
+
if (!disablePadding) {
|
|
171
|
+
paddingClass = dense ? "py-2 px-8" : "py-4 px-8";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div
|
|
176
|
+
ref={ref}
|
|
177
|
+
role="button"
|
|
178
|
+
tabIndex={disabled ? -1 : 0}
|
|
179
|
+
aria-disabled={disabled}
|
|
180
|
+
aria-selected={selected}
|
|
181
|
+
className={cn(
|
|
182
|
+
"flex cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
183
|
+
paddingClass,
|
|
184
|
+
selected
|
|
185
|
+
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
186
|
+
: hovered
|
|
187
|
+
? "bg-accent text-accent-foreground"
|
|
188
|
+
: "hover:bg-accent hover:text-accent-foreground",
|
|
189
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
190
|
+
className
|
|
191
|
+
)}
|
|
192
|
+
style={{
|
|
193
|
+
alignItems: "center",
|
|
194
|
+
...style,
|
|
195
|
+
}}
|
|
196
|
+
onClick={disabled ? undefined : onClick}
|
|
197
|
+
onKeyDown={handleKeyDown}
|
|
198
|
+
onMouseEnter={() => !disabled && setHovered(true)}
|
|
199
|
+
onMouseLeave={() => setHovered(false)}
|
|
200
|
+
{...props}
|
|
201
|
+
>
|
|
202
|
+
{children}
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
ListItemButton.displayName = "ListItemButton";
|
|
208
|
+
|
|
209
|
+
const ListItemAvatar = React.forwardRef(
|
|
210
|
+
({ className, style, children, ...props }, ref) => {
|
|
211
|
+
// Extract margin and minWidth from style to allow override
|
|
212
|
+
const { marginRight, minWidth, ...restStyle } = style || {};
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div
|
|
216
|
+
ref={ref}
|
|
217
|
+
className={cn("flex items-center justify-center", className)}
|
|
218
|
+
style={{
|
|
219
|
+
minWidth: minWidth !== undefined ? minWidth : "40px",
|
|
220
|
+
marginRight: marginRight !== undefined ? marginRight : "1rem",
|
|
221
|
+
...restStyle,
|
|
222
|
+
}}
|
|
223
|
+
{...props}
|
|
224
|
+
>
|
|
225
|
+
{children}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
ListItemAvatar.displayName = "ListItemAvatar";
|
|
231
|
+
|
|
232
|
+
export {
|
|
233
|
+
List,
|
|
234
|
+
ListItem,
|
|
235
|
+
ListItemIcon,
|
|
236
|
+
ListItemText,
|
|
237
|
+
ListItemButton,
|
|
238
|
+
ListItemAvatar,
|
|
239
|
+
};
|