@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,243 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const TableContext = React.createContext({
5
+ verticalDivider: false,
6
+ });
7
+
8
+ const Table = React.forwardRef(
9
+ (
10
+ {
11
+ className,
12
+ size = "medium",
13
+ sx,
14
+ style,
15
+ border = false,
16
+ verticalDivider = false,
17
+ children,
18
+ ...props
19
+ },
20
+ ref
21
+ ) => {
22
+ const mergedSx = React.useMemo(() => {
23
+ if (!sx) return {};
24
+ return typeof sx === "function" ? sx({}) : sx;
25
+ }, [sx]);
26
+
27
+ const sizeStyles = {
28
+ small: {
29
+ fontSize: "0.875rem",
30
+ },
31
+ medium: {
32
+ fontSize: "1rem",
33
+ },
34
+ };
35
+
36
+ return (
37
+ <TableContext.Provider value={{ verticalDivider }}>
38
+ <table
39
+ ref={ref}
40
+ className={cn("w-full", className)}
41
+ style={{
42
+ borderCollapse: "separate",
43
+ borderSpacing: 0,
44
+ ...(border && {
45
+ border: "1px solid hsl(var(--border))",
46
+ }),
47
+ ...sizeStyles[size],
48
+ ...mergedSx,
49
+ ...style,
50
+ }}
51
+ {...props}
52
+ >
53
+ {children}
54
+ </table>
55
+ </TableContext.Provider>
56
+ );
57
+ }
58
+ );
59
+ Table.displayName = "Table";
60
+
61
+ const TableContainer = React.forwardRef(
62
+ ({ className, component, sx, style, children, ...props }, ref) => {
63
+ const mergedSx = React.useMemo(() => {
64
+ if (!sx) return {};
65
+ return typeof sx === "function" ? sx({}) : sx;
66
+ }, [sx]);
67
+
68
+ const Component = component || "div";
69
+
70
+ return (
71
+ <Component
72
+ ref={ref}
73
+ className={cn("relative", className)}
74
+ style={{
75
+ ...mergedSx,
76
+ ...style,
77
+ }}
78
+ {...props}
79
+ >
80
+ {children}
81
+ </Component>
82
+ );
83
+ }
84
+ );
85
+ TableContainer.displayName = "TableContainer";
86
+
87
+ const TableHead = React.forwardRef(
88
+ ({ className, sx, style, children, ...props }, ref) => {
89
+ const mergedSx = React.useMemo(() => {
90
+ if (!sx) return {};
91
+ return typeof sx === "function" ? sx({}) : sx;
92
+ }, [sx]);
93
+
94
+ return (
95
+ <thead
96
+ ref={ref}
97
+ className={cn(className)}
98
+ style={{
99
+ ...mergedSx,
100
+ ...style,
101
+ }}
102
+ {...props}
103
+ >
104
+ {children}
105
+ </thead>
106
+ );
107
+ }
108
+ );
109
+ TableHead.displayName = "TableHead";
110
+
111
+ const TableBody = React.forwardRef(
112
+ ({ className, sx, style, children, ...props }, ref) => {
113
+ const mergedSx = React.useMemo(() => {
114
+ if (!sx) return {};
115
+ return typeof sx === "function" ? sx({}) : sx;
116
+ }, [sx]);
117
+
118
+ return (
119
+ <tbody
120
+ ref={ref}
121
+ className={cn(className)}
122
+ style={{
123
+ ...mergedSx,
124
+ ...style,
125
+ }}
126
+ {...props}
127
+ >
128
+ {children}
129
+ </tbody>
130
+ );
131
+ }
132
+ );
133
+ TableBody.displayName = "TableBody";
134
+
135
+ const TableRow = React.forwardRef(
136
+ ({ className, sx, style, children, hover, selected, ...props }, ref) => {
137
+ const mergedSx = React.useMemo(() => {
138
+ if (!sx) return {};
139
+ return typeof sx === "function" ? sx({}) : sx;
140
+ }, [sx]);
141
+
142
+ return (
143
+ <tr
144
+ ref={ref}
145
+ className={cn(
146
+ hover && "hover:bg-accent",
147
+ selected && "bg-accent",
148
+ className
149
+ )}
150
+ style={{
151
+ ...mergedSx,
152
+ ...style,
153
+ }}
154
+ {...props}
155
+ >
156
+ {children}
157
+ </tr>
158
+ );
159
+ }
160
+ );
161
+ TableRow.displayName = "TableRow";
162
+
163
+ const TableCell = React.forwardRef(
164
+ (
165
+ {
166
+ className,
167
+ align = "inherit",
168
+ padding = "normal",
169
+ size = "medium",
170
+ scope,
171
+ sx,
172
+ style,
173
+ children,
174
+ colSpan,
175
+ rowSpan,
176
+ ...props
177
+ },
178
+ ref
179
+ ) => {
180
+ const mergedSx = React.useMemo(() => {
181
+ if (!sx) return {};
182
+ return typeof sx === "function" ? sx({}) : sx;
183
+ }, [sx]);
184
+
185
+ const paddingMap = {
186
+ none: "0",
187
+ checkbox: "0 0 0 4px",
188
+ normal: "16px",
189
+ small: "8px",
190
+ };
191
+
192
+ const sizeStyles = {
193
+ small: {
194
+ padding: "6px 8px",
195
+ fontSize: "0.875rem",
196
+ },
197
+ medium: {
198
+ padding: paddingMap[padding] || paddingMap.normal,
199
+ fontSize: "1rem",
200
+ },
201
+ };
202
+
203
+ const alignStyles = {
204
+ left: { textAlign: "left" },
205
+ center: { textAlign: "center" },
206
+ right: { textAlign: "right" },
207
+ inherit: {},
208
+ };
209
+
210
+ const { verticalDivider } = React.useContext(TableContext);
211
+
212
+ const Component = scope ? "th" : "td";
213
+
214
+ return (
215
+ <Component
216
+ ref={ref}
217
+ scope={scope}
218
+ colSpan={colSpan}
219
+ rowSpan={rowSpan}
220
+ className={cn(
221
+ "border-b border-border",
222
+ verticalDivider && "border-r border-border last:border-r-0",
223
+ className
224
+ )}
225
+ style={{
226
+ ...sizeStyles[size],
227
+ ...alignStyles[align],
228
+ ...(verticalDivider && {
229
+ borderRight: "1px solid hsl(var(--border))",
230
+ }),
231
+ ...mergedSx,
232
+ ...style,
233
+ }}
234
+ {...props}
235
+ >
236
+ {children}
237
+ </Component>
238
+ );
239
+ }
240
+ );
241
+ TableCell.displayName = "TableCell";
242
+
243
+ export { Table, TableContainer, TableHead, TableBody, TableRow, TableCell };
@@ -0,0 +1,363 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const Tabs = React.forwardRef(
5
+ (
6
+ {
7
+ className,
8
+ value,
9
+ onChange,
10
+ children,
11
+ centered,
12
+ variant, // "standard" for underline style, undefined/"pills"/"button" for circle button style
13
+ style,
14
+ ...props
15
+ },
16
+ ref
17
+ ) => {
18
+ const tabsContainerRef = React.useRef(null);
19
+ const indicatorRef = React.useRef(null);
20
+ const [selectedValue, setSelectedValue] = React.useState(() => {
21
+ return value !== undefined ? value : 0;
22
+ });
23
+
24
+ // Sync internal state with value prop
25
+ React.useEffect(() => {
26
+ if (value !== undefined) {
27
+ setSelectedValue(value);
28
+ }
29
+ }, [value]);
30
+
31
+ const handleTabClick = React.useCallback(
32
+ (index) => {
33
+ const newValue = index;
34
+ if (value === undefined) {
35
+ setSelectedValue(newValue);
36
+ }
37
+ if (onChange) {
38
+ onChange(null, newValue);
39
+ }
40
+ },
41
+ [value, onChange]
42
+ );
43
+
44
+ const currentValue = value !== undefined ? value : selectedValue;
45
+ const isStandardVariant = variant === "standard";
46
+
47
+ // Update indicator position when selected tab changes (only for standard variant)
48
+ React.useEffect(() => {
49
+ if (
50
+ !isStandardVariant ||
51
+ !tabsContainerRef.current ||
52
+ !indicatorRef.current
53
+ )
54
+ return;
55
+
56
+ const tabs =
57
+ tabsContainerRef.current.querySelectorAll("[data-tab-index]");
58
+ const selectedTab = tabs[currentValue];
59
+
60
+ if (selectedTab) {
61
+ const containerRect = tabsContainerRef.current.getBoundingClientRect();
62
+ const tabRect = selectedTab.getBoundingClientRect();
63
+
64
+ indicatorRef.current.style.left = `${
65
+ tabRect.left - containerRect.left
66
+ }px`;
67
+ indicatorRef.current.style.width = `${tabRect.width}px`;
68
+ }
69
+ }, [currentValue, children, isStandardVariant]);
70
+
71
+ const tabsId = React.useId();
72
+
73
+ return (
74
+ <div
75
+ ref={ref}
76
+ className={cn(
77
+ isStandardVariant ? "relative mb-4" : "flex",
78
+ !isStandardVariant && centered && "justify-center",
79
+ className
80
+ )}
81
+ style={{
82
+ minHeight: "auto",
83
+ ...style,
84
+ }}
85
+ {...props}
86
+ >
87
+ <div
88
+ ref={tabsContainerRef}
89
+ role="tablist"
90
+ aria-label="Tabs"
91
+ className={cn(
92
+ isStandardVariant
93
+ ? "flex border-b relative w-full"
94
+ : "flex gap-4 px-8 py-4"
95
+ )}
96
+ style={{
97
+ ...(isStandardVariant && { borderColor: "hsl(var(--border))" }),
98
+ ...(isStandardVariant && { display: "flex", width: "100%" }),
99
+ }}
100
+ >
101
+ {React.Children.map(children, (child, index) => {
102
+ // Check if child is a Tab component by checking displayName or type
103
+ const isTabComponent =
104
+ React.isValidElement(child) &&
105
+ (child.type === Tab || child.type?.displayName === "Tab");
106
+
107
+ if (isTabComponent) {
108
+ // Ensure both values are numbers for comparison
109
+ const isSelected = Number(currentValue) === Number(index);
110
+ return React.cloneElement(child, {
111
+ ...child.props,
112
+ selected: isSelected,
113
+ onClick: () => handleTabClick(index),
114
+ index: index,
115
+ "data-tab-index": index,
116
+ variant: isStandardVariant ? "standard" : undefined,
117
+ id: `${tabsId}-tab-${index}`,
118
+ "aria-controls": `${tabsId}-panel-${index}`,
119
+ });
120
+ }
121
+ return child;
122
+ })}
123
+ </div>
124
+ {/* Indicator line - only for standard variant */}
125
+ {isStandardVariant && (
126
+ <div
127
+ ref={indicatorRef}
128
+ style={{
129
+ position: "absolute",
130
+ bottom: 0,
131
+ height: "2px",
132
+ backgroundColor: "hsl(var(--primary))",
133
+ transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
134
+ }}
135
+ />
136
+ )}
137
+ </div>
138
+ );
139
+ }
140
+ );
141
+ Tabs.displayName = "Tabs";
142
+
143
+ const Tab = React.forwardRef(
144
+ (
145
+ {
146
+ className,
147
+ label,
148
+ selected,
149
+ onClick,
150
+ index,
151
+ style,
152
+ variant,
153
+ id,
154
+ "aria-controls": ariaControls,
155
+ ...props
156
+ },
157
+ ref
158
+ ) => {
159
+ const isStandardVariant = variant === "standard";
160
+
161
+ const {
162
+ padding,
163
+ paddingLeft,
164
+ paddingRight,
165
+ paddingTop,
166
+ paddingBottom,
167
+ margin,
168
+ marginLeft,
169
+ marginRight,
170
+ backgroundColor: styleBackgroundColor,
171
+ color: styleColor,
172
+ ...restStyle
173
+ } = style || {};
174
+
175
+ const hasPaddingInStyle =
176
+ padding !== undefined ||
177
+ paddingLeft !== undefined ||
178
+ paddingRight !== undefined ||
179
+ paddingTop !== undefined ||
180
+ paddingBottom !== undefined;
181
+
182
+ // Tab styles based on variant
183
+ if (isStandardVariant) {
184
+ // Standard variant: underline style (NEW)
185
+ const defaultPaddingClass = hasPaddingInStyle ? "" : "px-3 py-4";
186
+ const finalColor =
187
+ styleColor !== undefined
188
+ ? styleColor
189
+ : selected
190
+ ? "hsl(var(--primary))"
191
+ : "hsl(var(--muted-foreground))";
192
+
193
+ const { onClick: propsOnClick, ...restProps } = props;
194
+
195
+ return (
196
+ <button
197
+ ref={ref}
198
+ type="button"
199
+ role="tab"
200
+ id={id}
201
+ aria-selected={selected}
202
+ aria-controls={ariaControls}
203
+ tabIndex={selected ? 0 : -1}
204
+ onClick={(e) => {
205
+ if (onClick) {
206
+ onClick(e);
207
+ }
208
+ if (propsOnClick) {
209
+ propsOnClick(e);
210
+ }
211
+ }}
212
+ onKeyDown={(e) => {
213
+ if (e.key === "Enter" || e.key === " ") {
214
+ e.preventDefault();
215
+ if (onClick) {
216
+ onClick(e);
217
+ }
218
+ }
219
+ }}
220
+ className={cn(
221
+ "cursor-pointer transition-all duration-200 text-sm border-none bg-transparent focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
222
+ defaultPaddingClass,
223
+ "min-w-[72px] min-h-[48px] flex-1",
224
+ selected ? "font-semibold" : "font-medium",
225
+ className
226
+ )}
227
+ style={{
228
+ color: finalColor,
229
+ textTransform: "none",
230
+ flex: 1,
231
+ minWidth: 0,
232
+ padding: padding,
233
+ paddingLeft: paddingLeft,
234
+ paddingRight: paddingRight,
235
+ paddingTop: paddingTop,
236
+ paddingBottom: paddingBottom,
237
+ margin: margin,
238
+ marginLeft: marginLeft,
239
+ marginRight: marginRight,
240
+ ...restStyle,
241
+ }}
242
+ onMouseEnter={(e) => {
243
+ if (!selected) {
244
+ e.currentTarget.style.backgroundColor = "hsl(var(--accent))";
245
+ }
246
+ }}
247
+ onMouseLeave={(e) => {
248
+ e.currentTarget.style.backgroundColor = "transparent";
249
+ }}
250
+ {...restProps}
251
+ >
252
+ {label}
253
+ </button>
254
+ );
255
+ } else {
256
+ // Pills variant: existing circle button style (DO NOT CHANGE - backward compatibility)
257
+ const getTabColors = () => {
258
+ if (selected) {
259
+ return {
260
+ color: "hsl(var(--primary-foreground))",
261
+ backgroundColor: "hsl(var(--primary))",
262
+ };
263
+ }
264
+ // Unselected tabs all have the same style
265
+ return {
266
+ color: "hsl(var(--foreground))",
267
+ backgroundColor: "hsl(var(--accent))",
268
+ };
269
+ };
270
+
271
+ const tabColors = getTabColors();
272
+
273
+ const hasMarginInStyle =
274
+ margin !== undefined ||
275
+ marginLeft !== undefined ||
276
+ marginRight !== undefined;
277
+
278
+ // Default padding/margin classes, but allow override
279
+ const defaultPaddingClass = hasPaddingInStyle ? "" : "px-4 py-3";
280
+ const defaultMarginClass = hasMarginInStyle ? "" : "mx-2";
281
+
282
+ // Use style backgroundColor if provided, otherwise use tabColors
283
+ const finalBackgroundColor =
284
+ styleBackgroundColor !== undefined
285
+ ? styleBackgroundColor
286
+ : tabColors.backgroundColor;
287
+
288
+ const finalColor =
289
+ styleColor !== undefined ? styleColor : tabColors.color;
290
+
291
+ // Ensure restStyle doesn't override backgroundColor or color
292
+ const { backgroundColor: _, color: __, ...cleanRestStyle } = restStyle;
293
+
294
+ // Extract onClick from props to avoid override
295
+ const { onClick: propsOnClick, ...restProps } = props;
296
+
297
+ return (
298
+ <button
299
+ ref={ref}
300
+ type="button"
301
+ role="tab"
302
+ id={id}
303
+ aria-selected={selected}
304
+ aria-controls={ariaControls}
305
+ tabIndex={selected ? 0 : -1}
306
+ onClick={(e) => {
307
+ if (onClick) {
308
+ onClick(e);
309
+ }
310
+ if (propsOnClick) {
311
+ propsOnClick(e);
312
+ }
313
+ }}
314
+ onKeyDown={(e) => {
315
+ if (e.key === "Enter" || e.key === " ") {
316
+ e.preventDefault();
317
+ if (onClick) {
318
+ onClick(e);
319
+ }
320
+ }
321
+ }}
322
+ className={cn(
323
+ "cursor-pointer transition-all duration-200 min-h-[32px] min-w-[100px] rounded-2xl text-sm border-none focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
324
+ defaultPaddingClass,
325
+ defaultMarginClass,
326
+ selected ? "font-semibold" : "font-medium",
327
+ className
328
+ )}
329
+ style={{
330
+ color: finalColor,
331
+ backgroundColor: finalBackgroundColor,
332
+ ...(padding !== undefined && { padding }),
333
+ ...(paddingLeft !== undefined && { paddingLeft }),
334
+ ...(paddingRight !== undefined && { paddingRight }),
335
+ ...(paddingTop !== undefined && { paddingTop }),
336
+ ...(paddingBottom !== undefined && { paddingBottom }),
337
+ ...(margin !== undefined && { margin }),
338
+ ...(marginLeft !== undefined && { marginLeft }),
339
+ ...(marginRight !== undefined && { marginRight }),
340
+ ...cleanRestStyle,
341
+ }}
342
+ onMouseEnter={(e) => {
343
+ if (!selected) {
344
+ e.currentTarget.style.backgroundColor = "hsl(var(--accent))";
345
+ } else {
346
+ e.currentTarget.style.backgroundColor =
347
+ "hsl(var(--primary) / 0.9)";
348
+ }
349
+ }}
350
+ onMouseLeave={(e) => {
351
+ e.currentTarget.style.backgroundColor = finalBackgroundColor;
352
+ }}
353
+ {...restProps}
354
+ >
355
+ {label}
356
+ </button>
357
+ );
358
+ }
359
+ }
360
+ );
361
+ Tab.displayName = "Tab";
362
+
363
+ export { Tabs, Tab };