react-side-sheet-pro 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.
@@ -0,0 +1,11 @@
1
+ import React, { ReactNode } from 'react';
2
+ import classNames from 'classnames';
3
+
4
+ export const SideSheetFooter: React.FC<{
5
+ children: ReactNode;
6
+ className?: string;
7
+ }> = ({ children, className }) => (
8
+ <footer className={classNames('sidesheet-footer', className)}>
9
+ {children}
10
+ </footer>
11
+ );
@@ -0,0 +1,21 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { HiX } from 'react-icons/hi';
3
+
4
+ export const SideSheetHeader: React.FC<{
5
+ title: string;
6
+ onClose?: () => void;
7
+ actions?: ReactNode;
8
+ }> = React.memo(({ title, onClose, actions }) => (
9
+ <header className="sidesheet-header">
10
+ {onClose && (
11
+ <button
12
+ className="sidesheet-header-close sidesheet-header-btn"
13
+ onClick={onClose}
14
+ >
15
+ <HiX />
16
+ </button>
17
+ )}
18
+ <div className="sidesheet-header-title">{title}</div>
19
+ {actions}
20
+ </header>
21
+ ));
@@ -0,0 +1,105 @@
1
+ import React, {
2
+ createContext,
3
+ ReactNode,
4
+ useCallback,
5
+ useEffect,
6
+ useReducer,
7
+ useRef,
8
+ } from 'react';
9
+ import { createPortal } from 'react-dom';
10
+ import {
11
+ DEFAULT_OPTIONS,
12
+ DEFAULT_SHEET_OPTIONS,
13
+ } from '../constants/defaultOptions';
14
+ import {
15
+ SideElement,
16
+ SideOptions,
17
+ SideSheetContextValue,
18
+ SideSheetOptions,
19
+ SideStackItem,
20
+ } from '../types';
21
+ import { SideSheetContainer } from '../components/SideSheetContainer';
22
+ import { SideSheetReducer } from '../contexts/SideSheetReducer';
23
+
24
+ export const SideSheetContext = createContext<SideSheetContextValue | null>(
25
+ null
26
+ );
27
+
28
+ export const SideSheetProvider: React.FC<{
29
+ children: ReactNode;
30
+ configuration: Partial<SideSheetOptions>;
31
+ }> = ({ children, configuration }) => {
32
+ const [stack, dispatch] = useReducer(SideSheetReducer, []);
33
+ const idRef = useRef(0);
34
+ const stackRef = useRef<SideStackItem[]>(stack);
35
+
36
+ useEffect(() => {
37
+ stackRef.current = stack;
38
+ }, [stack]);
39
+
40
+ const open = useCallback((element: SideElement, opts: SideOptions = {}) => {
41
+ const id = ++idRef.current;
42
+ const options = { ...DEFAULT_SHEET_OPTIONS, ...opts };
43
+ dispatch({
44
+ type: 'OPEN',
45
+ payload: { id, element, options, state: 'opening' },
46
+ });
47
+ setTimeout(() => {
48
+ dispatch({ type: 'SET_OPEN', id });
49
+ options.onOpen?.(id);
50
+ }, options.animationDuration);
51
+ return id;
52
+ }, []);
53
+
54
+ const close = useCallback(async (id: number | null) => {
55
+ const itemsToClose =
56
+ id === null
57
+ ? [...stackRef.current]
58
+ : stackRef.current.filter(i => i.id === id);
59
+
60
+ for (const item of itemsToClose) {
61
+ if (item.options.confirmBeforeClose) {
62
+ const confirmed = await item.options.confirmCallback(
63
+ item.options.confirmMessage
64
+ );
65
+ if (!confirmed) return;
66
+ }
67
+ item.options.onClose?.(item.id);
68
+ }
69
+ const duration =
70
+ itemsToClose[itemsToClose.length - 1]?.options.animationDuration;
71
+ dispatch({ type: 'CLOSE', id });
72
+ setTimeout(() => {
73
+ if (id === null) {
74
+ stackRef.current.forEach(item =>
75
+ dispatch({ type: 'REMOVE', id: item.id })
76
+ );
77
+ } else {
78
+ dispatch({ type: 'REMOVE', id: id! });
79
+ }
80
+ }, duration);
81
+ }, []);
82
+
83
+ const update = useCallback((id: number, options: Partial<SideOptions>) => {
84
+ dispatch({ type: 'UPDATE', id, options });
85
+ }, []);
86
+ const config = { ...DEFAULT_OPTIONS, ...configuration } as Required<
87
+ SideSheetOptions
88
+ >;
89
+
90
+ return (
91
+ <SideSheetContext.Provider value={{ open, close, update, config }}>
92
+ {children}
93
+ {createPortal(
94
+ <SideSheetContainer
95
+ stack={stack}
96
+ close={close}
97
+ open={open}
98
+ update={update}
99
+ config={config}
100
+ />,
101
+ document.body
102
+ )}
103
+ </SideSheetContext.Provider>
104
+ );
105
+ };
@@ -0,0 +1,22 @@
1
+ import { SideOptions, SideSheetOptions } from '../types';
2
+
3
+ export const DEFAULT_OPTIONS: Required<SideSheetOptions> = {
4
+ side: 'right',
5
+ mountStrategy: 'all',
6
+ };
7
+
8
+ export const DEFAULT_SHEET_OPTIONS: Required<SideOptions> = {
9
+ width: 400,
10
+ className: '',
11
+ confirmBeforeClose: false,
12
+ confirmMessage: 'Are you sure you want to close?',
13
+ confirmCallback: async (msg: string) =>
14
+ typeof window !== 'undefined'
15
+ ? Promise.resolve(window.confirm(msg))
16
+ : Promise.resolve(true),
17
+ closeOnOverlayClick: true,
18
+ closeOnEsc: true,
19
+ animationDuration: 240,
20
+ onOpen: () => {},
21
+ onClose: () => {},
22
+ };
@@ -0,0 +1,41 @@
1
+ import { SideOptions, SideStackItem } from '../types';
2
+
3
+ type Action =
4
+ | { type: 'OPEN'; payload: SideStackItem }
5
+ | { type: 'SET_OPEN'; id: number }
6
+ | { type: 'CLOSE'; id: number | null }
7
+ | { type: 'REMOVE'; id: number }
8
+ | { type: 'UPDATE'; id: number; options: Partial<SideOptions> };
9
+
10
+ export const SideSheetReducer = (
11
+ state: SideStackItem[],
12
+ action: Action
13
+ ): SideStackItem[] => {
14
+ switch (action.type) {
15
+ case 'OPEN':
16
+ return [...state, action.payload];
17
+ case 'SET_OPEN':
18
+ return state.map(item =>
19
+ item.id === action.id ? { ...item, state: 'open' } : item
20
+ );
21
+ case 'CLOSE':
22
+ return state.map(item =>
23
+ action.id === null || item.id === action.id
24
+ ? { ...item, state: 'closing' }
25
+ : item
26
+ );
27
+ case 'REMOVE':
28
+ return state.filter(item => item.id !== action.id);
29
+ case 'UPDATE':
30
+ return state.map(item =>
31
+ item.id === action.id
32
+ ? {
33
+ ...item,
34
+ options: { ...item.options, ...action.options },
35
+ }
36
+ : item
37
+ );
38
+ default:
39
+ return state;
40
+ }
41
+ };
@@ -0,0 +1,11 @@
1
+ import { useContext } from 'react';
2
+ import { SideSheetContextValue } from '../types';
3
+ import { SideSheetContext } from '../components/SideSheetProvider';
4
+
5
+ export const useSideSheet = (): SideSheetContextValue => {
6
+ const context = useContext(SideSheetContext);
7
+ if (!context) {
8
+ throw new Error('useSideSheet must be used within SideSheetProvider');
9
+ }
10
+ return context;
11
+ };
package/src/index.css ADDED
@@ -0,0 +1,191 @@
1
+ .sidesheet-overlay {
2
+ opacity: 0.3;
3
+ transition: all 0.4s;
4
+ background: #000;
5
+ bottom: 0;
6
+ left: 0;
7
+ position: fixed;
8
+ right: 0;
9
+ top: 0;
10
+ visibility: visible;
11
+ z-index: 10000;
12
+ }
13
+
14
+ .sidesheet-right {
15
+ right: 0;
16
+ transform: translateX(100%);
17
+ }
18
+
19
+ .sidesheet-left {
20
+ left: 0;
21
+ transform: translateX(-100%);
22
+ }
23
+
24
+ .sidesheet-right.sidesheet-animation-open {
25
+ animation: sidesheet-open-right 240ms cubic-bezier(0, 0, 0.3, 1);
26
+ }
27
+
28
+ .sidesheet-left.sidesheet-animation-open {
29
+ animation: sidesheet-open-left 240ms cubic-bezier(0, 0, 0.3, 1);
30
+ }
31
+
32
+ .sidesheet.sidesheet-animation-open {
33
+ transform: translateX(0);
34
+ }
35
+
36
+ .sidesheet-right.sidesheet-animation-closing {
37
+ animation: sidesheet-close-right 240ms cubic-bezier(0, 0, 0.3, 1);
38
+ }
39
+
40
+ .sidesheet-left.sidesheet-animation-closing {
41
+ animation: sidesheet-close-left 240ms cubic-bezier(0, 0, 0.3, 1);
42
+ }
43
+
44
+ @keyframes sidesheet-open-right {
45
+ from {
46
+ transform: translateX(100%);
47
+ }
48
+ to {
49
+ transform: translateX(0);
50
+ }
51
+ }
52
+
53
+ @keyframes sidesheet-close-right {
54
+ from {
55
+ transform: translateX(0);
56
+ }
57
+ to {
58
+ transform: translateX(100%);
59
+ }
60
+ }
61
+
62
+ @keyframes sidesheet-open-left {
63
+ from {
64
+ transform: translateX(-100%);
65
+ }
66
+ to {
67
+ transform: translateX(0);
68
+ }
69
+ }
70
+
71
+ @keyframes sidesheet-close-left {
72
+ from {
73
+ transform: translateX(0);
74
+ }
75
+ to {
76
+ transform: translateX(-100%);
77
+ }
78
+ }
79
+
80
+ .sidesheet {
81
+ flex-direction: column;
82
+ display: flex;
83
+ flex-wrap: nowrap;
84
+ background: #f1f3f4;
85
+ bottom: 0;
86
+ right: 0;
87
+ max-width: 100%;
88
+ overflow-x: hidden;
89
+ overflow-y: auto;
90
+ position: fixed;
91
+ z-index: 10000;
92
+ top: 0;
93
+ width: 100%;
94
+ box-shadow: 0 0 10px -5px rgba(0, 0, 0, 0.2), 0 0 24px 2px rgba(0, 0, 0, 0.14),
95
+ 0 0 30px 5px rgba(0, 0, 0, 0.12);
96
+ transition: transform 0.3s ease, width 0.3s ease;
97
+ }
98
+
99
+ .sheet-white {
100
+ background: #fff;
101
+ }
102
+
103
+ .sidesheet-header .sidesheet-header-btn {
104
+ background: transparent;
105
+ border: none;
106
+ cursor: pointer;
107
+ padding: 7px;
108
+ }
109
+
110
+ .sidesheet-header {
111
+ padding: 12px;
112
+ gap: 16px;
113
+ border-bottom: 1px solid #dadce0;
114
+ background: #fff;
115
+ display: flex;
116
+ justify-content: space-between;
117
+ align-items: center;
118
+ }
119
+
120
+ .sidesheet-header svg {
121
+ width: 24px;
122
+ height: 24px;
123
+ display: block;
124
+ }
125
+
126
+ .sidesheet-header .sidesheet-header-btn {
127
+ border-radius: 50%;
128
+ height: 40px;
129
+ line-height: 40px;
130
+ align-items: center;
131
+ width: 40px;
132
+ justify-content: center;
133
+ display: flex;
134
+ opacity: 0.8;
135
+ }
136
+
137
+ .sidesheet-header .sidesheet-header-btn:hover {
138
+ opacity: 1;
139
+ }
140
+
141
+ .sidesheet-header .sidesheet-header-btn:focus {
142
+ background-color: rgba(64, 64, 64, 0.12);
143
+ }
144
+
145
+ .sidesheet-header-close:hover {
146
+ cursor: pointer;
147
+ }
148
+
149
+ .sidesheet-header-title {
150
+ font-size: 20px;
151
+ font-weight: 400;
152
+ align-items: center;
153
+ display: flex;
154
+ flex: 1 1 auto;
155
+ overflow: hidden;
156
+ text-overflow: ellipsis;
157
+ white-space: nowrap;
158
+ padding: 6px 0;
159
+ }
160
+
161
+ .sidesheet-content.sidesheet-centered {
162
+ margin: 0 auto;
163
+ max-width: 768px;
164
+ width: 100%;
165
+ }
166
+
167
+ .sidesheet-content {
168
+ flex: 1 1 auto;
169
+ overflow-y: auto;
170
+ position: relative;
171
+ }
172
+
173
+ .sidesheet-content.sidesheet-padding {
174
+ padding: 24px;
175
+ }
176
+
177
+ .sidesheet-card {
178
+ background: #fff;
179
+ border-radius: 8px;
180
+ padding: 24px;
181
+ box-shadow: 0 0 0 1px #dadce0;
182
+ }
183
+
184
+ .sidesheet-footer {
185
+ padding: 16px;
186
+ border-top: 1px solid #dadce0;
187
+ background: #fff;
188
+ display: flex;
189
+ justify-content: space-between;
190
+ align-items: center;
191
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { SideSheetHeader } from './components/SideSheetHeader';
2
+ import { SideSheetContent } from './components/SideSheetContent';
3
+ import { SideSheetFooter } from './components/SideSheetFooter';
4
+ import { SideSheetProvider } from './components/SideSheetProvider';
5
+
6
+ export { useSideSheet } from './hooks/useSideSheet';
7
+ export * from './types';
8
+
9
+ export const SideSheet = {
10
+ Provider: SideSheetProvider,
11
+ Header: SideSheetHeader,
12
+ Content: SideSheetContent,
13
+ Footer: SideSheetFooter,
14
+ };
@@ -0,0 +1,45 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export type Sides = 'left' | 'right';
4
+
5
+ export interface SideSheetOptions {
6
+ side: Sides;
7
+ mountStrategy: 'all' | 'top-only';
8
+ }
9
+
10
+ export interface SideOptions {
11
+ width?: number;
12
+ className?: string;
13
+ confirmBeforeClose?: boolean;
14
+ confirmMessage?: string;
15
+ confirmCallback?: (message: string) => Promise<boolean>;
16
+ closeOnOverlayClick?: boolean;
17
+ closeOnEsc?: boolean;
18
+ animationDuration?: number;
19
+ onOpen?: (id: number) => void;
20
+ onClose?: (id: number) => void;
21
+ }
22
+
23
+ export interface SideElementProps {
24
+ sideId: number;
25
+ close: (id: number | null) => Promise<void>;
26
+ open: (element: SideElement, options?: SideOptions) => number;
27
+ update: (id: number, options: SideOptions) => void;
28
+ options: SideOptions;
29
+ }
30
+
31
+ export type SideElement = (props: SideElementProps) => ReactNode;
32
+
33
+ export interface SideStackItem {
34
+ id: number;
35
+ element: SideElement;
36
+ options: Required<SideOptions>;
37
+ state: 'opening' | 'open' | 'closing';
38
+ }
39
+
40
+ export interface SideSheetContextValue {
41
+ open: (el: SideElement, opts?: SideOptions) => number;
42
+ close: (id: number | null) => Promise<void>;
43
+ update: (id: number, opts: SideOptions) => void;
44
+ config: SideSheetOptions;
45
+ }