@myzbox/react-overlay 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.
@@ -0,0 +1,71 @@
1
+ import React, { createContext, useState, type ReactNode, useCallback } from 'react';
2
+ import { Toast } from './Toast';
3
+ import type { ToastOptions, ToastPosition } from '../../types/Toast';
4
+ import styles from '../../styles/Toast.module.css';
5
+ import classNames from 'classnames';
6
+
7
+ interface ToastContextProps {
8
+ addToast: (options: Omit<ToastOptions, 'id'> & { closeOthers?: boolean }) => void;
9
+ removeToast: (id: string) => void;
10
+ clearAll: () => void;
11
+ }
12
+
13
+ export const ToastContext = createContext<ToastContextProps | undefined>(undefined);
14
+
15
+ let idCounter = 0;
16
+
17
+ export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
18
+ const [toasts, setToasts] = useState<ToastOptions[]>([]);
19
+
20
+ const addToast = useCallback(({ closeOthers, delay = 0, ...options }: Omit<ToastOptions, 'id'> & { closeOthers?: boolean }) => {
21
+ const id = `toast-${idCounter++}`;
22
+ const newToast = { ...options, duration: options.duration, delay, id };
23
+
24
+ const dispatch = () => {
25
+ setToasts((prev) => {
26
+ if (closeOthers) {
27
+ return [newToast];
28
+ }
29
+ return [...prev, newToast];
30
+ });
31
+ };
32
+
33
+ if (delay > 0) {
34
+ setTimeout(dispatch, delay);
35
+ } else {
36
+ dispatch();
37
+ }
38
+ }, []);
39
+
40
+ const removeToast = useCallback((id: string) => {
41
+ setToasts((prev) => prev.filter((t) => t.id !== id));
42
+ }, []);
43
+
44
+ const clearAll = useCallback(() => {
45
+ setToasts([]);
46
+ }, []);
47
+
48
+ // Group toasts by position to render different containers
49
+ const toastsByPosition = toasts.reduce((acc, toast) => {
50
+ const pos = toast.position || 'top-right';
51
+ if (!acc[pos]) acc[pos] = [];
52
+ acc[pos].push(toast);
53
+ return acc;
54
+ }, {} as Record<ToastPosition, ToastOptions[]>);
55
+
56
+ return (
57
+ <ToastContext.Provider value={{ addToast, removeToast, clearAll }}>
58
+ {children}
59
+ {(Object.keys(toastsByPosition) as ToastPosition[]).map((pos) => (
60
+ <div key={pos} className={classNames(styles.container, styles[pos])}>
61
+ {toastsByPosition[pos].map((toast) => (
62
+ <Toast key={toast.id} {...toast} onDismiss={removeToast} />
63
+ ))}
64
+ </div>
65
+ ))}
66
+ </ToastContext.Provider>
67
+ );
68
+ };
69
+
70
+ export default ToastProvider;
71
+
@@ -0,0 +1,3 @@
1
+ export * from './Toast';
2
+ export * from './ToastProvider';
3
+ export * from './useToast';
@@ -0,0 +1,48 @@
1
+ import { useContext } from 'react';
2
+ import { ToastContext } from './ToastProvider';
3
+ import type { ToastOptions } from '../../types/Toast';
4
+
5
+ type ToastInput = React.ReactNode | (Omit<ToastOptions, 'id' | 'type'> & { content: React.ReactNode });
6
+
7
+ export const useToast = () => {
8
+ const context = useContext(ToastContext);
9
+
10
+ if (!context) {
11
+ throw new Error('useToast must be used within a ToastProvider');
12
+ }
13
+
14
+ const { addToast, removeToast, clearAll } = context;
15
+
16
+ const show = (type: ToastOptions['type'], input: ToastInput, options: Partial<ToastOptions> & { closeOthers?: boolean } = {}) => {
17
+ let content: React.ReactNode;
18
+ let finalOptions = { ...options };
19
+
20
+ if (typeof input === 'object' && input !== null && 'content' in input && !React.isValidElement(input)) {
21
+ // Handle case where input object contains content and options
22
+ const inputObj = input as Record<string, unknown>;
23
+ content = inputObj.content as React.ReactNode;
24
+ finalOptions = { ...finalOptions, ...inputObj } as Partial<ToastOptions> & { closeOthers?: boolean };
25
+ } else {
26
+ content = input as React.ReactNode;
27
+ }
28
+
29
+ addToast({
30
+ type,
31
+ content,
32
+ duration: 3000,
33
+ position: 'top-right',
34
+ ...finalOptions,
35
+ });
36
+ };
37
+
38
+ return {
39
+ success: (content: React.ReactNode, options?: Partial<ToastOptions> & { closeOthers?: boolean }) => show('success', content, options),
40
+ error: (content: React.ReactNode, options?: Partial<ToastOptions> & { closeOthers?: boolean }) => show('error', content, options),
41
+ warning: (content: React.ReactNode, options?: Partial<ToastOptions> & { closeOthers?: boolean }) => show('warning', content, options),
42
+ info: (content: React.ReactNode, options?: Partial<ToastOptions> & { closeOthers?: boolean }) => show('info', content, options),
43
+ dismiss: (id: string) => removeToast(id),
44
+ dismissAll: () => clearAll(),
45
+ };
46
+ };
47
+
48
+ import React from 'react'; // fix for React usage in type check above
@@ -0,0 +1,52 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import type { TooltipProps } from '../../types/Tooltip';
3
+ import styles from '../../styles/Tooltip.module.css';
4
+ import classNames from 'classnames';
5
+
6
+ export const Tooltip: React.FC<TooltipProps> = ({
7
+ children,
8
+ content,
9
+ position = 'top',
10
+ delay = 200,
11
+ width,
12
+ height,
13
+ className,
14
+ style,
15
+ }) => {
16
+ const [isVisible, setIsVisible] = useState(false);
17
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
18
+
19
+ const showTooltip = () => {
20
+ timeoutRef.current = setTimeout(() => {
21
+ setIsVisible(true);
22
+ }, delay);
23
+ };
24
+
25
+ const hideTooltip = () => {
26
+ if (timeoutRef.current) {
27
+ clearTimeout(timeoutRef.current);
28
+ }
29
+ setIsVisible(false);
30
+ };
31
+
32
+ return (
33
+ <div
34
+ className={styles.wrapper}
35
+ onMouseEnter={showTooltip}
36
+ onMouseLeave={hideTooltip}
37
+ onFocus={showTooltip}
38
+ onBlur={hideTooltip}
39
+ >
40
+ {children}
41
+ {isVisible && (
42
+ <div
43
+ className={classNames(styles.tooltip, styles[position], className)}
44
+ style={{ width, height, ...style }}
45
+ role="tooltip"
46
+ >
47
+ {content}
48
+ </div>
49
+ )}
50
+ </div>
51
+ );
52
+ };
@@ -0,0 +1,24 @@
1
+ import { useEffect, type RefObject } from 'react';
2
+
3
+ export const useClickOutside = (
4
+ ref: RefObject<HTMLElement>,
5
+ handler: (event: MouseEvent | TouchEvent) => void
6
+ ) => {
7
+ useEffect(() => {
8
+ const listener = (event: MouseEvent | TouchEvent) => {
9
+ // Do nothing if clicking ref's element or descendent elements
10
+ if (!ref.current || ref.current.contains(event.target as Node)) {
11
+ return;
12
+ }
13
+ handler(event);
14
+ };
15
+
16
+ document.addEventListener('mousedown', listener);
17
+ document.addEventListener('touchstart', listener);
18
+
19
+ return () => {
20
+ document.removeEventListener('mousedown', listener);
21
+ document.removeEventListener('touchstart', listener);
22
+ };
23
+ }, [ref, handler]);
24
+ };
@@ -0,0 +1,75 @@
1
+ import React, { useRef, useEffect, type RefObject, useCallback } from 'react';
2
+
3
+ export const useDraggable = (
4
+ enabled: boolean,
5
+ elementRef: RefObject<HTMLElement>
6
+ ) => {
7
+ const isDragging = useRef(false);
8
+ const dragStart = useRef({ x: 0, y: 0 });
9
+ const translate = useRef({ x: 0, y: 0 });
10
+
11
+ const onMouseMove = useCallback((e: globalThis.MouseEvent) => {
12
+ if (!isDragging.current || !elementRef.current) return;
13
+
14
+ const dx = e.clientX - dragStart.current.x;
15
+ const dy = e.clientY - dragStart.current.y;
16
+
17
+ const currentX = translate.current.x + dx;
18
+ const currentY = translate.current.y + dy;
19
+
20
+ elementRef.current.style.transform = `translate(${currentX}px, ${currentY}px)`;
21
+ }, [elementRef]);
22
+
23
+ const onMouseUp = useCallback((e: globalThis.MouseEvent) => {
24
+ if (!isDragging.current) return;
25
+ isDragging.current = false;
26
+
27
+ const dx = e.clientX - dragStart.current.x;
28
+ const dy = e.clientY - dragStart.current.y;
29
+
30
+ translate.current = {
31
+ x: translate.current.x + dx,
32
+ y: translate.current.y + dy
33
+ };
34
+
35
+ // Re-enable transition
36
+ if (elementRef.current) {
37
+ elementRef.current.style.transition = '';
38
+ }
39
+
40
+ document.body.style.userSelect = '';
41
+ window.removeEventListener('mousemove', onMouseMove);
42
+ window.removeEventListener('mouseup', onMouseUp);
43
+ }, [elementRef, onMouseMove]);
44
+
45
+ const onMouseDown = (e: React.MouseEvent) => {
46
+ if (!enabled || !elementRef.current) return;
47
+
48
+ isDragging.current = true;
49
+ dragStart.current = { x: e.clientX, y: e.clientY };
50
+
51
+ // Disable transition for instant follow
52
+ elementRef.current.style.transition = 'none';
53
+
54
+ document.body.style.userSelect = 'none';
55
+ window.addEventListener('mousemove', onMouseMove);
56
+ window.addEventListener('mouseup', onMouseUp);
57
+ };
58
+
59
+ useEffect(() => {
60
+ const cleanup = () => {
61
+ window.removeEventListener('mousemove', onMouseMove);
62
+ window.removeEventListener('mouseup', onMouseUp);
63
+ };
64
+ return cleanup;
65
+ }, [onMouseMove, onMouseUp]);
66
+
67
+ // Reset position if disabled
68
+ useEffect(() => {
69
+ if (!enabled) {
70
+ translate.current = { x: 0, y: 0 };
71
+ }
72
+ }, [enabled]);
73
+
74
+ return { onMouseDown };
75
+ };
@@ -0,0 +1,23 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export interface UseModalReturn {
4
+ isOpen: boolean;
5
+ open: () => void;
6
+ close: () => void;
7
+ toggle: () => void;
8
+ }
9
+
10
+ export const useModal = (initialState = false): UseModalReturn => {
11
+ const [isOpen, setIsOpen] = useState(initialState);
12
+
13
+ const open = useCallback(() => setIsOpen(true), []);
14
+ const close = useCallback(() => setIsOpen(false), []);
15
+ const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
16
+
17
+ return {
18
+ isOpen,
19
+ open,
20
+ close,
21
+ toggle,
22
+ };
23
+ };
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './components';
2
+ export * from './hooks/useModal';
3
+ export * from './hooks/useDraggable';
4
+ export * from './hooks/useClickOutside';
5
+ export * from './types/Modal';
6
+ export * from './types/Toast';
7
+ export * from './types/Popover';
8
+ export * from './types/Tooltip';
@@ -0,0 +1,314 @@
1
+ :root {
2
+ --rm-overlay-bg: rgba(0, 0, 0, 0.5);
3
+ --rm-modal-bg: #ffffff;
4
+ --rm-modal-shadow:
5
+ 0 4px 6px rgba(0, 0, 0, 0.1), 0 10px 15px rgba(0, 0, 0, 0.1);
6
+ --rm-text-color: #374151;
7
+ --rm-title-color: #111827;
8
+ --rm-border-color: #e5e7eb;
9
+ --rm-close-color: #374151;
10
+ --rm-close-hover-bg: #f3f4f6;
11
+ --rm-close-hover-color: #111827;
12
+ --rm-footer-bg: #f9fafb;
13
+ }
14
+
15
+ .overlay {
16
+ box-sizing: border-box;
17
+ position: fixed;
18
+ top: 0;
19
+ left: 0;
20
+ width: 100vw;
21
+ height: 100vh;
22
+ background-color: var(--rm-overlay-bg);
23
+ display: flex;
24
+ justify-content: center;
25
+ align-items: center;
26
+ z-index: 1000;
27
+ opacity: 0;
28
+ transition: opacity 0.2s ease-in-out;
29
+ pointer-events: none;
30
+ }
31
+
32
+ .overlay.open {
33
+ opacity: 1;
34
+ pointer-events: auto;
35
+ }
36
+
37
+ /* Positions */
38
+ .overlay.center {
39
+ justify-content: center;
40
+ align-items: center;
41
+ box-sizing: border-box;
42
+ }
43
+
44
+ .overlay.top {
45
+ align-items: flex-start;
46
+ padding-top: 2rem;
47
+ box-sizing: border-box;
48
+ }
49
+
50
+ .overlay.bottom {
51
+ align-items: flex-end;
52
+ padding-bottom: 2rem;
53
+ }
54
+
55
+ .overlay.left {
56
+ justify-content: flex-start;
57
+ height: 100vh;
58
+ box-sizing: border-box;
59
+ }
60
+
61
+ .overlay.right {
62
+ justify-content: flex-end;
63
+ height: 100vh;
64
+ }
65
+
66
+ .overlay.top-left {
67
+ justify-content: flex-start;
68
+ align-items: flex-start;
69
+ padding: 2rem;
70
+ box-sizing: border-box;
71
+ }
72
+
73
+ .overlay.top-right {
74
+ justify-content: flex-end;
75
+ align-items: flex-start;
76
+ padding: 2rem;
77
+ box-sizing: border-box;
78
+ }
79
+
80
+ .overlay.bottom-left {
81
+ justify-content: flex-start;
82
+ align-items: flex-end;
83
+ padding: 2rem;
84
+ box-sizing: border-box;
85
+ }
86
+
87
+ .overlay.bottom-right {
88
+ justify-content: flex-end;
89
+ align-items: flex-end;
90
+ padding: 2rem;
91
+ box-sizing: border-box;
92
+ }
93
+
94
+ .modal {
95
+ box-sizing: border-box;
96
+ background: var(--rm-modal-bg);
97
+ border-radius: 8px;
98
+ box-shadow: var(--rm-modal-shadow);
99
+ width: 100%;
100
+ max-width: 100%;
101
+ max-height: 90vh;
102
+ display: flex;
103
+ flex-direction: column;
104
+ position: relative;
105
+ overflow: hidden;
106
+ opacity: 0;
107
+ transform: scale(0.95);
108
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
109
+ }
110
+
111
+ .modal *,
112
+ .modal *::before,
113
+ .modal *::after {
114
+ box-sizing: border-box;
115
+ }
116
+
117
+ .modal.open {
118
+ opacity: 1;
119
+ transform: scale(1);
120
+ animation: openScale 0.2s cubic-bezier(0.4, 0, 0.2, 1);
121
+ }
122
+
123
+ /* Sizes */
124
+ .modal.sm {
125
+ width: 400px;
126
+ }
127
+
128
+ .modal.md {
129
+ width: 600px;
130
+ }
131
+
132
+ .modal.lg {
133
+ width: 800px;
134
+ }
135
+
136
+ .modal.xl {
137
+ width: 1140px;
138
+ }
139
+
140
+ .modal.full {
141
+ width: 98vw;
142
+ height: 96vh;
143
+ max-height: 96vh;
144
+ border-radius: 8px;
145
+ }
146
+
147
+ .modal.auto {
148
+ width: auto;
149
+ }
150
+
151
+ @keyframes openScale {
152
+ from {
153
+ opacity: 0;
154
+ transform: scale(0.95);
155
+ }
156
+
157
+ to {
158
+ opacity: 1;
159
+ transform: scale(1);
160
+ }
161
+ }
162
+
163
+ /* Animations */
164
+ .modal.slide-up {
165
+ transform: translateY(20px);
166
+ }
167
+
168
+ .modal.slide-up.open {
169
+ transform: translateY(0);
170
+ }
171
+
172
+ .modal.slide-down {
173
+ transform: translateY(-20px);
174
+ }
175
+
176
+ .modal.slide-down.open {
177
+ transform: translateY(0);
178
+ }
179
+
180
+ .modal.fade {
181
+ transform: none;
182
+ }
183
+
184
+ .modal.fade.open {
185
+ transform: none;
186
+ }
187
+
188
+ .modal.slide-left {
189
+ transform: translateX(-20px);
190
+ }
191
+
192
+ .modal.slide-left.open {
193
+ transform: translateX(0);
194
+ }
195
+
196
+ .modal.slide-right {
197
+ transform: translateX(20px);
198
+ }
199
+
200
+ .modal.slide-right.open {
201
+ transform: translateX(0);
202
+ }
203
+
204
+ /* Drawer Animations */
205
+ .modal.drawer-slide-right {
206
+ transform: translateX(100%);
207
+ }
208
+
209
+ .modal.drawer-slide-right.open {
210
+ transform: translateX(0);
211
+ }
212
+
213
+ .modal.drawer-slide-left {
214
+ transform: translateX(-100%);
215
+ }
216
+
217
+ .modal.drawer-slide-left.open {
218
+ transform: translateX(0);
219
+ }
220
+
221
+ .modal.drawer-slide-up {
222
+ transform: translateY(100%);
223
+ }
224
+
225
+ .modal.drawer-slide-up.open {
226
+ transform: translateY(0);
227
+ }
228
+
229
+ .modal.drawer-slide-down {
230
+ transform: translateY(-100%);
231
+ }
232
+
233
+ .modal.drawer-slide-down.open {
234
+ transform: translateY(0);
235
+ }
236
+
237
+ .header {
238
+ box-sizing: border-box;
239
+ width: 100%;
240
+ padding: 1rem 1.5rem;
241
+ border-bottom: 1px solid var(--rm-border-color);
242
+ display: flex;
243
+ justify-content: space-between;
244
+ align-items: center;
245
+ }
246
+
247
+ .header.draggable {
248
+ cursor: move;
249
+ user-select: none;
250
+ }
251
+
252
+ .title {
253
+ font-size: 1.25rem;
254
+ font-weight: 600;
255
+ color: var(--rm-title-color);
256
+ margin: 0;
257
+ }
258
+
259
+ .closeButton {
260
+ box-sizing: border-box;
261
+ background: transparent;
262
+ border: none;
263
+ cursor: pointer;
264
+ width: 32px;
265
+ height: 32px;
266
+ border-radius: 50%;
267
+ color: var(--rm-close-color);
268
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ margin-right: -4px;
273
+ background-color: transparent;
274
+ }
275
+
276
+ .closeButton:hover {
277
+ color: var(--rm-close-hover-color);
278
+ transform: scale(1.15);
279
+ }
280
+
281
+ .closeButton:active {
282
+ transform: scale(0.95);
283
+ }
284
+
285
+ .closeButton svg {
286
+ width: 16px;
287
+ height: 16px;
288
+ stroke: currentColor;
289
+ stroke-width: 3;
290
+ display: block;
291
+ transition: transform 0.2s;
292
+ }
293
+
294
+ .content {
295
+ box-sizing: border-box;
296
+ width: 100%;
297
+ padding: 1.5rem;
298
+ overflow-y: auto;
299
+ flex: 1;
300
+ color: var(--rm-text-color);
301
+ font-size: 1rem;
302
+ line-height: 1.5;
303
+ }
304
+
305
+ .footer {
306
+ box-sizing: border-box;
307
+ width: 100%;
308
+ padding: 1rem 1.5rem;
309
+ border-top: 1px solid var(--rm-border-color);
310
+ background-color: var(--rm-footer-bg);
311
+ display: flex;
312
+ justify-content: flex-end;
313
+ gap: 0.75rem;
314
+ }