pejay-ui 1.0.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/registry.json ADDED
@@ -0,0 +1,350 @@
1
+ {
2
+ "button": {
3
+ "name": "Button",
4
+ "category": "button",
5
+ "files": [
6
+ "templates/button/Button.tsx",
7
+ "templates/button/tooltip.tsx"
8
+ ],
9
+ "utils": [
10
+ "cn.ts"
11
+ ],
12
+ "peerDependencies": [
13
+ "clsx",
14
+ "tailwind-merge"
15
+ ]
16
+ },
17
+ "form/input": {
18
+ "name": "Input",
19
+ "category": "form",
20
+ "files": [
21
+ "templates/form/input.tsx"
22
+ ],
23
+ "utils": [
24
+ "cn.ts"
25
+ ],
26
+ "peerDependencies": [
27
+ "clsx",
28
+ "tailwind-merge",
29
+ "lucide-react"
30
+ ]
31
+ },
32
+ "form/amount-input": {
33
+ "name": "AmountInput",
34
+ "category": "form",
35
+ "files": [
36
+ "templates/form/amount-input.tsx"
37
+ ],
38
+ "utils": [
39
+ "cn.ts"
40
+ ],
41
+ "peerDependencies": [
42
+ "clsx",
43
+ "tailwind-merge",
44
+ "lucide-react"
45
+ ]
46
+ },
47
+ "form/checkbox": {
48
+ "name": "Checkbox",
49
+ "category": "form",
50
+ "files": [
51
+ "templates/form/checkbox.tsx"
52
+ ],
53
+ "utils": [
54
+ "cn.ts"
55
+ ],
56
+ "peerDependencies": [
57
+ "clsx",
58
+ "tailwind-merge",
59
+ "lucide-react"
60
+ ]
61
+ },
62
+ "form/checkbox-group": {
63
+ "name": "CheckboxGroup",
64
+ "category": "form",
65
+ "files": [
66
+ "templates/form/checkbox-group.tsx"
67
+ ],
68
+ "utils": [
69
+ "cn.ts"
70
+ ],
71
+ "peerDependencies": [
72
+ "clsx",
73
+ "tailwind-merge"
74
+ ],
75
+ "dependencies": [
76
+ "form/checkbox"
77
+ ]
78
+ },
79
+ "form/date-picker": {
80
+ "name": "DatePicker",
81
+ "category": "form",
82
+ "files": [
83
+ "templates/form/date-picker.tsx"
84
+ ],
85
+ "utils": [
86
+ "cn.ts"
87
+ ],
88
+ "peerDependencies": [
89
+ "clsx",
90
+ "tailwind-merge",
91
+ "lucide-react",
92
+ "@floating-ui/react"
93
+ ],
94
+ "dependencies": [
95
+ "select-dropdown/select-input"
96
+ ]
97
+ },
98
+ "form/date-range-picker": {
99
+ "name": "DateRangePicker",
100
+ "category": "form",
101
+ "files": [
102
+ "templates/form/date-range-picker.tsx"
103
+ ],
104
+ "utils": [
105
+ "cn.ts"
106
+ ],
107
+ "peerDependencies": [
108
+ "clsx",
109
+ "tailwind-merge",
110
+ "lucide-react",
111
+ "@floating-ui/react"
112
+ ],
113
+ "dependencies": [
114
+ "select-dropdown/select-input"
115
+ ]
116
+ },
117
+ "form/email-input": {
118
+ "name": "EmailInput",
119
+ "category": "form",
120
+ "files": [
121
+ "templates/form/email-input.tsx"
122
+ ],
123
+ "utils": [
124
+ "cn.ts"
125
+ ],
126
+ "peerDependencies": [
127
+ "clsx",
128
+ "tailwind-merge",
129
+ "lucide-react"
130
+ ]
131
+ },
132
+ "form/file-input": {
133
+ "name": "FileInput",
134
+ "category": "form",
135
+ "files": [
136
+ "templates/form/file-input.tsx"
137
+ ],
138
+ "utils": [
139
+ "cn.ts"
140
+ ],
141
+ "peerDependencies": [
142
+ "clsx",
143
+ "tailwind-merge",
144
+ "lucide-react"
145
+ ]
146
+ },
147
+ "form/number-input": {
148
+ "name": "NumberInput",
149
+ "category": "form",
150
+ "files": [
151
+ "templates/form/number-input.tsx"
152
+ ],
153
+ "utils": [
154
+ "cn.ts"
155
+ ],
156
+ "peerDependencies": [
157
+ "clsx",
158
+ "tailwind-merge",
159
+ "lucide-react"
160
+ ]
161
+ },
162
+ "form/password-input": {
163
+ "name": "PasswordInput",
164
+ "category": "form",
165
+ "files": [
166
+ "templates/form/password-input.tsx"
167
+ ],
168
+ "utils": [
169
+ "cn.ts"
170
+ ],
171
+ "peerDependencies": [
172
+ "clsx",
173
+ "tailwind-merge",
174
+ "lucide-react"
175
+ ]
176
+ },
177
+ "form/phone-input": {
178
+ "name": "PhoneInput",
179
+ "category": "form",
180
+ "files": [
181
+ "templates/form/phone-input.tsx"
182
+ ],
183
+ "utils": [
184
+ "cn.ts"
185
+ ],
186
+ "peerDependencies": [
187
+ "clsx",
188
+ "tailwind-merge",
189
+ "lucide-react"
190
+ ]
191
+ },
192
+ "form/radio": {
193
+ "name": "Radio",
194
+ "category": "form",
195
+ "files": [
196
+ "templates/form/radio.tsx"
197
+ ],
198
+ "utils": [
199
+ "cn.ts"
200
+ ],
201
+ "peerDependencies": [
202
+ "clsx",
203
+ "tailwind-merge"
204
+ ]
205
+ },
206
+ "form/radio-group": {
207
+ "name": "RadioGroup",
208
+ "category": "form",
209
+ "files": [
210
+ "templates/form/radio-group.tsx"
211
+ ],
212
+ "utils": [
213
+ "cn.ts"
214
+ ],
215
+ "peerDependencies": [
216
+ "clsx",
217
+ "tailwind-merge"
218
+ ],
219
+ "dependencies": [
220
+ "form/radio"
221
+ ]
222
+ },
223
+ "form/range-slider": {
224
+ "name": "RangeSlider",
225
+ "category": "form",
226
+ "files": [
227
+ "templates/form/range-slider.tsx"
228
+ ],
229
+ "utils": [
230
+ "cn.ts"
231
+ ],
232
+ "peerDependencies": [
233
+ "clsx",
234
+ "tailwind-merge"
235
+ ]
236
+ },
237
+ "form/switch": {
238
+ "name": "Switch",
239
+ "category": "form",
240
+ "files": [
241
+ "templates/form/switch.tsx"
242
+ ],
243
+ "utils": [
244
+ "cn.ts"
245
+ ],
246
+ "peerDependencies": [
247
+ "clsx",
248
+ "tailwind-merge"
249
+ ]
250
+ },
251
+ "form/textarea": {
252
+ "name": "Textarea",
253
+ "category": "form",
254
+ "files": [
255
+ "templates/form/textarea.tsx"
256
+ ],
257
+ "utils": [
258
+ "cn.ts"
259
+ ],
260
+ "peerDependencies": [
261
+ "clsx",
262
+ "tailwind-merge"
263
+ ]
264
+ },
265
+ "form/time-picker": {
266
+ "name": "TimePicker",
267
+ "category": "form",
268
+ "files": [
269
+ "templates/form/time-picker.tsx"
270
+ ],
271
+ "utils": [
272
+ "cn.ts"
273
+ ],
274
+ "peerDependencies": [
275
+ "clsx",
276
+ "tailwind-merge",
277
+ "lucide-react",
278
+ "@floating-ui/react"
279
+ ],
280
+ "dependencies": [
281
+ "select-dropdown/select-input"
282
+ ]
283
+ },
284
+ "form/time-range-picker": {
285
+ "name": "TimeRangePicker",
286
+ "category": "form",
287
+ "files": [
288
+ "templates/form/time-range-picker.tsx"
289
+ ],
290
+ "utils": [
291
+ "cn.ts"
292
+ ],
293
+ "peerDependencies": [
294
+ "clsx",
295
+ "tailwind-merge",
296
+ "lucide-react",
297
+ "@floating-ui/react"
298
+ ],
299
+ "dependencies": [
300
+ "select-dropdown/select-input"
301
+ ]
302
+ },
303
+ "form/url-input": {
304
+ "name": "UrlInput",
305
+ "category": "form",
306
+ "files": [
307
+ "templates/form/url-input.tsx"
308
+ ],
309
+ "utils": [
310
+ "cn.ts"
311
+ ],
312
+ "peerDependencies": [
313
+ "clsx",
314
+ "tailwind-merge",
315
+ "lucide-react"
316
+ ]
317
+ },
318
+ "dropdown/select-input": {
319
+ "name": "SelectInput",
320
+ "category": "select-dropdown",
321
+ "files": [
322
+ "templates/select-dropdown/select-input.tsx"
323
+ ],
324
+ "utils": [
325
+ "cn.ts"
326
+ ],
327
+ "peerDependencies": [
328
+ "clsx",
329
+ "tailwind-merge",
330
+ "lucide-react",
331
+ "@floating-ui/react"
332
+ ]
333
+ },
334
+ "dropdown/multiselect-input": {
335
+ "name": "MultiselectInput",
336
+ "category": "select-dropdown",
337
+ "files": [
338
+ "templates/select-dropdown/multiselect-input.tsx"
339
+ ],
340
+ "utils": [
341
+ "cn.ts"
342
+ ],
343
+ "peerDependencies": [
344
+ "clsx",
345
+ "tailwind-merge",
346
+ "lucide-react",
347
+ "@floating-ui/react"
348
+ ]
349
+ }
350
+ }
@@ -0,0 +1,156 @@
1
+ import React from "react";
2
+ import { cn } from "@/utils/cn";
3
+
4
+ import { Tooltip } from "./tooltip";
5
+
6
+ /* ─────────────────────────────────────────────
7
+ Types
8
+ ───────────────────────────────────────────── */
9
+
10
+ export type ButtonVariant =
11
+ /* ── Solid (filled bg, white text) ──────── */
12
+ | "primary"
13
+ | "danger"
14
+ | "success"
15
+ | "warning"
16
+ | "black"
17
+ | "white"
18
+ /* ── Soft (coloured text, tinted bg) ────── */
19
+ | "primary-soft"
20
+ | "danger-soft"
21
+ | "success-soft"
22
+ | "warning-soft"
23
+ | "black-soft"
24
+ | "white-soft"
25
+ /* ── Ghost (transparent bg → soft on hover) ─ */
26
+ | "primary-ghost"
27
+ | "danger-ghost"
28
+ | "success-ghost"
29
+ | "warning-ghost"
30
+ | "black-ghost"
31
+ | "white-ghost";
32
+ export type RoundedStyle = "full" | "lg" | "md" | "sm" | "none";
33
+
34
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
35
+ children?: React.ReactNode;
36
+ variant?: ButtonVariant;
37
+ rounded?: RoundedStyle;
38
+ /** Removes all hover and active state classes from the variant. */
39
+ disableHoverEffect?: boolean;
40
+ /** When true the button is disabled and shows the loader or fallback text. */
41
+ isLoading?: boolean;
42
+ /** Custom loader — pass any React node (e.g. a spinner component). */
43
+ loader?: React.ReactNode;
44
+ /** Sets width to 100% of the parent container. */
45
+ fullWidth?: boolean;
46
+ /** Optional content to show inside a hover tooltip. */
47
+ tooltipContent?: React.ReactNode | string | null;
48
+ }
49
+
50
+
51
+ /* ─────────────────────────────────────────────
52
+ Maps
53
+ ───────────────────────────────────────────── */
54
+
55
+ /*
56
+ * Solid variants — filled background, white (or black for 'white') text.
57
+ * Soft variants — coloured text + bg-current/10. Tint always matches text.
58
+ * Ghost variants — transparent bg, coloured text. On hover, bg-current/10
59
+ * fades in (same soft tint), giving a clean "reveal on hover" effect.
60
+ * bg-transparent → hover:bg-current/10 → active:bg-current/15
61
+ */
62
+ const variantMap: Record<ButtonVariant, string> = {
63
+ /* ── Solid ─────────────────────────────────────────── */
64
+ primary: "bg-sky-500 hover:bg-sky-600 active:bg-sky-700 text-white",
65
+ danger: "bg-red-600 hover:bg-red-700 active:bg-red-800 text-white",
66
+ success: "bg-emerald-600 hover:bg-emerald-700 active:bg-emerald-800 text-white",
67
+ warning: "bg-amber-500 hover:bg-amber-600 active:bg-amber-700 text-white",
68
+ black: "bg-black hover:bg-black/80 active:bg-black/70 text-white",
69
+ white: "bg-white hover:bg-white/80 active:bg-white/70 text-black",
70
+
71
+ /* ── Soft (coloured text, always-visible tint) ───────── */
72
+ "primary-soft": "text-sky-500 bg-current/10 hover:bg-current/15 active:bg-current/20",
73
+ "danger-soft": "text-red-600 bg-current/10 hover:bg-current/15 active:bg-current/20",
74
+ "success-soft": "text-emerald-600 bg-current/10 hover:bg-current/15 active:bg-current/20",
75
+ "warning-soft": "text-amber-500 bg-current/10 hover:bg-current/15 active:bg-current/20",
76
+ "black-soft": "text-black bg-current/10 hover:bg-current/15 active:bg-current/20",
77
+ "white-soft": "text-white bg-current/10 hover:bg-current/15 active:bg-current/20",
78
+
79
+ /* ── Ghost (transparent → soft tint on hover) ──────── */
80
+ "primary-ghost": "text-sky-500 bg-transparent hover:bg-current/10 active:bg-current/15",
81
+ "danger-ghost": "text-red-600 bg-transparent hover:bg-current/10 active:bg-current/15",
82
+ "success-ghost": "text-emerald-600 bg-transparent hover:bg-current/10 active:bg-current/15",
83
+ "warning-ghost": "text-amber-500 bg-transparent hover:bg-current/10 active:bg-current/15",
84
+ "black-ghost": "text-black bg-transparent hover:bg-current/10 active:bg-current/15",
85
+ "white-ghost": "text-white bg-transparent hover:bg-current/10 active:bg-current/15",
86
+ };
87
+
88
+ const roundedMap: Record<RoundedStyle, string> = {
89
+ full: "rounded-full",
90
+ lg: "rounded-lg",
91
+ md: "rounded-md",
92
+ sm: "rounded-sm",
93
+ none: "rounded-none",
94
+ };
95
+
96
+ /* Strip hover: and active: modifier classes so the button stays visually static */
97
+ const stripInteractive = (classes: string) =>
98
+ classes
99
+ .split(" ")
100
+ .filter((c) => !c.startsWith("hover:") && !c.startsWith("active:"))
101
+ .join(" ");
102
+
103
+ export const Button = ({
104
+ variant = "primary",
105
+ rounded = "lg",
106
+ disableHoverEffect = false,
107
+ isLoading = false,
108
+ loader,
109
+ fullWidth = false,
110
+ tooltipContent,
111
+ className,
112
+ children,
113
+ type = "button",
114
+ disabled,
115
+ ...props
116
+ }: ButtonProps) => {
117
+ /* Resolve variant classes, stripping hover/active when disableHoverEffect is set */
118
+ const variantClasses = disableHoverEffect
119
+ ? stripInteractive(variantMap[variant])
120
+ : variantMap[variant];
121
+
122
+ /* Render content: loader node / fallback text / normal children */
123
+ const content = isLoading
124
+ ? (loader ?? <span className="text-sm font-medium">Loading…</span>)
125
+ : children;
126
+
127
+ return (
128
+ <Tooltip content={tooltipContent}>
129
+ <button
130
+ type={type}
131
+ disabled={disabled || isLoading}
132
+ className={cn(
133
+ /* Base layout & typography */
134
+ "inline-flex items-center justify-center gap-2 px-5 h-9",
135
+ "whitespace-nowrap text-sm font-medium",
136
+ /* Smooth state transitions */
137
+ "transition-colors duration-150 cursor-pointer",
138
+ /* Keyboard focus ring (Accessibility) */
139
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-sky-500",
140
+ /* Disabled state (covers isLoading too) */
141
+ "disabled:opacity-50 disabled:cursor-not-allowed",
142
+ /* Full width utility */
143
+ fullWidth && "w-full",
144
+ variantClasses,
145
+ roundedMap[rounded],
146
+ className,
147
+ )}
148
+ {...props}
149
+ >
150
+ {content}
151
+ </button>
152
+ </Tooltip>
153
+ );
154
+ };
155
+
156
+
@@ -0,0 +1,2 @@
1
+ export * from "./Button";
2
+ export * from "./tooltip";
@@ -0,0 +1,124 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import {
3
+ useFloating,
4
+ autoUpdate,
5
+ offset,
6
+ flip,
7
+ shift,
8
+ useHover,
9
+ useFocus,
10
+ useDismiss,
11
+ useRole,
12
+ useInteractions,
13
+ FloatingPortal,
14
+ type Placement,
15
+ } from "@floating-ui/react";
16
+ import { cn } from "@/utils/cn";
17
+
18
+ interface TooltipProps {
19
+ children: React.ReactNode | string;
20
+ content?: React.ReactNode | string | null | undefined;
21
+ className?: string;
22
+ /** Position of the tooltip relative to the trigger. Defaults to "top" */
23
+ direction?: Placement;
24
+ /** Whether the tooltip should be disabled */
25
+ disabled?: boolean;
26
+ /** Custom class for the reference wrapper element */
27
+ wrapperClassName?: string;
28
+ /** If true, the reference wrapper spans 100% width instead of shrink-wrapping */
29
+ fullWidth?: boolean;
30
+ }
31
+
32
+
33
+ /**
34
+ * A custom Tooltip component built with Floating UI for professional positioning
35
+ * and Portals to avoid container clipping (overflow: hidden).
36
+ */
37
+ export const Tooltip = ({
38
+ children,
39
+ content = null,
40
+ className,
41
+ direction = "top",
42
+ disabled = false,
43
+ wrapperClassName,
44
+ fullWidth = false,
45
+ }: TooltipProps) => {
46
+ const [isOpen, setIsOpen] = useState(false);
47
+
48
+ // Force close tooltip when it becomes disabled or content is removed
49
+ useEffect(() => {
50
+ const isContentEmpty = !content || (typeof content === "string" && content.trim() === "");
51
+ if (disabled || isContentEmpty) {
52
+ setIsOpen(false);
53
+ }
54
+ }, [disabled, content]);
55
+
56
+ // 1. Setup Floating UI logic
57
+ const { refs, floatingStyles, context } = useFloating({
58
+ open: isOpen && !disabled && !!content,
59
+ onOpenChange: setIsOpen,
60
+ placement: direction,
61
+ whileElementsMounted: autoUpdate,
62
+ middleware: [
63
+ offset(16), // Matches Spotify's gap
64
+ flip({ fallbackAxisSideDirection: "start" }), // Flips if hits screen edge
65
+ shift({ padding: 5 }), // Shifts slightly if hitting side edge
66
+ ],
67
+ });
68
+
69
+ // 2. Setup Interactions
70
+ const hover = useHover(context, { move: false, delay: 50 });
71
+ const focus = useFocus(context);
72
+ const dismiss = useDismiss(context);
73
+ const role = useRole(context, { role: "tooltip" });
74
+
75
+ const { getReferenceProps, getFloatingProps } = useInteractions([
76
+ hover,
77
+ focus,
78
+ dismiss,
79
+ role,
80
+ ]);
81
+
82
+ if (disabled) {
83
+ return <>{children}</>;
84
+ }
85
+
86
+ // If disabled or no content provided, just return the trigger without the wrapper div
87
+ if (!content || (typeof content === "string" && content.trim() === "")) {
88
+ return <>{children}</>;
89
+ }
90
+
91
+ return (
92
+ <>
93
+ {/* Trigger element - wrapped in a div to serve as the reference point */}
94
+ <div
95
+ ref={refs.setReference}
96
+ {...getReferenceProps()}
97
+ className={cn(
98
+ fullWidth ? "w-full flex items-center min-w-0" : "w-fit inline-flex items-center min-w-0",
99
+ wrapperClassName
100
+ )}
101
+ >
102
+ {children}
103
+ </div>
104
+
105
+ {/* Floating element - rendered in a Portal to break out of overflow:hidden parents */}
106
+ {isOpen && (
107
+ <FloatingPortal>
108
+ <div
109
+ ref={refs.setFloating}
110
+ style={floatingStyles}
111
+ {...getFloatingProps()}
112
+ className={cn(
113
+ "z-9999 px-2 py-1 text-xs font-medium text-black bg-gray-100 rounded-sm max-w-xs whitespace-normal break-words pointer-events-none",
114
+ isOpen ? "opacity-100 scale-100" : "opacity-0 scale-95",
115
+ className,
116
+ )}
117
+ >
118
+ {content}
119
+ </div>
120
+ </FloatingPortal>
121
+ )}
122
+ </>
123
+ );
124
+ };