@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,67 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const Container = React.forwardRef(
5
+ (
6
+ {
7
+ className,
8
+ style,
9
+ maxWidth = "lg",
10
+ fixed = false,
11
+ disableGutters = false,
12
+ children,
13
+ ...props
14
+ },
15
+ ref
16
+ ) => {
17
+ // Map maxWidth to actual width values
18
+ const getMaxWidth = () => {
19
+ if (maxWidth === false) {
20
+ return "none";
21
+ }
22
+ const maxWidthMap = {
23
+ xs: "444px",
24
+ sm: "600px",
25
+ md: "900px",
26
+ lg: "1200px",
27
+ xl: "1536px",
28
+ };
29
+ return maxWidthMap[maxWidth] || maxWidthMap.lg;
30
+ };
31
+
32
+ const maxWidthValue = getMaxWidth();
33
+
34
+ // Get padding based on disableGutters
35
+ const getPadding = () => {
36
+ if (disableGutters) {
37
+ return { paddingLeft: 0, paddingRight: 0 };
38
+ }
39
+ // Default MUI Container padding
40
+ return {
41
+ paddingLeft: "16px",
42
+ paddingRight: "16px",
43
+ };
44
+ };
45
+
46
+ return (
47
+ <div
48
+ ref={ref}
49
+ className={cn("mx-auto", className)}
50
+ style={{
51
+ width: "100%",
52
+ maxWidth: maxWidthValue,
53
+ ...(fixed && { width: maxWidthValue }),
54
+ ...getPadding(),
55
+ ...style,
56
+ }}
57
+ {...props}
58
+ >
59
+ {children}
60
+ </div>
61
+ );
62
+ }
63
+ );
64
+
65
+ Container.displayName = "Container";
66
+
67
+ export { Container };
@@ -0,0 +1,528 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { TextField } from "./text-field";
4
+ import * as dayjsModule from "dayjs";
5
+ const dayjs = dayjsModule.default || dayjsModule;
6
+
7
+ const DatePicker = React.forwardRef(
8
+ (
9
+ {
10
+ value,
11
+ onChange,
12
+ clearable = false,
13
+ slotProps,
14
+ className,
15
+ style,
16
+ onOpenChange,
17
+ ...props
18
+ },
19
+ ref
20
+ ) => {
21
+ const [open, setOpen] = React.useState(false);
22
+ const [selectedDate, setSelectedDate] = React.useState(
23
+ value ? dayjs(value) : null
24
+ );
25
+ const calendarRef = React.useRef(null);
26
+ const inputRef = React.useRef(null);
27
+ const yearPickerRef = React.useRef(null);
28
+ const currentYearButtonRef = React.useRef(null);
29
+
30
+ React.useImperativeHandle(ref, () => inputRef.current);
31
+
32
+ React.useEffect(() => {
33
+ setSelectedDate(value ? dayjs(value) : null);
34
+ }, [value]);
35
+
36
+ const handleDateChange = (newDate) => {
37
+ setSelectedDate(newDate);
38
+ if (onChange) {
39
+ onChange(newDate);
40
+ }
41
+ setOpen(false);
42
+ if (onOpenChange) {
43
+ onOpenChange(false);
44
+ }
45
+ };
46
+
47
+ const handleClear = (e) => {
48
+ e.stopPropagation();
49
+ setSelectedDate(null);
50
+ if (onChange) {
51
+ onChange(null);
52
+ }
53
+ };
54
+
55
+ const handleClickOutside = (event) => {
56
+ if (
57
+ calendarRef.current &&
58
+ !calendarRef.current.contains(event.target) &&
59
+ inputRef.current &&
60
+ !inputRef.current.contains(event.target)
61
+ ) {
62
+ setOpen(false);
63
+ setShowYearPicker(false);
64
+ if (onOpenChange) {
65
+ onOpenChange(false);
66
+ }
67
+ }
68
+ };
69
+
70
+ React.useEffect(() => {
71
+ if (open) {
72
+ document.addEventListener("mousedown", handleClickOutside);
73
+ return () => {
74
+ document.removeEventListener("mousedown", handleClickOutside);
75
+ };
76
+ }
77
+ }, [open]);
78
+
79
+ const textFieldProps = slotProps?.textField || {};
80
+ const displayValue = selectedDate ? selectedDate.format("YYYY-MM-DD") : "";
81
+ const [viewMode, setViewMode] = React.useState("month"); // "month" or "year"
82
+ const [showYearPicker, setShowYearPicker] = React.useState(false);
83
+
84
+ // Scroll to current year when year picker opens
85
+ React.useEffect(() => {
86
+ if (
87
+ showYearPicker &&
88
+ currentYearButtonRef.current &&
89
+ yearPickerRef.current
90
+ ) {
91
+ // Use setTimeout to ensure DOM is rendered
92
+ setTimeout(() => {
93
+ const button = currentYearButtonRef.current;
94
+ const container = yearPickerRef.current;
95
+ if (button && container) {
96
+ const buttonTop = button.offsetTop;
97
+ const containerHeight = container.clientHeight;
98
+ const scrollTop =
99
+ buttonTop - containerHeight / 2 + button.clientHeight / 2;
100
+ container.scrollTop = scrollTop;
101
+ }
102
+ }, 0);
103
+ }
104
+ }, [showYearPicker]);
105
+
106
+ // Generate calendar days
107
+ const getCalendarDays = () => {
108
+ const startOfMonth = selectedDate
109
+ ? selectedDate.startOf("month")
110
+ : dayjs().startOf("month");
111
+ const endOfMonth = selectedDate
112
+ ? selectedDate.endOf("month")
113
+ : dayjs().endOf("month");
114
+ const startDate = startOfMonth.startOf("week");
115
+ const endDate = endOfMonth.endOf("week");
116
+ const days = [];
117
+ let currentDate = startDate;
118
+
119
+ while (currentDate.isBefore(endDate) || currentDate.isSame(endDate)) {
120
+ days.push(currentDate);
121
+ currentDate = currentDate.add(1, "day");
122
+ }
123
+
124
+ return days;
125
+ };
126
+
127
+ const calendarDays = getCalendarDays();
128
+ const currentMonth = selectedDate ? selectedDate.month() : dayjs().month();
129
+ const currentYear = selectedDate ? selectedDate.year() : dayjs().year();
130
+
131
+ const handlePrevMonth = () => {
132
+ const newDate = selectedDate
133
+ ? selectedDate.subtract(1, "month")
134
+ : dayjs().subtract(1, "month");
135
+ setSelectedDate(newDate);
136
+ };
137
+
138
+ const handleNextMonth = () => {
139
+ const newDate = selectedDate
140
+ ? selectedDate.add(1, "month")
141
+ : dayjs().add(1, "month");
142
+ setSelectedDate(newDate);
143
+ };
144
+
145
+ const handleDayClick = (day) => {
146
+ handleDateChange(day);
147
+ };
148
+
149
+ return (
150
+ <div
151
+ ref={calendarRef}
152
+ className={cn("relative", className)}
153
+ style={style}
154
+ >
155
+ <TextField
156
+ {...textFieldProps}
157
+ ref={inputRef}
158
+ value={displayValue || ""}
159
+ onClick={() => {
160
+ const newOpen = !open;
161
+ setOpen(newOpen);
162
+ if (onOpenChange) {
163
+ onOpenChange(newOpen);
164
+ }
165
+ }}
166
+ readOnly
167
+ placeholder={textFieldProps.placeholder || "MM/DD/YYYY"}
168
+ style={{
169
+ ...textFieldProps.style,
170
+ fontSize: "0.875rem",
171
+ height: textFieldProps.style?.height || "40px",
172
+ }}
173
+ InputProps={{
174
+ ...textFieldProps.InputProps,
175
+ style: {
176
+ ...textFieldProps.InputProps?.style,
177
+ backgroundColor:
178
+ textFieldProps.InputProps?.style?.backgroundColor,
179
+ },
180
+ endAdornment: (
181
+ <div className="flex items-center gap-1">
182
+ {clearable && selectedDate && (
183
+ <div
184
+ className="cursor-pointer"
185
+ onClick={handleClear}
186
+ style={{
187
+ display: "flex",
188
+ alignItems: "center",
189
+ padding: "4px",
190
+ }}
191
+ >
192
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
193
+ <path
194
+ d="M12 4L4 12M4 4L12 12"
195
+ stroke="hsl(var(--muted-foreground))"
196
+ strokeWidth="2"
197
+ strokeLinecap="round"
198
+ />
199
+ </svg>
200
+ </div>
201
+ )}
202
+ <div
203
+ className="cursor-pointer"
204
+ style={{ padding: "4px" }}
205
+ onClick={(e) => {
206
+ e.stopPropagation();
207
+ const newOpen = !open;
208
+ setOpen(newOpen);
209
+ if (onOpenChange) {
210
+ onOpenChange(newOpen);
211
+ }
212
+ }}
213
+ >
214
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
215
+ <path
216
+ d="M6 2V4M14 2V4M3 8H17M4 4H16C16.5523 4 17 4.44772 17 5V16C17 16.5523 16.5523 17 16 17H4C3.44772 17 3 16.5523 3 16V5C3 4.44772 3.44772 4 4 4Z"
217
+ stroke="hsl(var(--muted-foreground))"
218
+ strokeWidth="1.5"
219
+ strokeLinecap="round"
220
+ strokeLinejoin="round"
221
+ />
222
+ </svg>
223
+ </div>
224
+ </div>
225
+ ),
226
+ }}
227
+ />
228
+ {open && (
229
+ <div
230
+ className="absolute z-50 mt-1 rounded-2xl border shadow-lg p-4"
231
+ style={{
232
+ backgroundColor: "hsl(var(--card))",
233
+ borderColor: "hsl(var(--border))",
234
+ top: "100%",
235
+ minWidth: "300px",
236
+ }}
237
+ >
238
+ {/* Calendar Header */}
239
+ <div
240
+ className="flex items-center justify-between mb-4"
241
+ style={{
242
+ color: "hsl(var(--foreground))",
243
+ }}
244
+ >
245
+ <button
246
+ type="button"
247
+ onClick={() => {
248
+ if (viewMode === "year") {
249
+ setSelectedDate(
250
+ selectedDate
251
+ ? selectedDate.subtract(10, "year")
252
+ : dayjs().subtract(10, "year")
253
+ );
254
+ } else {
255
+ handlePrevMonth();
256
+ }
257
+ }}
258
+ className="p-1 rounded-lg hover:bg-action-hover transition-colors"
259
+ style={{
260
+ color: "hsl(var(--foreground))",
261
+ }}
262
+ >
263
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
264
+ <path
265
+ d="M12 15L7 10L12 5"
266
+ stroke="currentColor"
267
+ strokeWidth="2"
268
+ strokeLinecap="round"
269
+ strokeLinejoin="round"
270
+ />
271
+ </svg>
272
+ </button>
273
+ <div className="relative">
274
+ <button
275
+ type="button"
276
+ onClick={() => {
277
+ if (viewMode === "month") {
278
+ setShowYearPicker(!showYearPicker);
279
+ } else {
280
+ setViewMode("month");
281
+ }
282
+ }}
283
+ className="font-semibold px-2 py-1 rounded-lg hover:bg-action-hover transition-colors flex items-center gap-1 text-foreground"
284
+ >
285
+ {viewMode === "month"
286
+ ? `${dayjs()
287
+ .month(currentMonth)
288
+ .format("MMMM")} ${currentYear}`
289
+ : `${currentYear}`}
290
+ {viewMode === "month" && (
291
+ <svg
292
+ width="16"
293
+ height="16"
294
+ viewBox="0 0 16 16"
295
+ fill="none"
296
+ style={{
297
+ transform: showYearPicker
298
+ ? "rotate(180deg)"
299
+ : "rotate(0deg)",
300
+ transition: "transform 0.2s",
301
+ }}
302
+ >
303
+ <path
304
+ d="M4 6L8 10L12 6"
305
+ stroke="currentColor"
306
+ strokeWidth="2"
307
+ strokeLinecap="round"
308
+ strokeLinejoin="round"
309
+ />
310
+ </svg>
311
+ )}
312
+ </button>
313
+ {showYearPicker && viewMode === "month" && (
314
+ <div
315
+ ref={yearPickerRef}
316
+ className="absolute z-10 mt-1 rounded-lg border shadow-lg max-h-48 overflow-auto"
317
+ style={{
318
+ backgroundColor: "hsl(var(--card))",
319
+ borderColor: "hsl(var(--border))",
320
+ top: "100%",
321
+ left: 0,
322
+ minWidth: "120px",
323
+ }}
324
+ >
325
+ {Array.from({ length: 201 }, (_, i) => {
326
+ // Generate years from currentYear - 100 to currentYear + 100
327
+ const currentYearValue = dayjs().year();
328
+ const year = currentYearValue - 100 + i;
329
+ const isCurrentYear = year === currentYear;
330
+ return (
331
+ <button
332
+ key={year}
333
+ ref={isCurrentYear ? currentYearButtonRef : null}
334
+ type="button"
335
+ onClick={() => {
336
+ const newDate = selectedDate
337
+ ? selectedDate.year(year)
338
+ : dayjs().year(year).month(currentMonth);
339
+ setSelectedDate(newDate);
340
+ setShowYearPicker(false);
341
+ }}
342
+ className="w-full px-3 py-1.5 text-sm text-left hover:bg-action-hover transition-colors"
343
+ style={{
344
+ backgroundColor: isCurrentYear
345
+ ? "hsl(var(--accent))" || "rgba(0, 0, 0, 0.08)"
346
+ : "transparent",
347
+ color: "hsl(var(--foreground))",
348
+ }}
349
+ onMouseEnter={(e) => {
350
+ if (!isCurrentYear) {
351
+ e.currentTarget.style.backgroundColor =
352
+ "hsl(var(--accent))" || "rgba(0, 0, 0, 0.04)";
353
+ }
354
+ }}
355
+ onMouseLeave={(e) => {
356
+ e.currentTarget.style.backgroundColor =
357
+ isCurrentYear
358
+ ? "hsl(var(--accent))" || "rgba(0, 0, 0, 0.08)"
359
+ : "transparent";
360
+ }}
361
+ >
362
+ {year}
363
+ </button>
364
+ );
365
+ })}
366
+ </div>
367
+ )}
368
+ </div>
369
+ <button
370
+ type="button"
371
+ onClick={() => {
372
+ if (viewMode === "year") {
373
+ setSelectedDate(
374
+ selectedDate
375
+ ? selectedDate.add(10, "year")
376
+ : dayjs().add(10, "year")
377
+ );
378
+ } else {
379
+ handleNextMonth();
380
+ }
381
+ }}
382
+ className="p-1 rounded-lg hover:bg-action-hover transition-colors"
383
+ style={{
384
+ color: "hsl(var(--foreground))",
385
+ }}
386
+ >
387
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
388
+ <path
389
+ d="M8 5L13 10L8 15"
390
+ stroke="currentColor"
391
+ strokeWidth="2"
392
+ strokeLinecap="round"
393
+ strokeLinejoin="round"
394
+ />
395
+ </svg>
396
+ </button>
397
+ </div>
398
+
399
+ {viewMode === "month" ? (
400
+ <>
401
+ {/* Calendar Grid */}
402
+ <div className="grid grid-cols-7 gap-1 mb-2">
403
+ {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map(
404
+ (day) => (
405
+ <div
406
+ key={day}
407
+ className="text-center text-xs font-medium py-1 text-muted-foreground"
408
+ >
409
+ {day}
410
+ </div>
411
+ )
412
+ )}
413
+ </div>
414
+ <div className="grid grid-cols-7 gap-1">
415
+ {calendarDays.map((day, index) => {
416
+ const isCurrentMonth = day.month() === currentMonth;
417
+ const isToday = day.isSame(dayjs(), "day");
418
+ const isSelected =
419
+ selectedDate && day.isSame(selectedDate, "day");
420
+
421
+ return (
422
+ <button
423
+ key={index}
424
+ type="button"
425
+ onClick={() => handleDayClick(day)}
426
+ className={cn(
427
+ "aspect-square rounded-lg text-sm transition-colors",
428
+ !isCurrentMonth && "opacity-30",
429
+ isSelected && "font-semibold"
430
+ )}
431
+ style={{
432
+ backgroundColor: isSelected
433
+ ? "hsl(var(--primary))"
434
+ : isToday
435
+ ? "hsl(var(--accent))" || "rgba(0, 0, 0, 0.04)"
436
+ : "transparent",
437
+ color: isSelected
438
+ ? "hsl(var(--primary-foreground))"
439
+ : "hsl(var(--foreground))",
440
+ border:
441
+ isToday && !isSelected
442
+ ? "1px solid hsl(var(--primary))"
443
+ : "none",
444
+ }}
445
+ onMouseEnter={(e) => {
446
+ if (!isSelected) {
447
+ e.currentTarget.style.backgroundColor =
448
+ "hsl(var(--accent))";
449
+ }
450
+ }}
451
+ onMouseLeave={(e) => {
452
+ e.currentTarget.style.backgroundColor = isSelected
453
+ ? "hsl(var(--primary))"
454
+ : isToday
455
+ ? "hsl(var(--accent))" || "rgba(0, 0, 0, 0.04)"
456
+ : "transparent";
457
+ }}
458
+ >
459
+ {day.date()}
460
+ </button>
461
+ );
462
+ })}
463
+ </div>
464
+ </>
465
+ ) : (
466
+ /* Year View */
467
+ <div className="grid grid-cols-4 gap-2">
468
+ {Array.from({ length: 12 }, (_, i) => {
469
+ const monthIndex = i;
470
+ const isCurrentMonth =
471
+ monthIndex === dayjs().month() &&
472
+ currentYear === dayjs().year();
473
+ const isSelected =
474
+ selectedDate &&
475
+ selectedDate.month() === monthIndex &&
476
+ selectedDate.year() === currentYear;
477
+
478
+ return (
479
+ <button
480
+ key={monthIndex}
481
+ type="button"
482
+ onClick={() => {
483
+ const newDate = selectedDate
484
+ ? selectedDate.month(monthIndex)
485
+ : dayjs().month(monthIndex).year(currentYear);
486
+ setSelectedDate(newDate);
487
+ setViewMode("month");
488
+ }}
489
+ className="p-2 rounded-lg text-sm transition-colors"
490
+ style={{
491
+ backgroundColor: isSelected
492
+ ? "hsl(var(--primary))"
493
+ : isCurrentMonth
494
+ ? "hsl(var(--accent))"
495
+ : "transparent",
496
+ color: isSelected
497
+ ? "hsl(var(--primary-foreground))"
498
+ : "hsl(var(--foreground))",
499
+ }}
500
+ onMouseEnter={(e) => {
501
+ if (!isSelected) {
502
+ e.currentTarget.style.backgroundColor =
503
+ "hsl(var(--accent))";
504
+ }
505
+ }}
506
+ onMouseLeave={(e) => {
507
+ e.currentTarget.style.backgroundColor = isSelected
508
+ ? "hsl(var(--primary))"
509
+ : isCurrentMonth
510
+ ? "hsl(var(--accent))"
511
+ : "transparent";
512
+ }}
513
+ >
514
+ {dayjs().month(monthIndex).format("MMM")}
515
+ </button>
516
+ );
517
+ })}
518
+ </div>
519
+ )}
520
+ </div>
521
+ )}
522
+ </div>
523
+ );
524
+ }
525
+ );
526
+ DatePicker.displayName = "DatePicker";
527
+
528
+ export { DatePicker };
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ import { Typography } from "./typography";
3
+
4
+ const DialogContentText = React.forwardRef(
5
+ ({ className, style, children, ...props }, ref) => {
6
+ return (
7
+ <Typography
8
+ ref={ref}
9
+ variant="body2"
10
+ component="p"
11
+ color="muted"
12
+ className={className}
13
+ style={{
14
+ marginBottom: "16px",
15
+ ...style,
16
+ }}
17
+ {...props}
18
+ >
19
+ {children}
20
+ </Typography>
21
+ );
22
+ }
23
+ );
24
+
25
+ DialogContentText.displayName = "DialogContentText";
26
+
27
+ export { DialogContentText };