@mnee-ui/ui 0.0.1 → 0.0.2

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.
@@ -1,164 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useId, useRef } from "react";
4
- import { X } from "lucide-react";
5
- import { cn } from "@/lib/utils";
6
-
7
- export type DrawerWidth = "sm" | "md" | "lg" | "xl";
8
- export type DrawerSide = "left" | "right";
9
-
10
- export interface DrawerProps {
11
- isOpen: boolean;
12
- onClose: () => void;
13
- title?: string;
14
- children?: React.ReactNode;
15
- width?: DrawerWidth;
16
- /** Which edge the drawer slides in from */
17
- side?: DrawerSide;
18
- /** Footer content — rendered in a sticky bar at the bottom */
19
- footer?: React.ReactNode;
20
- className?: string;
21
- }
22
-
23
- const widthStyles: Record<DrawerWidth, string> = {
24
- sm: "w-80",
25
- md: "w-[480px]",
26
- lg: "w-[600px]",
27
- xl: "w-[800px]",
28
- };
29
-
30
- const sideStyles: Record<DrawerSide, { position: string; translate: string }> = {
31
- right: { position: "right-0", translate: "translate-x-full" },
32
- left: { position: "left-0", translate: "-translate-x-full" },
33
- };
34
-
35
- export function Drawer({
36
- isOpen, onClose, title, children, footer,
37
- width = "md", side = "right", className,
38
- }: DrawerProps) {
39
- const headingId = useId();
40
- const panelRef = useRef<HTMLDivElement>(null);
41
- const previousFocusRef = useRef<HTMLElement | null>(null);
42
-
43
- // Close on Escape key
44
- useEffect(() => {
45
- if (!isOpen) return;
46
- const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
47
- document.addEventListener("keydown", handler);
48
- return () => document.removeEventListener("keydown", handler);
49
- }, [isOpen, onClose]);
50
-
51
- // Capture focus before open; move focus into panel on open; restore on close
52
- useEffect(() => {
53
- if (isOpen) {
54
- previousFocusRef.current = document.activeElement as HTMLElement;
55
- requestAnimationFrame(() => panelRef.current?.focus());
56
- } else {
57
- previousFocusRef.current?.focus();
58
- }
59
- }, [isOpen]);
60
-
61
- // Body scroll lock with scrollbar-width compensation to prevent layout shift
62
- useEffect(() => {
63
- if (isOpen) {
64
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
65
- document.body.style.paddingRight = `${scrollbarWidth}px`;
66
- document.body.style.overflow = "hidden";
67
- } else {
68
- document.body.style.overflow = "";
69
- document.body.style.paddingRight = "";
70
- }
71
- return () => {
72
- document.body.style.overflow = "";
73
- document.body.style.paddingRight = "";
74
- };
75
- }, [isOpen]);
76
-
77
- const { position, translate } = sideStyles[side];
78
-
79
- return (
80
- <>
81
- {/* Backdrop */}
82
- <div
83
- aria-hidden="true"
84
- onClick={onClose}
85
- className={cn(
86
- "fixed inset-0 z-40 bg-black/50 transition-opacity duration-300",
87
- isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
88
- )}
89
- />
90
-
91
- {/* Panel */}
92
- <div
93
- ref={panelRef}
94
- role="dialog"
95
- aria-modal="true"
96
- aria-labelledby={title ? headingId : undefined}
97
- tabIndex={-1}
98
- className={cn(
99
- "fixed top-0 z-50 h-full bg-white shadow-2xl flex flex-col outline-none",
100
- "transition-transform duration-300 ease-in-out",
101
- position,
102
- widthStyles[width],
103
- isOpen ? "translate-x-0" : translate,
104
- className
105
- )}
106
- >
107
- {/* Header */}
108
- <DrawerHeader>
109
- {title && (
110
- <h2 id={headingId} className="text-lg font-semibold text-gray-900 flex-1">{title}</h2>
111
- )}
112
- <button
113
- onClick={onClose}
114
- aria-label="Close drawer"
115
- className="text-gray-400 hover:text-gray-600 transition-colors ml-auto"
116
- >
117
- <X size={20} />
118
- </button>
119
- </DrawerHeader>
120
-
121
- {/* Body */}
122
- <DrawerBody>{children}</DrawerBody>
123
-
124
- {/* Footer */}
125
- {footer && <DrawerFooter>{footer}</DrawerFooter>}
126
- </div>
127
- </>
128
- );
129
- }
130
-
131
- // ─── Sub-components (also usable standalone for custom drawer layouts) ─────────
132
-
133
- export function DrawerHeader({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
134
- return (
135
- <div
136
- className={cn("flex items-center gap-3 px-6 py-4 border-b border-gray-200 shrink-0", className)}
137
- {...props}
138
- >
139
- {children}
140
- </div>
141
- );
142
- }
143
-
144
- export function DrawerBody({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
145
- return (
146
- <div
147
- className={cn("flex-1 overflow-y-auto px-6 py-5", className)}
148
- {...props}
149
- >
150
- {children}
151
- </div>
152
- );
153
- }
154
-
155
- export function DrawerFooter({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
156
- return (
157
- <div
158
- className={cn("shrink-0 px-6 py-4 border-t border-gray-200 bg-white", className)}
159
- {...props}
160
- >
161
- {children}
162
- </div>
163
- );
164
- }
@@ -1,96 +0,0 @@
1
- import { type LucideIcon, type LucideProps } from "lucide-react";
2
- import { cn } from "@/lib/utils";
3
- import type { SVGProps } from "react";
4
-
5
- // ─── Icon wrapper ──────────────────────────────────────────────────────────────
6
-
7
- export type IconSize = "xs" | "sm" | "md" | "lg" | "xl";
8
-
9
- const sizeMap: Record<IconSize, number> = {
10
- xs: 12,
11
- sm: 14,
12
- md: 16,
13
- lg: 20,
14
- xl: 24,
15
- };
16
-
17
- export interface IconProps extends Omit<LucideProps, "size"> {
18
- /** Any Lucide icon component, e.g. import { Wallet } from "lucide-react" */
19
- icon: LucideIcon;
20
- /** Design-system size token. Defaults to "md" (16px). */
21
- size?: IconSize;
22
- }
23
-
24
- export function Icon({ icon: LucideComponent, size = "md", className, ...props }: IconProps) {
25
- return (
26
- <LucideComponent
27
- size={sizeMap[size]}
28
- className={cn("shrink-0", className)}
29
- {...props}
30
- />
31
- );
32
- }
33
-
34
- // ─── Brand icon ───────────────────────────────────────────────────────────────
35
-
36
- export function MneeIcon(props: SVGProps<SVGSVGElement>) {
37
- return (
38
- <svg
39
- xmlns="http://www.w3.org/2000/svg"
40
- width="1em"
41
- height="1em"
42
- fill="none"
43
- viewBox="0 0 797 797"
44
- {...props}
45
- >
46
- <g filter="url(#MneeIcon_a)">
47
- <path
48
- fill="url(#MneeIcon_b)"
49
- d="M148.438 398.438c0-138.072 111.928-250 250-250 138.071 0 250 111.928 250 250 0 138.071-111.929 250-250 250-138.072 0-250-111.929-250-250"
50
- shapeRendering="crispEdges"
51
- />
52
- <path
53
- fill="#05121F"
54
- d="M148.438 398.438c0-138.072 111.928-250 250-250 138.071 0 250 111.928 250 250 0 138.071-111.929 250-250 250-138.072 0-250-111.929-250-250"
55
- shapeRendering="crispEdges"
56
- />
57
- <path
58
- fill="url(#MneeIcon_c)"
59
- d="M164.062 398.438c0-129.442 104.934-234.376 234.376-234.376 129.441 0 234.374 104.934 234.374 234.376 0 129.441-104.933 234.374-234.374 234.374-129.442 0-234.376-104.933-234.376-234.374"
60
- />
61
- <path
62
- fill="url(#MneeIcon_d)"
63
- fillRule="evenodd"
64
- d="m482.765 413.641-.381-88.048a17.06 17.06 0 0 0-5.028-12.031c-3.206-3.205-7.505-5.027-12.043-5.027-4.539 0-8.838 1.822-12.043 5.027s-5.029 7.5-5.029 12.038v145.674c0 13.255-5.267 25.861-14.637 35.227-9.371 9.369-21.982 14.634-35.237 14.634-13.257 0-25.87-5.265-35.238-14.634-9.371-9.366-14.638-21.972-14.638-35.227V325.6c0-4.538-1.823-8.834-5.028-12.038-3.206-3.205-7.505-5.027-12.043-5.027-4.539 0-8.838 1.822-12.043 5.027a17.06 17.06 0 0 0-5.026 12.031l-.383 88.048c-1.37 11.023-6.398 21.223-14.255 29.082-9.37 9.367-21.982 14.634-35.24 14.634-13.256 0-25.869-5.267-35.237-14.634-7.86-7.859-12.888-18.064-14.258-29.09l-.405-24.216-45.026-.008h-21.109l.168-3.435c3.049-62.067 28.993-120.38 72.935-164.313 47.048-47.036 110.287-73.223 176.823-73.223 66.538 0 129.774 26.187 176.822 73.223 30.786 30.78 53.107 69.091 64.523 111.106l1.121 4.13h-34.039l-.709-2.312c-10.404-33.867-29.041-64.69-54.092-89.735-40.88-40.868-95.814-63.618-153.627-63.618s-112.749 22.75-153.63 63.618c-30.057 30.051-50.697 68.188-59.218 109.832l-.397 1.94h62.284v50.872c0 4.536 1.823 8.836 5.028 12.04s7.505 5.028 12.043 5.028 8.838-1.824 12.043-5.028 5.029-7.501 5.029-12.04v-81.893c0-13.252 5.267-25.861 14.637-35.227 9.368-9.369 21.981-14.635 35.238-14.635 13.258 0 25.866 5.266 35.237 14.635 9.371 9.365 14.638 21.975 14.638 35.227v145.674c0 4.538 1.823 8.836 5.028 12.041s7.505 5.024 12.044 5.024c4.537 0 8.837-1.82 12.042-5.024 3.205-3.205 5.027-7.503 5.027-12.041V325.601c0-13.252 5.267-25.861 14.638-35.227 9.369-9.369 21.981-14.635 35.239-14.635s25.868 5.266 35.237 14.635c9.371 9.365 14.638 21.975 14.638 35.227v81.893c0 4.536 1.823 8.836 5.027 12.04s7.504 5.028 12.042 5.028 8.838-1.824 12.044-5.028c3.205-3.204 5.028-7.501 5.028-12.04v-50.872h62.279l-.004-.025h33.355l.435 2.765a252 252 0 0 1 3.044 39.074c0 66.52-26.198 129.742-73.245 176.778-47.049 47.036-110.286 73.224-176.822 73.224-66.538 0-129.776-26.188-176.824-73.224-43.53-43.519-69.423-101.186-72.845-162.651l-.193-3.457h32.838l.192 3.071c3.302 52.872 25.745 102.398 63.205 139.849 40.878 40.87 95.815 63.616 153.628 63.616s112.747-22.746 153.627-63.616c40.881-40.869 63.635-95.789 63.635-153.587q.001-3.73-.125-7.458l-.053-1.561h-33.291l-.408 24.213c-1.37 11.026-6.398 21.231-14.258 29.09-9.37 9.367-21.981 14.634-35.239 14.634-13.256 0-25.87-5.267-35.238-14.634-7.857-7.856-12.887-18.056-14.257-29.082"
65
- clipRule="evenodd"
66
- />
67
- </g>
68
- <defs>
69
- <linearGradient id="MneeIcon_b" x1={148.438} x2={648.438} y1={148.438} y2={648.438} gradientUnits="userSpaceOnUse">
70
- <stop stopColor="#FF6900" />
71
- <stop offset={0.5} stopColor="#FE9A00" />
72
- <stop offset={1} stopColor="#FDC700" />
73
- </linearGradient>
74
- <linearGradient id="MneeIcon_c" x1={450.422} x2={558.798} y1={592.492} y2={90.982} gradientUnits="userSpaceOnUse">
75
- <stop stopColor="#05121F" />
76
- <stop offset={1} stopColor="#05121F" />
77
- </linearGradient>
78
- <linearGradient id="MneeIcon_d" x1={253.85} x2={537.261} y1={576.276} y2={231.407} gradientUnits="userSpaceOnUse">
79
- <stop stopColor="#E88C1F" />
80
- <stop offset={1} stopColor="#FFDC46" />
81
- </linearGradient>
82
- <filter id="MneeIcon_a" width={796.875} height={796.875} x={0} y={0} colorInterpolationFilters="sRGB" filterUnits="userSpaceOnUse">
83
- <feFlood floodOpacity={0} result="BackgroundImageFix" />
84
- <feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
85
- <feMorphology in="SourceAlpha" radius={46.875} result="effect1_dropShadow_3676_2343" />
86
- <feOffset />
87
- <feGaussianBlur stdDeviation={97.656} />
88
- <feComposite in2="hardAlpha" operator="out" />
89
- <feColorMatrix values="0 0 0 0 1 0 0 0 0 0.410735 0 0 0 0 0 0 0 0 0.3 0" />
90
- <feBlend in2="BackgroundImageFix" result="effect1_dropShadow_3676_2343" />
91
- <feBlend in="SourceGraphic" in2="effect1_dropShadow_3676_2343" result="shape" />
92
- </filter>
93
- </defs>
94
- </svg>
95
- );
96
- }
@@ -1,129 +0,0 @@
1
- import { cn } from "@/lib/utils";
2
-
3
- export type InputSize = "sm" | "md" | "lg";
4
-
5
- export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
6
- label?: string;
7
- hint?: string;
8
- error?: string;
9
- size?: InputSize;
10
- prefix?: string;
11
- suffix?: string;
12
- leadingIcon?: React.ReactNode;
13
- trailingIcon?: React.ReactNode;
14
- }
15
-
16
- const sizeStyles: Record<InputSize, string> = {
17
- sm: "rounded-md text-xs",
18
- md: "rounded-lg text-sm",
19
- lg: "rounded-lg text-base",
20
- };
21
-
22
- const inputPaddingStyles: Record<InputSize, string> = {
23
- sm: "py-1.5 px-2.5",
24
- md: "py-2 px-3",
25
- lg: "py-2.5 px-4",
26
- };
27
-
28
- const addonPaddingStyles: Record<InputSize, string> = {
29
- sm: "px-2",
30
- md: "px-2.5",
31
- lg: "px-3",
32
- };
33
-
34
- const iconSizeStyles: Record<InputSize, string> = {
35
- sm: "[&>svg]:size-3.5",
36
- md: "[&>svg]:size-4",
37
- lg: "[&>svg]:size-5",
38
- };
39
-
40
- export function Input({
41
- label,
42
- hint,
43
- error,
44
- size = "md",
45
- className,
46
- id,
47
- disabled,
48
- required,
49
- prefix,
50
- suffix,
51
- leadingIcon,
52
- trailingIcon,
53
- ...props
54
- }: InputProps) {
55
- const inputId = id ?? (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined);
56
- const hasLeading = leadingIcon != null || prefix != null;
57
- const hasTrailing = trailingIcon != null || suffix != null;
58
-
59
- return (
60
- <div className={cn("w-full flex flex-col gap-1", className)}>
61
- {label && (
62
- <label
63
- htmlFor={inputId}
64
- className="text-sm font-medium text-gray-900"
65
- >
66
- {label}
67
- {required && (
68
- <span aria-hidden="true" className="text-error ml-0.5">*</span>
69
- )}
70
- </label>
71
- )}
72
- <div
73
- className={cn(
74
- "flex items-center w-full border bg-white transition-colors",
75
- "focus-within:ring-2 focus-within:ring-brand/50",
76
- "has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50",
77
- error
78
- ? "border-error focus-within:border-error focus-within:ring-error/30"
79
- : "border-gray-300 focus-within:border-brand",
80
- sizeStyles[size],
81
- )}
82
- >
83
- {leadingIcon && (
84
- <span className={cn("flex items-center pointer-events-none text-gray-400", addonPaddingStyles[size], iconSizeStyles[size])}>
85
- {leadingIcon}
86
- </span>
87
- )}
88
- {prefix && (
89
- <span className={cn("flex items-center pointer-events-none text-gray-400 select-none", addonPaddingStyles[size])}>
90
- {prefix}
91
- </span>
92
- )}
93
- <input
94
- id={inputId}
95
- disabled={disabled}
96
- required={required}
97
- aria-invalid={!!error}
98
- aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}
99
- className={cn(
100
- "flex-1 min-w-0 bg-transparent outline-none placeholder:text-gray-400",
101
- inputPaddingStyles[size],
102
- hasLeading && "pl-1.5",
103
- hasTrailing && "pr-1.5",
104
- )}
105
- {...props}
106
- />
107
- {suffix && (
108
- <span className={cn("flex items-center pointer-events-none text-gray-400 select-none", addonPaddingStyles[size])}>
109
- {suffix}
110
- </span>
111
- )}
112
- {trailingIcon && (
113
- <span className={cn("flex items-center pointer-events-none text-gray-400", addonPaddingStyles[size], iconSizeStyles[size])}>
114
- {trailingIcon}
115
- </span>
116
- )}
117
- </div>
118
- {error ? (
119
- <p id={`${inputId}-error`} className="text-xs text-error">
120
- {error}
121
- </p>
122
- ) : hint ? (
123
- <p id={`${inputId}-hint`} className="text-xs text-muted">
124
- {hint}
125
- </p>
126
- ) : null}
127
- </div>
128
- );
129
- }
@@ -1,134 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect } from "react";
4
- import { X } from "lucide-react";
5
- import { cn } from "@/lib/utils";
6
-
7
- export type ModalSize = "sm" | "md" | "lg";
8
-
9
- export interface ModalProps {
10
- isOpen: boolean;
11
- onClose: () => void;
12
- title?: string;
13
- children?: React.ReactNode;
14
- size?: ModalSize;
15
- /** Footer content — rendered in a sticky bar at the bottom */
16
- footer?: React.ReactNode;
17
- className?: string;
18
- }
19
-
20
- const sizeStyles: Record<ModalSize, string> = {
21
- sm: "w-[400px]",
22
- md: "w-[520px]",
23
- lg: "w-[640px]",
24
- };
25
-
26
- export function Modal({ isOpen, onClose, title, children, footer, size = "sm", className }: ModalProps) {
27
- // Close on Escape key
28
- useEffect(() => {
29
- if (!isOpen) return;
30
- const handler = (e: KeyboardEvent) => {
31
- if (e.key === "Escape") onClose();
32
- };
33
- document.addEventListener("keydown", handler);
34
- return () => document.removeEventListener("keydown", handler);
35
- }, [isOpen, onClose]);
36
-
37
- // Prevent body scroll while open
38
- useEffect(() => {
39
- document.body.style.overflow = isOpen ? "hidden" : "";
40
- return () => { document.body.style.overflow = ""; };
41
- }, [isOpen]);
42
-
43
- return (
44
- <>
45
- {/* Backdrop */}
46
- <div
47
- aria-hidden="true"
48
- onClick={onClose}
49
- className={cn(
50
- "fixed inset-0 z-40 bg-black/50 transition-opacity duration-200",
51
- isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
52
- )}
53
- />
54
-
55
- {/* Centering container — pointer-events-none so clicks reach the backdrop */}
56
- <div
57
- className={cn(
58
- "fixed inset-0 z-50 flex items-center justify-center p-4",
59
- "pointer-events-none"
60
- )}
61
- >
62
- {/* Panel */}
63
- <div
64
- role="dialog"
65
- aria-modal="true"
66
- aria-label={title}
67
- onClick={(e) => e.stopPropagation()}
68
- className={cn(
69
- "bg-white rounded-lg shadow-2xl flex flex-col max-h-[90vh] pointer-events-auto",
70
- "transition-all duration-200 ease-out",
71
- sizeStyles[size],
72
- isOpen ? "opacity-100 scale-100" : "opacity-0 scale-95",
73
- className
74
- )}
75
- >
76
- {/* Header */}
77
- <ModalHeader>
78
- {title && (
79
- <h2 className="text-lg font-semibold text-gray-900 flex-1">{title}</h2>
80
- )}
81
- <button
82
- onClick={onClose}
83
- aria-label="Close modal"
84
- className="text-gray-400 hover:text-gray-600 transition-colors ml-auto"
85
- >
86
- <X size={20} />
87
- </button>
88
- </ModalHeader>
89
-
90
- {/* Body */}
91
- <ModalBody>{children}</ModalBody>
92
-
93
- {/* Footer */}
94
- {footer && <ModalFooter>{footer}</ModalFooter>}
95
- </div>
96
- </div>
97
- </>
98
- );
99
- }
100
-
101
- // ─── Sub-components (also usable standalone for custom modal layouts) ─────────
102
-
103
- export function ModalHeader({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
104
- return (
105
- <div
106
- className={cn("flex items-center gap-3 px-6 py-4 border-b border-gray-200 shrink-0", className)}
107
- {...props}
108
- >
109
- {children}
110
- </div>
111
- );
112
- }
113
-
114
- export function ModalBody({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
115
- return (
116
- <div
117
- className={cn("flex-1 overflow-y-auto px-6 py-5", className)}
118
- {...props}
119
- >
120
- {children}
121
- </div>
122
- );
123
- }
124
-
125
- export function ModalFooter({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
126
- return (
127
- <div
128
- className={cn("shrink-0 px-6 py-4 border-t border-gray-200 bg-white rounded-b-lg", className)}
129
- {...props}
130
- >
131
- {children}
132
- </div>
133
- );
134
- }