react-slide-panel 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/README.md +96 -0
- package/dist/index.css +221 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +235 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +214 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +217 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# react-slide-panel
|
|
2
|
+
|
|
3
|
+
A React component that renders a **slide-out drawer** (or sidebar panel) from any edge of the screen—right, left, top, or bottom. It uses a modal overlay, optional scroll lock, and supports header, body, and footer slots. Written in TypeScript. Port of [vue3-side-panel](https://github.com/headmandev/vue3-side-panel).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install react-slide-panel
|
|
9
|
+
# or
|
|
10
|
+
yarn add react-slide-panel
|
|
11
|
+
# or
|
|
12
|
+
pnpm add react-slide-panel
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Peer dependencies:** `react` and `react-dom` (>= 16.8.0).
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Import the component and the default styles:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { SlidePanel } from 'react-slide-panel';
|
|
23
|
+
import 'react-slide-panel/dist/styles.css';
|
|
24
|
+
|
|
25
|
+
function App() {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<button onClick={() => setOpen(true)}>Open drawer</button>
|
|
31
|
+
<SlidePanel
|
|
32
|
+
open={open}
|
|
33
|
+
onOpenChange={setOpen}
|
|
34
|
+
side="right"
|
|
35
|
+
width="400px"
|
|
36
|
+
header={<h2>Title</h2>}
|
|
37
|
+
footer={<button onClick={() => setOpen(false)}>Done</button>}
|
|
38
|
+
>
|
|
39
|
+
<p>Drawer content goes here.</p>
|
|
40
|
+
</SlidePanel>
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Open/close is **controlled** via the `open` and `onOpenChange` props. The drawer is rendered in a portal and can slide in from `top`, `right`, `bottom`, or `left`.
|
|
47
|
+
|
|
48
|
+
A **demo app** in the repo (`npm run demo` from the root) shows more examples (all sides, header/footer, scroll lock, custom width, etc.).
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### SlidePanel
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Description |
|
|
55
|
+
|------|------|---------|-------------|
|
|
56
|
+
| `open` | `boolean` | required | Controlled open state |
|
|
57
|
+
| `onOpenChange` | `(open: boolean) => void` | required | Called when open state should change |
|
|
58
|
+
| `onClosed` | `() => void` | — | Called after leave transition finishes |
|
|
59
|
+
| `onOpened` | `() => void` | — | Called after enter transition finishes |
|
|
60
|
+
| `idName` | `string` | `'rsp-container'` | Id for the portal container |
|
|
61
|
+
| `hideCloseBtn` | `boolean` | `false` | Hide the default close button |
|
|
62
|
+
| `noClose` | `boolean` | `false` | Prevent closing on overlay click |
|
|
63
|
+
| `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'right'` | Side the drawer slides from |
|
|
64
|
+
| `rerender` | `boolean` | `false` | Unmount content when closed (remount on open) |
|
|
65
|
+
| `zIndex` | `number \| 'auto'` | `'auto'` | z-index (auto = max on page) |
|
|
66
|
+
| `width` | `string` | `'auto'` | Drawer width (left/right) |
|
|
67
|
+
| `height` | `string` | `'auto'` | Drawer height (top/bottom) |
|
|
68
|
+
| `lockScroll` | `boolean` | `false` | Lock body scroll when open |
|
|
69
|
+
| `lockScrollHtml` | `boolean` | `true` | Set `overflow: hidden` on `html` when locking |
|
|
70
|
+
| `overlayColor` | `string` | `'black'` | Overlay background color |
|
|
71
|
+
| `overlayOpacity` | `number` | `0.5` | Overlay opacity (0–1) |
|
|
72
|
+
| `overlayDuration` | `number` | `500` | Overlay transition duration (ms) |
|
|
73
|
+
| `panelColor` | `string` | `'white'` | Drawer background color |
|
|
74
|
+
| `panelDuration` | `number` | `300` | Drawer transition duration (ms) |
|
|
75
|
+
| `transitionName` | `string` | `slide-{side}` | Custom transition class name |
|
|
76
|
+
| `headerClass` | `string` | `''` | Class for header container |
|
|
77
|
+
| `bodyClass` | `string` | `''` | Class for body container |
|
|
78
|
+
| `footerClass` | `string` | `''` | Class for footer container |
|
|
79
|
+
| `header` | `ReactNode` | — | Header content |
|
|
80
|
+
| `footer` | `ReactNode` | — | Footer content |
|
|
81
|
+
| `children` | `ReactNode` | — | Body content |
|
|
82
|
+
| `className` | `string` | — | Extra class for panel root |
|
|
83
|
+
| `style` | `CSSProperties` | — | Inline styles for panel root |
|
|
84
|
+
|
|
85
|
+
### SidePanelCloseButton
|
|
86
|
+
|
|
87
|
+
Optional close button component (used by default inside the drawer).
|
|
88
|
+
|
|
89
|
+
| Prop | Type | Description |
|
|
90
|
+
|------|------|-------------|
|
|
91
|
+
| `onClose` | `() => void` | Called when the button is clicked |
|
|
92
|
+
| `className` | `string` | Optional class name |
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/* src/styles.css */
|
|
2
|
+
.rsp-wrapper .rsp-overlay {
|
|
3
|
+
position: fixed;
|
|
4
|
+
top: 0;
|
|
5
|
+
left: 0;
|
|
6
|
+
width: 100%;
|
|
7
|
+
height: 100%;
|
|
8
|
+
}
|
|
9
|
+
.rsp-wrapper .rsp {
|
|
10
|
+
position: fixed;
|
|
11
|
+
}
|
|
12
|
+
.rsp-wrapper .rsp--right-side,
|
|
13
|
+
.rsp-wrapper .rsp--left-side {
|
|
14
|
+
top: 0;
|
|
15
|
+
height: 100%;
|
|
16
|
+
}
|
|
17
|
+
.rsp-wrapper .rsp--right-side {
|
|
18
|
+
right: 0;
|
|
19
|
+
left: unset;
|
|
20
|
+
}
|
|
21
|
+
.rsp-wrapper .rsp--left-side {
|
|
22
|
+
right: unset;
|
|
23
|
+
left: 0;
|
|
24
|
+
}
|
|
25
|
+
.rsp-wrapper .rsp--bottom-side,
|
|
26
|
+
.rsp-wrapper .rsp--top-side {
|
|
27
|
+
left: 0;
|
|
28
|
+
width: 100%;
|
|
29
|
+
}
|
|
30
|
+
.rsp-wrapper .rsp--bottom-side {
|
|
31
|
+
bottom: 0;
|
|
32
|
+
}
|
|
33
|
+
.rsp-wrapper .rsp--top-side {
|
|
34
|
+
top: 0;
|
|
35
|
+
}
|
|
36
|
+
.rsp-wrapper .rsp__header,
|
|
37
|
+
.rsp-wrapper .rsp__body,
|
|
38
|
+
.rsp-wrapper .rsp__footer {
|
|
39
|
+
overflow: auto;
|
|
40
|
+
}
|
|
41
|
+
.rsp-wrapper .rsp__body {
|
|
42
|
+
position: relative;
|
|
43
|
+
}
|
|
44
|
+
.rsp-wrapper .overlay-enter-active {
|
|
45
|
+
animation: rsp-overlay-in forwards;
|
|
46
|
+
}
|
|
47
|
+
.rsp-wrapper .overlay-leave-active {
|
|
48
|
+
animation: rsp-overlay-out forwards;
|
|
49
|
+
}
|
|
50
|
+
@keyframes rsp-overlay-in {
|
|
51
|
+
0% {
|
|
52
|
+
opacity: 0;
|
|
53
|
+
}
|
|
54
|
+
100% {
|
|
55
|
+
opacity: var(--overlay-opacity, 0.5);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
@keyframes rsp-overlay-out {
|
|
59
|
+
0% {
|
|
60
|
+
opacity: var(--overlay-opacity, 0.5);
|
|
61
|
+
}
|
|
62
|
+
100% {
|
|
63
|
+
opacity: 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
.rsp-wrapper .slide-right-enter-active {
|
|
67
|
+
animation: rsp-slide-right-in forwards;
|
|
68
|
+
}
|
|
69
|
+
.rsp-wrapper .slide-right-leave-active {
|
|
70
|
+
animation: rsp-slide-right-out forwards;
|
|
71
|
+
}
|
|
72
|
+
@keyframes rsp-slide-right-in {
|
|
73
|
+
0% {
|
|
74
|
+
transform: translateX(100%);
|
|
75
|
+
opacity: 0;
|
|
76
|
+
}
|
|
77
|
+
100% {
|
|
78
|
+
transform: translateX(0);
|
|
79
|
+
opacity: 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
@keyframes rsp-slide-right-out {
|
|
83
|
+
0% {
|
|
84
|
+
transform: translateX(0);
|
|
85
|
+
opacity: 1;
|
|
86
|
+
}
|
|
87
|
+
100% {
|
|
88
|
+
transform: translateX(100%);
|
|
89
|
+
opacity: 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
.rsp-wrapper .slide-left-enter-active {
|
|
93
|
+
animation: rsp-slide-left-in forwards;
|
|
94
|
+
}
|
|
95
|
+
.rsp-wrapper .slide-left-leave-active {
|
|
96
|
+
animation: rsp-slide-left-out forwards;
|
|
97
|
+
}
|
|
98
|
+
@keyframes rsp-slide-left-in {
|
|
99
|
+
0% {
|
|
100
|
+
transform: translateX(-100%);
|
|
101
|
+
opacity: 0;
|
|
102
|
+
}
|
|
103
|
+
100% {
|
|
104
|
+
transform: translateX(0);
|
|
105
|
+
opacity: 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
@keyframes rsp-slide-left-out {
|
|
109
|
+
0% {
|
|
110
|
+
transform: translateX(0);
|
|
111
|
+
opacity: 1;
|
|
112
|
+
}
|
|
113
|
+
100% {
|
|
114
|
+
transform: translateX(-100%);
|
|
115
|
+
opacity: 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
.rsp-wrapper .slide-top-enter-active {
|
|
119
|
+
animation: rsp-slide-top-in forwards;
|
|
120
|
+
}
|
|
121
|
+
.rsp-wrapper .slide-top-leave-active {
|
|
122
|
+
animation: rsp-slide-top-out forwards;
|
|
123
|
+
}
|
|
124
|
+
@keyframes rsp-slide-top-in {
|
|
125
|
+
0% {
|
|
126
|
+
transform: translateY(-100%);
|
|
127
|
+
opacity: 0;
|
|
128
|
+
}
|
|
129
|
+
100% {
|
|
130
|
+
transform: translateY(0);
|
|
131
|
+
opacity: 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
@keyframes rsp-slide-top-out {
|
|
135
|
+
0% {
|
|
136
|
+
transform: translateY(0);
|
|
137
|
+
opacity: 1;
|
|
138
|
+
}
|
|
139
|
+
100% {
|
|
140
|
+
transform: translateY(-100%);
|
|
141
|
+
opacity: 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
.rsp-wrapper .slide-bottom-enter-active {
|
|
145
|
+
animation: rsp-slide-bottom-in forwards;
|
|
146
|
+
}
|
|
147
|
+
.rsp-wrapper .slide-bottom-leave-active {
|
|
148
|
+
animation: rsp-slide-bottom-out forwards;
|
|
149
|
+
}
|
|
150
|
+
@keyframes rsp-slide-bottom-in {
|
|
151
|
+
0% {
|
|
152
|
+
transform: translateY(100%);
|
|
153
|
+
opacity: 0;
|
|
154
|
+
}
|
|
155
|
+
100% {
|
|
156
|
+
transform: translateY(0);
|
|
157
|
+
opacity: 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
@keyframes rsp-slide-bottom-out {
|
|
161
|
+
0% {
|
|
162
|
+
transform: translateY(0);
|
|
163
|
+
opacity: 1;
|
|
164
|
+
}
|
|
165
|
+
100% {
|
|
166
|
+
transform: translateY(100%);
|
|
167
|
+
opacity: 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
.rsp-close {
|
|
171
|
+
position: absolute;
|
|
172
|
+
top: 24px;
|
|
173
|
+
right: 24px;
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: center;
|
|
177
|
+
width: 36px;
|
|
178
|
+
height: 36px;
|
|
179
|
+
padding: 0;
|
|
180
|
+
margin: 0;
|
|
181
|
+
font: inherit;
|
|
182
|
+
color: inherit;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
background: white;
|
|
185
|
+
border: none;
|
|
186
|
+
border-radius: 50%;
|
|
187
|
+
transition: background-color 0.1s;
|
|
188
|
+
-webkit-tap-highlight-color: transparent;
|
|
189
|
+
}
|
|
190
|
+
.rsp-close:hover {
|
|
191
|
+
background: #f7f7f7;
|
|
192
|
+
}
|
|
193
|
+
.rsp-close__x {
|
|
194
|
+
position: relative;
|
|
195
|
+
left: -1px;
|
|
196
|
+
display: block;
|
|
197
|
+
width: 20px;
|
|
198
|
+
height: 20px;
|
|
199
|
+
}
|
|
200
|
+
.rsp-close__x::before,
|
|
201
|
+
.rsp-close__x::after {
|
|
202
|
+
position: absolute;
|
|
203
|
+
width: 2px;
|
|
204
|
+
height: 20px;
|
|
205
|
+
content: " ";
|
|
206
|
+
background-color: #777;
|
|
207
|
+
}
|
|
208
|
+
.rsp-close__x::before {
|
|
209
|
+
transform: rotate(-45deg);
|
|
210
|
+
}
|
|
211
|
+
.rsp-close__x::after {
|
|
212
|
+
transform: rotate(45deg);
|
|
213
|
+
}
|
|
214
|
+
.rsp-close:active {
|
|
215
|
+
top: 23px;
|
|
216
|
+
right: 23px;
|
|
217
|
+
width: 38px;
|
|
218
|
+
height: 38px;
|
|
219
|
+
background: #f1f1f1;
|
|
220
|
+
}
|
|
221
|
+
/*# sourceMappingURL=index.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/styles.css"],"sourcesContent":[".rsp-wrapper .rsp-overlay {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.rsp-wrapper .rsp {\n position: fixed;\n}\n\n.rsp-wrapper .rsp--right-side,\n.rsp-wrapper .rsp--left-side {\n top: 0;\n height: 100%;\n}\n\n.rsp-wrapper .rsp--right-side {\n right: 0;\n left: unset;\n}\n\n.rsp-wrapper .rsp--left-side {\n right: unset;\n left: 0;\n}\n\n.rsp-wrapper .rsp--bottom-side,\n.rsp-wrapper .rsp--top-side {\n left: 0;\n width: 100%;\n}\n\n.rsp-wrapper .rsp--bottom-side {\n bottom: 0;\n}\n\n.rsp-wrapper .rsp--top-side {\n top: 0;\n}\n\n.rsp-wrapper .rsp__header,\n.rsp-wrapper .rsp__body,\n.rsp-wrapper .rsp__footer {\n overflow: auto;\n}\n\n.rsp-wrapper .rsp__body {\n position: relative;\n}\n\n/* =========================================\n OVERLAY ANIMATIONS\n ========================================= */\n.rsp-wrapper .overlay-enter-active {\n animation: rsp-overlay-in forwards;\n}\n\n.rsp-wrapper .overlay-leave-active {\n animation: rsp-overlay-out forwards;\n}\n\n@keyframes rsp-overlay-in {\n 0% { opacity: 0; }\n 100% { opacity: var(--overlay-opacity, 0.5); }\n}\n\n@keyframes rsp-overlay-out {\n 0% { opacity: var(--overlay-opacity, 0.5); }\n 100% { opacity: 0; }\n}\n\n/* =========================================\n SLIDE RIGHT\n ========================================= */\n.rsp-wrapper .slide-right-enter-active {\n animation: rsp-slide-right-in forwards;\n}\n\n.rsp-wrapper .slide-right-leave-active {\n animation: rsp-slide-right-out forwards;\n}\n\n@keyframes rsp-slide-right-in {\n 0% { transform: translateX(100%); opacity: 0; }\n 100% { transform: translateX(0); opacity: 1; }\n}\n\n@keyframes rsp-slide-right-out {\n 0% { transform: translateX(0); opacity: 1; }\n 100% { transform: translateX(100%); opacity: 0; }\n}\n\n/* =========================================\n SLIDE LEFT\n ========================================= */\n.rsp-wrapper .slide-left-enter-active {\n animation: rsp-slide-left-in forwards;\n}\n\n.rsp-wrapper .slide-left-leave-active {\n animation: rsp-slide-left-out forwards;\n}\n\n@keyframes rsp-slide-left-in {\n 0% { transform: translateX(-100%); opacity: 0; }\n 100% { transform: translateX(0); opacity: 1; }\n}\n\n@keyframes rsp-slide-left-out {\n 0% { transform: translateX(0); opacity: 1; }\n 100% { transform: translateX(-100%); opacity: 0; }\n}\n\n/* =========================================\n SLIDE TOP\n ========================================= */\n.rsp-wrapper .slide-top-enter-active {\n animation: rsp-slide-top-in forwards;\n}\n\n.rsp-wrapper .slide-top-leave-active {\n animation: rsp-slide-top-out forwards;\n}\n\n@keyframes rsp-slide-top-in {\n 0% { transform: translateY(-100%); opacity: 0; }\n 100% { transform: translateY(0); opacity: 1; }\n}\n\n@keyframes rsp-slide-top-out {\n 0% { transform: translateY(0); opacity: 1; }\n 100% { transform: translateY(-100%); opacity: 0; }\n}\n\n/* =========================================\n SLIDE BOTTOM\n ========================================= */\n.rsp-wrapper .slide-bottom-enter-active {\n animation: rsp-slide-bottom-in forwards;\n}\n\n.rsp-wrapper .slide-bottom-leave-active {\n animation: rsp-slide-bottom-out forwards;\n}\n\n@keyframes rsp-slide-bottom-in {\n 0% { transform: translateY(100%); opacity: 0; }\n 100% { transform: translateY(0); opacity: 1; }\n}\n\n@keyframes rsp-slide-bottom-out {\n 0% { transform: translateY(0); opacity: 1; }\n 100% { transform: translateY(100%); opacity: 0; }\n}\n\n/* =========================================\n CLOSE BUTTON\n ========================================= */\n.rsp-close {\n position: absolute;\n top: 24px;\n right: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n margin: 0;\n font: inherit;\n color: inherit;\n cursor: pointer;\n background: white;\n border: none;\n border-radius: 50%;\n transition: background-color 0.1s;\n -webkit-tap-highlight-color: transparent;\n}\n\n.rsp-close:hover {\n background: #f7f7f7;\n}\n\n.rsp-close__x {\n position: relative;\n left: -1px;\n display: block;\n width: 20px;\n height: 20px;\n}\n\n.rsp-close__x::before,\n.rsp-close__x::after {\n position: absolute;\n width: 2px;\n height: 20px;\n content: ' ';\n background-color: #777;\n}\n\n.rsp-close__x::before {\n transform: rotate(-45deg);\n}\n\n.rsp-close__x::after {\n transform: rotate(45deg);\n}\n\n.rsp-close:active {\n top: 23px;\n right: 23px;\n width: 38px;\n height: 38px;\n background: #f1f1f1;\n}"],"mappings":"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","names":[]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
type SlidePanelSide = 'top' | 'right' | 'bottom' | 'left';
|
|
5
|
+
interface SlidePanelProps {
|
|
6
|
+
/** Whether the panel is open. */
|
|
7
|
+
open: boolean;
|
|
8
|
+
/** Callback when open state should change (e.g. user closes). Pass the new boolean to update controlled state. */
|
|
9
|
+
onOpenChange: (open: boolean) => void;
|
|
10
|
+
/** Called when the close transition finishes and the panel is hidden. */
|
|
11
|
+
onClosed?: () => void;
|
|
12
|
+
/** Called when the open transition finishes and the panel is visible. */
|
|
13
|
+
onOpened?: () => void;
|
|
14
|
+
/** ID of the portal container element. Default: `'rsp-container'`. */
|
|
15
|
+
idName?: string;
|
|
16
|
+
/** Hide the default close button. */
|
|
17
|
+
hideCloseBtn?: boolean;
|
|
18
|
+
/** Disable closing by clicking the overlay. */
|
|
19
|
+
noClose?: boolean;
|
|
20
|
+
/** Which side the panel slides from. Default: `'right'`. */
|
|
21
|
+
side?: SlidePanelSide;
|
|
22
|
+
/** When true, unmount panel when closed and remount when opened. Default: `true`. */
|
|
23
|
+
rerender?: boolean;
|
|
24
|
+
/** z-index of overlay and panel. `'auto'` uses a high default. */
|
|
25
|
+
zIndex?: number | 'auto';
|
|
26
|
+
/** Panel width (e.g. `'500px'`). Only for side `'left'` or `'right'`. */
|
|
27
|
+
width?: string;
|
|
28
|
+
/** Panel height (e.g. `'500px'`). Only for side `'top'` or `'bottom'`. */
|
|
29
|
+
height?: string;
|
|
30
|
+
/** Lock body scroll when the panel is open. */
|
|
31
|
+
lockScroll?: boolean;
|
|
32
|
+
/** When lockScroll is true, also set overflow hidden on html. Default: `true`. */
|
|
33
|
+
lockScrollHtml?: boolean;
|
|
34
|
+
/** Overlay background color (any valid CSS color). Default: `'black'`. */
|
|
35
|
+
overlayColor?: string;
|
|
36
|
+
/** Overlay opacity (0–1). Default: `0.5`. */
|
|
37
|
+
overlayOpacity?: number;
|
|
38
|
+
/** Overlay transition duration in ms. Default: `500`. */
|
|
39
|
+
overlayDuration?: number;
|
|
40
|
+
/** Panel background color. Default: `'white'`. */
|
|
41
|
+
panelColor?: string;
|
|
42
|
+
/** Panel transition duration in ms. Default: `300`. */
|
|
43
|
+
panelDuration?: number;
|
|
44
|
+
/** Animation class name: `slide-right` | `slide-left` | `slide-top` | `slide-bottom`, or custom. Default picks from `side`. */
|
|
45
|
+
transitionName?: string;
|
|
46
|
+
/** Class for the header container. */
|
|
47
|
+
headerClass?: string;
|
|
48
|
+
/** Class for the scrollable body container. */
|
|
49
|
+
bodyClass?: string;
|
|
50
|
+
/** Class for the footer container. */
|
|
51
|
+
footerClass?: string;
|
|
52
|
+
/** Optional fixed header content. */
|
|
53
|
+
header?: React.ReactNode;
|
|
54
|
+
/** Optional fixed footer content. */
|
|
55
|
+
footer?: React.ReactNode;
|
|
56
|
+
/** Panel body content. */
|
|
57
|
+
children?: React.ReactNode;
|
|
58
|
+
/** Additional class for the panel element. */
|
|
59
|
+
className?: string;
|
|
60
|
+
/** Inline styles for the panel element. */
|
|
61
|
+
style?: React.CSSProperties;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A modal slide panel that slides in from the chosen edge (top, right, bottom, left).
|
|
65
|
+
* Renders via a portal and supports overlay, scroll lock, and custom header/footer.
|
|
66
|
+
*/
|
|
67
|
+
declare function SlidePanel({ open, onOpenChange, onClosed, onOpened, idName, hideCloseBtn, noClose, side, rerender, zIndex: zIndexProp, width, height, lockScroll, lockScrollHtml, overlayColor, overlayOpacity, overlayDuration, panelColor, panelDuration, transitionName, headerClass, bodyClass, footerClass, header, footer, children, className, style: styleProp, }: SlidePanelProps): React.ReactPortal | null;
|
|
68
|
+
declare namespace SlidePanel {
|
|
69
|
+
var displayName: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface SidePanelCloseButtonProps {
|
|
73
|
+
onClose: () => void;
|
|
74
|
+
className?: string;
|
|
75
|
+
}
|
|
76
|
+
declare function SidePanelCloseButton({ onClose, className }: SidePanelCloseButtonProps): react_jsx_runtime.JSX.Element;
|
|
77
|
+
|
|
78
|
+
export { SidePanelCloseButton, type SidePanelCloseButtonProps, SlidePanel, type SlidePanelProps, type SlidePanelSide };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
type SlidePanelSide = 'top' | 'right' | 'bottom' | 'left';
|
|
5
|
+
interface SlidePanelProps {
|
|
6
|
+
/** Whether the panel is open. */
|
|
7
|
+
open: boolean;
|
|
8
|
+
/** Callback when open state should change (e.g. user closes). Pass the new boolean to update controlled state. */
|
|
9
|
+
onOpenChange: (open: boolean) => void;
|
|
10
|
+
/** Called when the close transition finishes and the panel is hidden. */
|
|
11
|
+
onClosed?: () => void;
|
|
12
|
+
/** Called when the open transition finishes and the panel is visible. */
|
|
13
|
+
onOpened?: () => void;
|
|
14
|
+
/** ID of the portal container element. Default: `'rsp-container'`. */
|
|
15
|
+
idName?: string;
|
|
16
|
+
/** Hide the default close button. */
|
|
17
|
+
hideCloseBtn?: boolean;
|
|
18
|
+
/** Disable closing by clicking the overlay. */
|
|
19
|
+
noClose?: boolean;
|
|
20
|
+
/** Which side the panel slides from. Default: `'right'`. */
|
|
21
|
+
side?: SlidePanelSide;
|
|
22
|
+
/** When true, unmount panel when closed and remount when opened. Default: `true`. */
|
|
23
|
+
rerender?: boolean;
|
|
24
|
+
/** z-index of overlay and panel. `'auto'` uses a high default. */
|
|
25
|
+
zIndex?: number | 'auto';
|
|
26
|
+
/** Panel width (e.g. `'500px'`). Only for side `'left'` or `'right'`. */
|
|
27
|
+
width?: string;
|
|
28
|
+
/** Panel height (e.g. `'500px'`). Only for side `'top'` or `'bottom'`. */
|
|
29
|
+
height?: string;
|
|
30
|
+
/** Lock body scroll when the panel is open. */
|
|
31
|
+
lockScroll?: boolean;
|
|
32
|
+
/** When lockScroll is true, also set overflow hidden on html. Default: `true`. */
|
|
33
|
+
lockScrollHtml?: boolean;
|
|
34
|
+
/** Overlay background color (any valid CSS color). Default: `'black'`. */
|
|
35
|
+
overlayColor?: string;
|
|
36
|
+
/** Overlay opacity (0–1). Default: `0.5`. */
|
|
37
|
+
overlayOpacity?: number;
|
|
38
|
+
/** Overlay transition duration in ms. Default: `500`. */
|
|
39
|
+
overlayDuration?: number;
|
|
40
|
+
/** Panel background color. Default: `'white'`. */
|
|
41
|
+
panelColor?: string;
|
|
42
|
+
/** Panel transition duration in ms. Default: `300`. */
|
|
43
|
+
panelDuration?: number;
|
|
44
|
+
/** Animation class name: `slide-right` | `slide-left` | `slide-top` | `slide-bottom`, or custom. Default picks from `side`. */
|
|
45
|
+
transitionName?: string;
|
|
46
|
+
/** Class for the header container. */
|
|
47
|
+
headerClass?: string;
|
|
48
|
+
/** Class for the scrollable body container. */
|
|
49
|
+
bodyClass?: string;
|
|
50
|
+
/** Class for the footer container. */
|
|
51
|
+
footerClass?: string;
|
|
52
|
+
/** Optional fixed header content. */
|
|
53
|
+
header?: React.ReactNode;
|
|
54
|
+
/** Optional fixed footer content. */
|
|
55
|
+
footer?: React.ReactNode;
|
|
56
|
+
/** Panel body content. */
|
|
57
|
+
children?: React.ReactNode;
|
|
58
|
+
/** Additional class for the panel element. */
|
|
59
|
+
className?: string;
|
|
60
|
+
/** Inline styles for the panel element. */
|
|
61
|
+
style?: React.CSSProperties;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A modal slide panel that slides in from the chosen edge (top, right, bottom, left).
|
|
65
|
+
* Renders via a portal and supports overlay, scroll lock, and custom header/footer.
|
|
66
|
+
*/
|
|
67
|
+
declare function SlidePanel({ open, onOpenChange, onClosed, onOpened, idName, hideCloseBtn, noClose, side, rerender, zIndex: zIndexProp, width, height, lockScroll, lockScrollHtml, overlayColor, overlayOpacity, overlayDuration, panelColor, panelDuration, transitionName, headerClass, bodyClass, footerClass, header, footer, children, className, style: styleProp, }: SlidePanelProps): React.ReactPortal | null;
|
|
68
|
+
declare namespace SlidePanel {
|
|
69
|
+
var displayName: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface SidePanelCloseButtonProps {
|
|
73
|
+
onClose: () => void;
|
|
74
|
+
className?: string;
|
|
75
|
+
}
|
|
76
|
+
declare function SidePanelCloseButton({ onClose, className }: SidePanelCloseButtonProps): react_jsx_runtime.JSX.Element;
|
|
77
|
+
|
|
78
|
+
export { SidePanelCloseButton, type SidePanelCloseButtonProps, SlidePanel, type SlidePanelProps, type SlidePanelSide };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
SidePanelCloseButton: () => SidePanelCloseButton,
|
|
24
|
+
SlidePanel: () => SlidePanel
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/SlidePanel.tsx
|
|
29
|
+
var import_react = require("react");
|
|
30
|
+
var import_react_dom = require("react-dom");
|
|
31
|
+
var import_body_scroll_lock = require("body-scroll-lock");
|
|
32
|
+
|
|
33
|
+
// src/SlidePanelCloseButton.tsx
|
|
34
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
35
|
+
function SidePanelCloseButton({ onClose, className }) {
|
|
36
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
37
|
+
"button",
|
|
38
|
+
{
|
|
39
|
+
type: "button",
|
|
40
|
+
className: className ? `rsp-close ${className}` : "rsp-close",
|
|
41
|
+
onClick: onClose,
|
|
42
|
+
"aria-label": "Close panel",
|
|
43
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "rsp-close__x" })
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/SlidePanel.tsx
|
|
49
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
50
|
+
function SlidePanel({
|
|
51
|
+
open,
|
|
52
|
+
onOpenChange,
|
|
53
|
+
onClosed,
|
|
54
|
+
onOpened,
|
|
55
|
+
idName = "rsp-container",
|
|
56
|
+
hideCloseBtn = false,
|
|
57
|
+
noClose = false,
|
|
58
|
+
side = "right",
|
|
59
|
+
rerender = true,
|
|
60
|
+
zIndex: zIndexProp = "auto",
|
|
61
|
+
width = "auto",
|
|
62
|
+
height = "auto",
|
|
63
|
+
lockScroll = false,
|
|
64
|
+
lockScrollHtml = true,
|
|
65
|
+
overlayColor = "black",
|
|
66
|
+
overlayOpacity = 0.5,
|
|
67
|
+
overlayDuration = 500,
|
|
68
|
+
panelColor = "white",
|
|
69
|
+
panelDuration = 300,
|
|
70
|
+
transitionName,
|
|
71
|
+
headerClass = "",
|
|
72
|
+
bodyClass = "",
|
|
73
|
+
footerClass = "",
|
|
74
|
+
header,
|
|
75
|
+
footer,
|
|
76
|
+
children,
|
|
77
|
+
className = "",
|
|
78
|
+
style: styleProp
|
|
79
|
+
}) {
|
|
80
|
+
const [isMounted, setIsMounted] = (0, import_react.useState)(open);
|
|
81
|
+
const [isExiting, setIsExiting] = (0, import_react.useState)(false);
|
|
82
|
+
const [portalContainer, setPortalContainer] = (0, import_react.useState)(null);
|
|
83
|
+
const panelRef = (0, import_react.useRef)(null);
|
|
84
|
+
const containerCreatedByUsRef = (0, import_react.useRef)(false);
|
|
85
|
+
const transition = transitionName ?? `slide-${side}`;
|
|
86
|
+
const callbacks = (0, import_react.useRef)({ onOpened, onClosed });
|
|
87
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
88
|
+
callbacks.current = { onOpened, onClosed };
|
|
89
|
+
});
|
|
90
|
+
(0, import_react.useEffect)(() => {
|
|
91
|
+
let timeoutId;
|
|
92
|
+
if (open) {
|
|
93
|
+
setIsMounted(true);
|
|
94
|
+
setIsExiting(false);
|
|
95
|
+
timeoutId = setTimeout(() => {
|
|
96
|
+
callbacks.current.onOpened?.();
|
|
97
|
+
}, Math.max(overlayDuration, panelDuration));
|
|
98
|
+
} else if (isMounted) {
|
|
99
|
+
setIsExiting(true);
|
|
100
|
+
timeoutId = setTimeout(() => {
|
|
101
|
+
setIsMounted(false);
|
|
102
|
+
setIsExiting(false);
|
|
103
|
+
callbacks.current.onClosed?.();
|
|
104
|
+
}, Math.max(overlayDuration, panelDuration));
|
|
105
|
+
}
|
|
106
|
+
return () => clearTimeout(timeoutId);
|
|
107
|
+
}, [open, isMounted, overlayDuration, panelDuration]);
|
|
108
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
109
|
+
if (typeof document === "undefined") return;
|
|
110
|
+
let el = document.getElementById(idName);
|
|
111
|
+
if (!el) {
|
|
112
|
+
el = document.createElement("div");
|
|
113
|
+
el.setAttribute("id", idName);
|
|
114
|
+
document.body.appendChild(el);
|
|
115
|
+
containerCreatedByUsRef.current = true;
|
|
116
|
+
}
|
|
117
|
+
setPortalContainer(el);
|
|
118
|
+
return () => {
|
|
119
|
+
if (containerCreatedByUsRef.current && el?.parentNode) {
|
|
120
|
+
document.body.removeChild(el);
|
|
121
|
+
containerCreatedByUsRef.current = false;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}, [idName]);
|
|
125
|
+
const originalHtmlOverflowRef = (0, import_react.useRef)(null);
|
|
126
|
+
const wasAlreadyLockedRef = (0, import_react.useRef)(false);
|
|
127
|
+
(0, import_react.useEffect)(() => {
|
|
128
|
+
const el = panelRef.current;
|
|
129
|
+
const shouldLock = open && lockScroll && el;
|
|
130
|
+
if (shouldLock) {
|
|
131
|
+
const htmlOverflow = document.documentElement.style.overflow;
|
|
132
|
+
const bodyOverflow = document.body.style.overflow;
|
|
133
|
+
wasAlreadyLockedRef.current = htmlOverflow === "hidden" || bodyOverflow === "hidden";
|
|
134
|
+
originalHtmlOverflowRef.current = htmlOverflow;
|
|
135
|
+
(0, import_body_scroll_lock.disableBodyScroll)(el, { reserveScrollBarGap: true });
|
|
136
|
+
if (lockScrollHtml && !wasAlreadyLockedRef.current) {
|
|
137
|
+
document.documentElement.style.overflow = "hidden";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return () => {
|
|
141
|
+
if (shouldLock && el) {
|
|
142
|
+
(0, import_body_scroll_lock.enableBodyScroll)(el);
|
|
143
|
+
if (lockScrollHtml && !wasAlreadyLockedRef.current) {
|
|
144
|
+
if (originalHtmlOverflowRef.current) {
|
|
145
|
+
document.documentElement.style.overflow = originalHtmlOverflowRef.current;
|
|
146
|
+
} else {
|
|
147
|
+
document.documentElement.style.removeProperty("overflow");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}, [open, lockScroll, lockScrollHtml]);
|
|
153
|
+
const closePanel = (0, import_react.useCallback)(() => {
|
|
154
|
+
onOpenChange(false);
|
|
155
|
+
}, [onOpenChange]);
|
|
156
|
+
const overlayStyles = (0, import_react.useMemo)(
|
|
157
|
+
() => ({
|
|
158
|
+
zIndex: zIndexProp === "auto" ? 9999 : zIndexProp,
|
|
159
|
+
animationDuration: `${overlayDuration}ms`,
|
|
160
|
+
["--overlay-opacity"]: overlayOpacity,
|
|
161
|
+
opacity: isExiting ? 0 : overlayOpacity,
|
|
162
|
+
pointerEvents: isExiting ? "none" : "auto",
|
|
163
|
+
backgroundColor: overlayColor
|
|
164
|
+
}),
|
|
165
|
+
[zIndexProp, overlayDuration, overlayOpacity, overlayColor, isExiting]
|
|
166
|
+
);
|
|
167
|
+
const panelStyles = (0, import_react.useMemo)(() => {
|
|
168
|
+
const base = {
|
|
169
|
+
zIndex: zIndexProp === "auto" ? 9999 : zIndexProp,
|
|
170
|
+
backgroundColor: panelColor,
|
|
171
|
+
animationDuration: `${panelDuration}ms`,
|
|
172
|
+
display: "flex",
|
|
173
|
+
flexDirection: "column",
|
|
174
|
+
maxWidth: "100%",
|
|
175
|
+
...styleProp
|
|
176
|
+
};
|
|
177
|
+
if (side === "left" || side === "right") {
|
|
178
|
+
base.width = width;
|
|
179
|
+
base.height = "100%";
|
|
180
|
+
} else {
|
|
181
|
+
base.height = height;
|
|
182
|
+
base.width = "100%";
|
|
183
|
+
base.maxHeight = "100vh";
|
|
184
|
+
}
|
|
185
|
+
return base;
|
|
186
|
+
}, [zIndexProp, panelColor, panelDuration, side, width, height, styleProp]);
|
|
187
|
+
const isCompletelyHidden = !isMounted;
|
|
188
|
+
if (!portalContainer || isCompletelyHidden && rerender) return null;
|
|
189
|
+
const overlayTransitionClass = isExiting ? "overlay-leave-active" : "overlay-enter-active";
|
|
190
|
+
const panelTransitionClass = isExiting ? `${transition}-leave-active` : `${transition}-enter-active`;
|
|
191
|
+
const panelContent = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
192
|
+
"div",
|
|
193
|
+
{
|
|
194
|
+
ref: panelRef,
|
|
195
|
+
className: `rsp rsp--${side}-side ${panelTransitionClass} ${className}`.trim(),
|
|
196
|
+
style: panelStyles,
|
|
197
|
+
children: [
|
|
198
|
+
header != null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: [headerClass, "rsp__header"].filter(Boolean).join(" "), style: { flexShrink: 0 }, children: header }),
|
|
199
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: [bodyClass, "rsp__body"].filter(Boolean).join(" "), style: { flexGrow: 1, overflowY: "auto" }, children: [
|
|
200
|
+
children,
|
|
201
|
+
!hideCloseBtn && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SidePanelCloseButton, { onClose: closePanel })
|
|
202
|
+
] }),
|
|
203
|
+
footer != null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: [footerClass, "rsp__footer"].filter(Boolean).join(" "), style: { flexShrink: 0 }, children: footer })
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
const content = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
208
|
+
"div",
|
|
209
|
+
{
|
|
210
|
+
className: `rsp-wrapper${!isCompletelyHidden ? " rsp-wrapper--active" : ""}`,
|
|
211
|
+
style: { display: isCompletelyHidden ? "none" : "block" },
|
|
212
|
+
children: [
|
|
213
|
+
!isCompletelyHidden && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
214
|
+
"div",
|
|
215
|
+
{
|
|
216
|
+
className: `rsp-overlay ${overlayTransitionClass}`,
|
|
217
|
+
style: overlayStyles,
|
|
218
|
+
onClick: () => !noClose && closePanel(),
|
|
219
|
+
role: "presentation",
|
|
220
|
+
"aria-hidden": "true"
|
|
221
|
+
}
|
|
222
|
+
),
|
|
223
|
+
panelContent
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
return (0, import_react_dom.createPortal)(content, portalContainer);
|
|
228
|
+
}
|
|
229
|
+
SlidePanel.displayName = "SlidePanel";
|
|
230
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
231
|
+
0 && (module.exports = {
|
|
232
|
+
SidePanelCloseButton,
|
|
233
|
+
SlidePanel
|
|
234
|
+
});
|
|
235
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/SlidePanel.tsx","../src/SlidePanelCloseButton.tsx"],"sourcesContent":["export { SlidePanel } from './SlidePanel';\nexport type { SlidePanelProps, SlidePanelSide } from './SlidePanel';\nexport { SidePanelCloseButton } from './SlidePanelCloseButton';\nexport type { SidePanelCloseButtonProps } from './SlidePanelCloseButton';\n","import React, {\n useRef,\n useEffect,\n useState,\n useCallback,\n useLayoutEffect,\n useMemo,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';\nimport { SidePanelCloseButton } from './SlidePanelCloseButton';\nimport './styles.css';\n\nexport type SlidePanelSide = 'top' | 'right' | 'bottom' | 'left';\n\nexport interface SlidePanelProps {\n /** Whether the panel is open. */\n open: boolean;\n /** Callback when open state should change (e.g. user closes). Pass the new boolean to update controlled state. */\n onOpenChange: (open: boolean) => void;\n /** Called when the close transition finishes and the panel is hidden. */\n onClosed?: () => void;\n /** Called when the open transition finishes and the panel is visible. */\n onOpened?: () => void;\n /** ID of the portal container element. Default: `'rsp-container'`. */\n idName?: string;\n /** Hide the default close button. */\n hideCloseBtn?: boolean;\n /** Disable closing by clicking the overlay. */\n noClose?: boolean;\n /** Which side the panel slides from. Default: `'right'`. */\n side?: SlidePanelSide;\n /** When true, unmount panel when closed and remount when opened. Default: `true`. */\n rerender?: boolean;\n /** z-index of overlay and panel. `'auto'` uses a high default. */\n zIndex?: number | 'auto';\n /** Panel width (e.g. `'500px'`). Only for side `'left'` or `'right'`. */\n width?: string;\n /** Panel height (e.g. `'500px'`). Only for side `'top'` or `'bottom'`. */\n height?: string;\n /** Lock body scroll when the panel is open. */\n lockScroll?: boolean;\n /** When lockScroll is true, also set overflow hidden on html. Default: `true`. */\n lockScrollHtml?: boolean;\n /** Overlay background color (any valid CSS color). Default: `'black'`. */\n overlayColor?: string;\n /** Overlay opacity (0–1). Default: `0.5`. */\n overlayOpacity?: number;\n /** Overlay transition duration in ms. Default: `500`. */\n overlayDuration?: number;\n /** Panel background color. Default: `'white'`. */\n panelColor?: string;\n /** Panel transition duration in ms. Default: `300`. */\n panelDuration?: number;\n /** Animation class name: `slide-right` | `slide-left` | `slide-top` | `slide-bottom`, or custom. Default picks from `side`. */\n transitionName?: string;\n /** Class for the header container. */\n headerClass?: string;\n /** Class for the scrollable body container. */\n bodyClass?: string;\n /** Class for the footer container. */\n footerClass?: string;\n /** Optional fixed header content. */\n header?: React.ReactNode;\n /** Optional fixed footer content. */\n footer?: React.ReactNode;\n /** Panel body content. */\n children?: React.ReactNode;\n /** Additional class for the panel element. */\n className?: string;\n /** Inline styles for the panel element. */\n style?: React.CSSProperties;\n}\n\n/**\n * A modal slide panel that slides in from the chosen edge (top, right, bottom, left).\n * Renders via a portal and supports overlay, scroll lock, and custom header/footer.\n */\nexport function SlidePanel({\n open,\n onOpenChange,\n onClosed,\n onOpened,\n idName = 'rsp-container',\n hideCloseBtn = false,\n noClose = false,\n side = 'right',\n rerender = true,\n zIndex: zIndexProp = 'auto',\n width = 'auto',\n height = 'auto',\n lockScroll = false,\n lockScrollHtml = true,\n overlayColor = 'black',\n overlayOpacity = 0.5,\n overlayDuration = 500,\n panelColor = 'white',\n panelDuration = 300,\n transitionName,\n headerClass = '',\n bodyClass = '',\n footerClass = '',\n header,\n footer,\n children,\n className = '',\n style: styleProp,\n}: SlidePanelProps) {\n const [isMounted, setIsMounted] = useState(open);\n const [isExiting, setIsExiting] = useState(false);\n const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);\n\n const panelRef = useRef<HTMLDivElement | null>(null);\n const containerCreatedByUsRef = useRef(false);\n\n const transition = transitionName ?? `slide-${side}`;\n\n const callbacks = useRef({ onOpened, onClosed });\n useLayoutEffect(() => {\n callbacks.current = { onOpened, onClosed };\n });\n\n // 1. Manage Mount & Exit Animation States\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout>;\n\n if (open) {\n setIsMounted(true);\n setIsExiting(false);\n timeoutId = setTimeout(() => {\n callbacks.current.onOpened?.();\n }, Math.max(overlayDuration, panelDuration));\n } else if (isMounted) {\n setIsExiting(true);\n timeoutId = setTimeout(() => {\n setIsMounted(false);\n setIsExiting(false);\n callbacks.current.onClosed?.();\n }, Math.max(overlayDuration, panelDuration));\n }\n\n return () => clearTimeout(timeoutId);\n }, [open, isMounted, overlayDuration, panelDuration]);\n\n // 2. Safely Create/Destroy Portal Container\n useLayoutEffect(() => {\n if (typeof document === 'undefined') return;\n let el = document.getElementById(idName);\n if (!el) {\n el = document.createElement('div');\n el.setAttribute('id', idName);\n document.body.appendChild(el);\n containerCreatedByUsRef.current = true;\n }\n setPortalContainer(el);\n return () => {\n if (containerCreatedByUsRef.current && el?.parentNode) {\n document.body.removeChild(el);\n containerCreatedByUsRef.current = false;\n }\n };\n }, [idName]);\n\n // 3. Robust Scroll Locking (Nested Modal Support)\n const originalHtmlOverflowRef = useRef<string | null>(null);\n const wasAlreadyLockedRef = useRef(false);\n\n useEffect(() => {\n const el = panelRef.current;\n const shouldLock = open && lockScroll && el;\n\n if (shouldLock) {\n // Record the exact state of the page *before* we apply our locks\n const htmlOverflow = document.documentElement.style.overflow;\n const bodyOverflow = document.body.style.overflow;\n \n wasAlreadyLockedRef.current = htmlOverflow === 'hidden' || bodyOverflow === 'hidden';\n originalHtmlOverflowRef.current = htmlOverflow;\n\n disableBodyScroll(el, { reserveScrollBarGap: true });\n \n // Only force HTML overflow if the page wasn't already locked by something else\n if (lockScrollHtml && !wasAlreadyLockedRef.current) {\n document.documentElement.style.overflow = 'hidden';\n }\n }\n\n return () => {\n if (shouldLock && el) {\n enableBodyScroll(el);\n \n // Only restore the manual HTML overflow if *we* were the ones who hid it\n if (lockScrollHtml && !wasAlreadyLockedRef.current) {\n if (originalHtmlOverflowRef.current) {\n document.documentElement.style.overflow = originalHtmlOverflowRef.current;\n } else {\n document.documentElement.style.removeProperty('overflow');\n }\n }\n }\n };\n }, [open, lockScroll, lockScrollHtml]);\n\n const closePanel = useCallback(() => {\n onOpenChange(false);\n }, [onOpenChange]);\n\n // 4. Styles matched perfectly to the CSS Keyframes\n const overlayStyles: React.CSSProperties = useMemo(\n () => ({\n zIndex: zIndexProp === 'auto' ? 9999 : zIndexProp,\n animationDuration: `${overlayDuration}ms`,\n ['--overlay-opacity' as string]: overlayOpacity,\n opacity: isExiting ? 0 : overlayOpacity,\n pointerEvents: isExiting ? 'none' : 'auto', \n backgroundColor: overlayColor,\n }),\n [zIndexProp, overlayDuration, overlayOpacity, overlayColor, isExiting]\n );\n\n const panelStyles: React.CSSProperties = useMemo(() => {\n const base: React.CSSProperties = {\n zIndex: zIndexProp === 'auto' ? 9999 : zIndexProp,\n backgroundColor: panelColor,\n animationDuration: `${panelDuration}ms`,\n display: 'flex', \n flexDirection: 'column',\n maxWidth: '100%',\n ...styleProp,\n };\n\n if (side === 'left' || side === 'right') {\n base.width = width;\n base.height = '100%';\n } else {\n base.height = height;\n base.width = '100%';\n base.maxHeight = '100vh';\n }\n return base;\n }, [zIndexProp, panelColor, panelDuration, side, width, height, styleProp]);\n\n const isCompletelyHidden = !isMounted;\n if (!portalContainer || (isCompletelyHidden && rerender)) return null;\n\n const overlayTransitionClass = isExiting ? 'overlay-leave-active' : 'overlay-enter-active';\n const panelTransitionClass = isExiting ? `${transition}-leave-active` : `${transition}-enter-active`;\n\n const panelContent = (\n <div\n ref={panelRef}\n className={`rsp rsp--${side}-side ${panelTransitionClass} ${className}`.trim()}\n style={panelStyles}\n >\n {header != null && (\n <div className={[headerClass, 'rsp__header'].filter(Boolean).join(' ')} style={{ flexShrink: 0 }}>\n {header}\n </div>\n )}\n \n <div className={[bodyClass, 'rsp__body'].filter(Boolean).join(' ')} style={{ flexGrow: 1, overflowY: 'auto' }}>\n {children}\n {!hideCloseBtn && <SidePanelCloseButton onClose={closePanel} />}\n </div>\n\n {footer != null && (\n <div className={[footerClass, 'rsp__footer'].filter(Boolean).join(' ')} style={{ flexShrink: 0 }}>\n {footer}\n </div>\n )}\n </div>\n );\n\n const content = (\n <div \n className={`rsp-wrapper${!isCompletelyHidden ? ' rsp-wrapper--active' : ''}`}\n style={{ display: isCompletelyHidden ? 'none' : 'block' }}\n >\n {!isCompletelyHidden && (\n <div\n className={`rsp-overlay ${overlayTransitionClass}`}\n style={overlayStyles}\n onClick={() => !noClose && closePanel()}\n role=\"presentation\"\n aria-hidden=\"true\"\n />\n )}\n {panelContent}\n </div>\n );\n\n return createPortal(content, portalContainer);\n}\n\nSlidePanel.displayName = 'SlidePanel';","export interface SidePanelCloseButtonProps {\n onClose: () => void;\n className?: string;\n}\n\nexport function SidePanelCloseButton({ onClose, className }: SidePanelCloseButtonProps) {\n return (\n <button\n type=\"button\"\n className={className ? `rsp-close ${className}` : 'rsp-close'}\n onClick={onClose}\n aria-label=\"Close panel\"\n >\n <span className=\"rsp-close__x\" />\n </button>\n );\n}\n"],"mappings":"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","names":["import_jsx_runtime"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// src/SlidePanel.tsx
|
|
2
|
+
import {
|
|
3
|
+
useRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useState,
|
|
6
|
+
useCallback,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useMemo
|
|
9
|
+
} from "react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
11
|
+
import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock";
|
|
12
|
+
|
|
13
|
+
// src/SlidePanelCloseButton.tsx
|
|
14
|
+
import { jsx } from "react/jsx-runtime";
|
|
15
|
+
function SidePanelCloseButton({ onClose, className }) {
|
|
16
|
+
return /* @__PURE__ */ jsx(
|
|
17
|
+
"button",
|
|
18
|
+
{
|
|
19
|
+
type: "button",
|
|
20
|
+
className: className ? `rsp-close ${className}` : "rsp-close",
|
|
21
|
+
onClick: onClose,
|
|
22
|
+
"aria-label": "Close panel",
|
|
23
|
+
children: /* @__PURE__ */ jsx("span", { className: "rsp-close__x" })
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/SlidePanel.tsx
|
|
29
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
30
|
+
function SlidePanel({
|
|
31
|
+
open,
|
|
32
|
+
onOpenChange,
|
|
33
|
+
onClosed,
|
|
34
|
+
onOpened,
|
|
35
|
+
idName = "rsp-container",
|
|
36
|
+
hideCloseBtn = false,
|
|
37
|
+
noClose = false,
|
|
38
|
+
side = "right",
|
|
39
|
+
rerender = true,
|
|
40
|
+
zIndex: zIndexProp = "auto",
|
|
41
|
+
width = "auto",
|
|
42
|
+
height = "auto",
|
|
43
|
+
lockScroll = false,
|
|
44
|
+
lockScrollHtml = true,
|
|
45
|
+
overlayColor = "black",
|
|
46
|
+
overlayOpacity = 0.5,
|
|
47
|
+
overlayDuration = 500,
|
|
48
|
+
panelColor = "white",
|
|
49
|
+
panelDuration = 300,
|
|
50
|
+
transitionName,
|
|
51
|
+
headerClass = "",
|
|
52
|
+
bodyClass = "",
|
|
53
|
+
footerClass = "",
|
|
54
|
+
header,
|
|
55
|
+
footer,
|
|
56
|
+
children,
|
|
57
|
+
className = "",
|
|
58
|
+
style: styleProp
|
|
59
|
+
}) {
|
|
60
|
+
const [isMounted, setIsMounted] = useState(open);
|
|
61
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
62
|
+
const [portalContainer, setPortalContainer] = useState(null);
|
|
63
|
+
const panelRef = useRef(null);
|
|
64
|
+
const containerCreatedByUsRef = useRef(false);
|
|
65
|
+
const transition = transitionName ?? `slide-${side}`;
|
|
66
|
+
const callbacks = useRef({ onOpened, onClosed });
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
callbacks.current = { onOpened, onClosed };
|
|
69
|
+
});
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
let timeoutId;
|
|
72
|
+
if (open) {
|
|
73
|
+
setIsMounted(true);
|
|
74
|
+
setIsExiting(false);
|
|
75
|
+
timeoutId = setTimeout(() => {
|
|
76
|
+
callbacks.current.onOpened?.();
|
|
77
|
+
}, Math.max(overlayDuration, panelDuration));
|
|
78
|
+
} else if (isMounted) {
|
|
79
|
+
setIsExiting(true);
|
|
80
|
+
timeoutId = setTimeout(() => {
|
|
81
|
+
setIsMounted(false);
|
|
82
|
+
setIsExiting(false);
|
|
83
|
+
callbacks.current.onClosed?.();
|
|
84
|
+
}, Math.max(overlayDuration, panelDuration));
|
|
85
|
+
}
|
|
86
|
+
return () => clearTimeout(timeoutId);
|
|
87
|
+
}, [open, isMounted, overlayDuration, panelDuration]);
|
|
88
|
+
useLayoutEffect(() => {
|
|
89
|
+
if (typeof document === "undefined") return;
|
|
90
|
+
let el = document.getElementById(idName);
|
|
91
|
+
if (!el) {
|
|
92
|
+
el = document.createElement("div");
|
|
93
|
+
el.setAttribute("id", idName);
|
|
94
|
+
document.body.appendChild(el);
|
|
95
|
+
containerCreatedByUsRef.current = true;
|
|
96
|
+
}
|
|
97
|
+
setPortalContainer(el);
|
|
98
|
+
return () => {
|
|
99
|
+
if (containerCreatedByUsRef.current && el?.parentNode) {
|
|
100
|
+
document.body.removeChild(el);
|
|
101
|
+
containerCreatedByUsRef.current = false;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}, [idName]);
|
|
105
|
+
const originalHtmlOverflowRef = useRef(null);
|
|
106
|
+
const wasAlreadyLockedRef = useRef(false);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const el = panelRef.current;
|
|
109
|
+
const shouldLock = open && lockScroll && el;
|
|
110
|
+
if (shouldLock) {
|
|
111
|
+
const htmlOverflow = document.documentElement.style.overflow;
|
|
112
|
+
const bodyOverflow = document.body.style.overflow;
|
|
113
|
+
wasAlreadyLockedRef.current = htmlOverflow === "hidden" || bodyOverflow === "hidden";
|
|
114
|
+
originalHtmlOverflowRef.current = htmlOverflow;
|
|
115
|
+
disableBodyScroll(el, { reserveScrollBarGap: true });
|
|
116
|
+
if (lockScrollHtml && !wasAlreadyLockedRef.current) {
|
|
117
|
+
document.documentElement.style.overflow = "hidden";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return () => {
|
|
121
|
+
if (shouldLock && el) {
|
|
122
|
+
enableBodyScroll(el);
|
|
123
|
+
if (lockScrollHtml && !wasAlreadyLockedRef.current) {
|
|
124
|
+
if (originalHtmlOverflowRef.current) {
|
|
125
|
+
document.documentElement.style.overflow = originalHtmlOverflowRef.current;
|
|
126
|
+
} else {
|
|
127
|
+
document.documentElement.style.removeProperty("overflow");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}, [open, lockScroll, lockScrollHtml]);
|
|
133
|
+
const closePanel = useCallback(() => {
|
|
134
|
+
onOpenChange(false);
|
|
135
|
+
}, [onOpenChange]);
|
|
136
|
+
const overlayStyles = useMemo(
|
|
137
|
+
() => ({
|
|
138
|
+
zIndex: zIndexProp === "auto" ? 9999 : zIndexProp,
|
|
139
|
+
animationDuration: `${overlayDuration}ms`,
|
|
140
|
+
["--overlay-opacity"]: overlayOpacity,
|
|
141
|
+
opacity: isExiting ? 0 : overlayOpacity,
|
|
142
|
+
pointerEvents: isExiting ? "none" : "auto",
|
|
143
|
+
backgroundColor: overlayColor
|
|
144
|
+
}),
|
|
145
|
+
[zIndexProp, overlayDuration, overlayOpacity, overlayColor, isExiting]
|
|
146
|
+
);
|
|
147
|
+
const panelStyles = useMemo(() => {
|
|
148
|
+
const base = {
|
|
149
|
+
zIndex: zIndexProp === "auto" ? 9999 : zIndexProp,
|
|
150
|
+
backgroundColor: panelColor,
|
|
151
|
+
animationDuration: `${panelDuration}ms`,
|
|
152
|
+
display: "flex",
|
|
153
|
+
flexDirection: "column",
|
|
154
|
+
maxWidth: "100%",
|
|
155
|
+
...styleProp
|
|
156
|
+
};
|
|
157
|
+
if (side === "left" || side === "right") {
|
|
158
|
+
base.width = width;
|
|
159
|
+
base.height = "100%";
|
|
160
|
+
} else {
|
|
161
|
+
base.height = height;
|
|
162
|
+
base.width = "100%";
|
|
163
|
+
base.maxHeight = "100vh";
|
|
164
|
+
}
|
|
165
|
+
return base;
|
|
166
|
+
}, [zIndexProp, panelColor, panelDuration, side, width, height, styleProp]);
|
|
167
|
+
const isCompletelyHidden = !isMounted;
|
|
168
|
+
if (!portalContainer || isCompletelyHidden && rerender) return null;
|
|
169
|
+
const overlayTransitionClass = isExiting ? "overlay-leave-active" : "overlay-enter-active";
|
|
170
|
+
const panelTransitionClass = isExiting ? `${transition}-leave-active` : `${transition}-enter-active`;
|
|
171
|
+
const panelContent = /* @__PURE__ */ jsxs(
|
|
172
|
+
"div",
|
|
173
|
+
{
|
|
174
|
+
ref: panelRef,
|
|
175
|
+
className: `rsp rsp--${side}-side ${panelTransitionClass} ${className}`.trim(),
|
|
176
|
+
style: panelStyles,
|
|
177
|
+
children: [
|
|
178
|
+
header != null && /* @__PURE__ */ jsx2("div", { className: [headerClass, "rsp__header"].filter(Boolean).join(" "), style: { flexShrink: 0 }, children: header }),
|
|
179
|
+
/* @__PURE__ */ jsxs("div", { className: [bodyClass, "rsp__body"].filter(Boolean).join(" "), style: { flexGrow: 1, overflowY: "auto" }, children: [
|
|
180
|
+
children,
|
|
181
|
+
!hideCloseBtn && /* @__PURE__ */ jsx2(SidePanelCloseButton, { onClose: closePanel })
|
|
182
|
+
] }),
|
|
183
|
+
footer != null && /* @__PURE__ */ jsx2("div", { className: [footerClass, "rsp__footer"].filter(Boolean).join(" "), style: { flexShrink: 0 }, children: footer })
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
const content = /* @__PURE__ */ jsxs(
|
|
188
|
+
"div",
|
|
189
|
+
{
|
|
190
|
+
className: `rsp-wrapper${!isCompletelyHidden ? " rsp-wrapper--active" : ""}`,
|
|
191
|
+
style: { display: isCompletelyHidden ? "none" : "block" },
|
|
192
|
+
children: [
|
|
193
|
+
!isCompletelyHidden && /* @__PURE__ */ jsx2(
|
|
194
|
+
"div",
|
|
195
|
+
{
|
|
196
|
+
className: `rsp-overlay ${overlayTransitionClass}`,
|
|
197
|
+
style: overlayStyles,
|
|
198
|
+
onClick: () => !noClose && closePanel(),
|
|
199
|
+
role: "presentation",
|
|
200
|
+
"aria-hidden": "true"
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
panelContent
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
return createPortal(content, portalContainer);
|
|
208
|
+
}
|
|
209
|
+
SlidePanel.displayName = "SlidePanel";
|
|
210
|
+
export {
|
|
211
|
+
SidePanelCloseButton,
|
|
212
|
+
SlidePanel
|
|
213
|
+
};
|
|
214
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/SlidePanel.tsx","../src/SlidePanelCloseButton.tsx"],"sourcesContent":["import React, {\n useRef,\n useEffect,\n useState,\n useCallback,\n useLayoutEffect,\n useMemo,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';\nimport { SidePanelCloseButton } from './SlidePanelCloseButton';\nimport './styles.css';\n\nexport type SlidePanelSide = 'top' | 'right' | 'bottom' | 'left';\n\nexport interface SlidePanelProps {\n /** Whether the panel is open. */\n open: boolean;\n /** Callback when open state should change (e.g. user closes). Pass the new boolean to update controlled state. */\n onOpenChange: (open: boolean) => void;\n /** Called when the close transition finishes and the panel is hidden. */\n onClosed?: () => void;\n /** Called when the open transition finishes and the panel is visible. */\n onOpened?: () => void;\n /** ID of the portal container element. Default: `'rsp-container'`. */\n idName?: string;\n /** Hide the default close button. */\n hideCloseBtn?: boolean;\n /** Disable closing by clicking the overlay. */\n noClose?: boolean;\n /** Which side the panel slides from. Default: `'right'`. */\n side?: SlidePanelSide;\n /** When true, unmount panel when closed and remount when opened. Default: `true`. */\n rerender?: boolean;\n /** z-index of overlay and panel. `'auto'` uses a high default. */\n zIndex?: number | 'auto';\n /** Panel width (e.g. `'500px'`). Only for side `'left'` or `'right'`. */\n width?: string;\n /** Panel height (e.g. `'500px'`). Only for side `'top'` or `'bottom'`. */\n height?: string;\n /** Lock body scroll when the panel is open. */\n lockScroll?: boolean;\n /** When lockScroll is true, also set overflow hidden on html. Default: `true`. */\n lockScrollHtml?: boolean;\n /** Overlay background color (any valid CSS color). Default: `'black'`. */\n overlayColor?: string;\n /** Overlay opacity (0–1). Default: `0.5`. */\n overlayOpacity?: number;\n /** Overlay transition duration in ms. Default: `500`. */\n overlayDuration?: number;\n /** Panel background color. Default: `'white'`. */\n panelColor?: string;\n /** Panel transition duration in ms. Default: `300`. */\n panelDuration?: number;\n /** Animation class name: `slide-right` | `slide-left` | `slide-top` | `slide-bottom`, or custom. Default picks from `side`. */\n transitionName?: string;\n /** Class for the header container. */\n headerClass?: string;\n /** Class for the scrollable body container. */\n bodyClass?: string;\n /** Class for the footer container. */\n footerClass?: string;\n /** Optional fixed header content. */\n header?: React.ReactNode;\n /** Optional fixed footer content. */\n footer?: React.ReactNode;\n /** Panel body content. */\n children?: React.ReactNode;\n /** Additional class for the panel element. */\n className?: string;\n /** Inline styles for the panel element. */\n style?: React.CSSProperties;\n}\n\n/**\n * A modal slide panel that slides in from the chosen edge (top, right, bottom, left).\n * Renders via a portal and supports overlay, scroll lock, and custom header/footer.\n */\nexport function SlidePanel({\n open,\n onOpenChange,\n onClosed,\n onOpened,\n idName = 'rsp-container',\n hideCloseBtn = false,\n noClose = false,\n side = 'right',\n rerender = true,\n zIndex: zIndexProp = 'auto',\n width = 'auto',\n height = 'auto',\n lockScroll = false,\n lockScrollHtml = true,\n overlayColor = 'black',\n overlayOpacity = 0.5,\n overlayDuration = 500,\n panelColor = 'white',\n panelDuration = 300,\n transitionName,\n headerClass = '',\n bodyClass = '',\n footerClass = '',\n header,\n footer,\n children,\n className = '',\n style: styleProp,\n}: SlidePanelProps) {\n const [isMounted, setIsMounted] = useState(open);\n const [isExiting, setIsExiting] = useState(false);\n const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);\n\n const panelRef = useRef<HTMLDivElement | null>(null);\n const containerCreatedByUsRef = useRef(false);\n\n const transition = transitionName ?? `slide-${side}`;\n\n const callbacks = useRef({ onOpened, onClosed });\n useLayoutEffect(() => {\n callbacks.current = { onOpened, onClosed };\n });\n\n // 1. Manage Mount & Exit Animation States\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout>;\n\n if (open) {\n setIsMounted(true);\n setIsExiting(false);\n timeoutId = setTimeout(() => {\n callbacks.current.onOpened?.();\n }, Math.max(overlayDuration, panelDuration));\n } else if (isMounted) {\n setIsExiting(true);\n timeoutId = setTimeout(() => {\n setIsMounted(false);\n setIsExiting(false);\n callbacks.current.onClosed?.();\n }, Math.max(overlayDuration, panelDuration));\n }\n\n return () => clearTimeout(timeoutId);\n }, [open, isMounted, overlayDuration, panelDuration]);\n\n // 2. Safely Create/Destroy Portal Container\n useLayoutEffect(() => {\n if (typeof document === 'undefined') return;\n let el = document.getElementById(idName);\n if (!el) {\n el = document.createElement('div');\n el.setAttribute('id', idName);\n document.body.appendChild(el);\n containerCreatedByUsRef.current = true;\n }\n setPortalContainer(el);\n return () => {\n if (containerCreatedByUsRef.current && el?.parentNode) {\n document.body.removeChild(el);\n containerCreatedByUsRef.current = false;\n }\n };\n }, [idName]);\n\n // 3. Robust Scroll Locking (Nested Modal Support)\n const originalHtmlOverflowRef = useRef<string | null>(null);\n const wasAlreadyLockedRef = useRef(false);\n\n useEffect(() => {\n const el = panelRef.current;\n const shouldLock = open && lockScroll && el;\n\n if (shouldLock) {\n // Record the exact state of the page *before* we apply our locks\n const htmlOverflow = document.documentElement.style.overflow;\n const bodyOverflow = document.body.style.overflow;\n \n wasAlreadyLockedRef.current = htmlOverflow === 'hidden' || bodyOverflow === 'hidden';\n originalHtmlOverflowRef.current = htmlOverflow;\n\n disableBodyScroll(el, { reserveScrollBarGap: true });\n \n // Only force HTML overflow if the page wasn't already locked by something else\n if (lockScrollHtml && !wasAlreadyLockedRef.current) {\n document.documentElement.style.overflow = 'hidden';\n }\n }\n\n return () => {\n if (shouldLock && el) {\n enableBodyScroll(el);\n \n // Only restore the manual HTML overflow if *we* were the ones who hid it\n if (lockScrollHtml && !wasAlreadyLockedRef.current) {\n if (originalHtmlOverflowRef.current) {\n document.documentElement.style.overflow = originalHtmlOverflowRef.current;\n } else {\n document.documentElement.style.removeProperty('overflow');\n }\n }\n }\n };\n }, [open, lockScroll, lockScrollHtml]);\n\n const closePanel = useCallback(() => {\n onOpenChange(false);\n }, [onOpenChange]);\n\n // 4. Styles matched perfectly to the CSS Keyframes\n const overlayStyles: React.CSSProperties = useMemo(\n () => ({\n zIndex: zIndexProp === 'auto' ? 9999 : zIndexProp,\n animationDuration: `${overlayDuration}ms`,\n ['--overlay-opacity' as string]: overlayOpacity,\n opacity: isExiting ? 0 : overlayOpacity,\n pointerEvents: isExiting ? 'none' : 'auto', \n backgroundColor: overlayColor,\n }),\n [zIndexProp, overlayDuration, overlayOpacity, overlayColor, isExiting]\n );\n\n const panelStyles: React.CSSProperties = useMemo(() => {\n const base: React.CSSProperties = {\n zIndex: zIndexProp === 'auto' ? 9999 : zIndexProp,\n backgroundColor: panelColor,\n animationDuration: `${panelDuration}ms`,\n display: 'flex', \n flexDirection: 'column',\n maxWidth: '100%',\n ...styleProp,\n };\n\n if (side === 'left' || side === 'right') {\n base.width = width;\n base.height = '100%';\n } else {\n base.height = height;\n base.width = '100%';\n base.maxHeight = '100vh';\n }\n return base;\n }, [zIndexProp, panelColor, panelDuration, side, width, height, styleProp]);\n\n const isCompletelyHidden = !isMounted;\n if (!portalContainer || (isCompletelyHidden && rerender)) return null;\n\n const overlayTransitionClass = isExiting ? 'overlay-leave-active' : 'overlay-enter-active';\n const panelTransitionClass = isExiting ? `${transition}-leave-active` : `${transition}-enter-active`;\n\n const panelContent = (\n <div\n ref={panelRef}\n className={`rsp rsp--${side}-side ${panelTransitionClass} ${className}`.trim()}\n style={panelStyles}\n >\n {header != null && (\n <div className={[headerClass, 'rsp__header'].filter(Boolean).join(' ')} style={{ flexShrink: 0 }}>\n {header}\n </div>\n )}\n \n <div className={[bodyClass, 'rsp__body'].filter(Boolean).join(' ')} style={{ flexGrow: 1, overflowY: 'auto' }}>\n {children}\n {!hideCloseBtn && <SidePanelCloseButton onClose={closePanel} />}\n </div>\n\n {footer != null && (\n <div className={[footerClass, 'rsp__footer'].filter(Boolean).join(' ')} style={{ flexShrink: 0 }}>\n {footer}\n </div>\n )}\n </div>\n );\n\n const content = (\n <div \n className={`rsp-wrapper${!isCompletelyHidden ? ' rsp-wrapper--active' : ''}`}\n style={{ display: isCompletelyHidden ? 'none' : 'block' }}\n >\n {!isCompletelyHidden && (\n <div\n className={`rsp-overlay ${overlayTransitionClass}`}\n style={overlayStyles}\n onClick={() => !noClose && closePanel()}\n role=\"presentation\"\n aria-hidden=\"true\"\n />\n )}\n {panelContent}\n </div>\n );\n\n return createPortal(content, portalContainer);\n}\n\nSlidePanel.displayName = 'SlidePanel';","export interface SidePanelCloseButtonProps {\n onClose: () => void;\n className?: string;\n}\n\nexport function SidePanelCloseButton({ onClose, className }: SidePanelCloseButtonProps) {\n return (\n <button\n type=\"button\"\n className={className ? `rsp-close ${className}` : 'rsp-close'}\n onClick={onClose}\n aria-label=\"Close panel\"\n >\n <span className=\"rsp-close__x\" />\n </button>\n );\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB,wBAAwB;;;ACI9C;AARC,SAAS,qBAAqB,EAAE,SAAS,UAAU,GAA8B;AACtF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW,YAAY,aAAa,SAAS,KAAK;AAAA,MAClD,SAAS;AAAA,MACT,cAAW;AAAA,MAEX,8BAAC,UAAK,WAAU,gBAAe;AAAA;AAAA,EACjC;AAEJ;;;AD+OQ,gBAAAA,MAKF,YALE;AAjLD,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,eAAe;AAAA,EACf,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ,aAAa;AAAA,EACrB,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB;AAAA,EACA,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,OAAO;AACT,GAAoB;AAClB,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAC/C,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAA6B,IAAI;AAE/E,QAAM,WAAW,OAA8B,IAAI;AACnD,QAAM,0BAA0B,OAAO,KAAK;AAE5C,QAAM,aAAa,kBAAkB,SAAS,IAAI;AAElD,QAAM,YAAY,OAAO,EAAE,UAAU,SAAS,CAAC;AAC/C,kBAAgB,MAAM;AACpB,cAAU,UAAU,EAAE,UAAU,SAAS;AAAA,EAC3C,CAAC;AAGD,YAAU,MAAM;AACd,QAAI;AAEJ,QAAI,MAAM;AACR,mBAAa,IAAI;AACjB,mBAAa,KAAK;AAClB,kBAAY,WAAW,MAAM;AAC3B,kBAAU,QAAQ,WAAW;AAAA,MAC/B,GAAG,KAAK,IAAI,iBAAiB,aAAa,CAAC;AAAA,IAC7C,WAAW,WAAW;AACpB,mBAAa,IAAI;AACjB,kBAAY,WAAW,MAAM;AAC3B,qBAAa,KAAK;AAClB,qBAAa,KAAK;AAClB,kBAAU,QAAQ,WAAW;AAAA,MAC/B,GAAG,KAAK,IAAI,iBAAiB,aAAa,CAAC;AAAA,IAC7C;AAEA,WAAO,MAAM,aAAa,SAAS;AAAA,EACrC,GAAG,CAAC,MAAM,WAAW,iBAAiB,aAAa,CAAC;AAGpD,kBAAgB,MAAM;AACpB,QAAI,OAAO,aAAa,YAAa;AACrC,QAAI,KAAK,SAAS,eAAe,MAAM;AACvC,QAAI,CAAC,IAAI;AACP,WAAK,SAAS,cAAc,KAAK;AACjC,SAAG,aAAa,MAAM,MAAM;AAC5B,eAAS,KAAK,YAAY,EAAE;AAC5B,8BAAwB,UAAU;AAAA,IACpC;AACA,uBAAmB,EAAE;AACrB,WAAO,MAAM;AACX,UAAI,wBAAwB,WAAW,IAAI,YAAY;AACrD,iBAAS,KAAK,YAAY,EAAE;AAC5B,gCAAwB,UAAU;AAAA,MACpC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAGX,QAAM,0BAA0B,OAAsB,IAAI;AAC1D,QAAM,sBAAsB,OAAO,KAAK;AAExC,YAAU,MAAM;AACd,UAAM,KAAK,SAAS;AACpB,UAAM,aAAa,QAAQ,cAAc;AAEzC,QAAI,YAAY;AAEd,YAAM,eAAe,SAAS,gBAAgB,MAAM;AACpD,YAAM,eAAe,SAAS,KAAK,MAAM;AAEzC,0BAAoB,UAAU,iBAAiB,YAAY,iBAAiB;AAC5E,8BAAwB,UAAU;AAElC,wBAAkB,IAAI,EAAE,qBAAqB,KAAK,CAAC;AAGnD,UAAI,kBAAkB,CAAC,oBAAoB,SAAS;AAClD,iBAAS,gBAAgB,MAAM,WAAW;AAAA,MAC5C;AAAA,IACF;AAEA,WAAO,MAAM;AACX,UAAI,cAAc,IAAI;AACpB,yBAAiB,EAAE;AAGnB,YAAI,kBAAkB,CAAC,oBAAoB,SAAS;AAClD,cAAI,wBAAwB,SAAS;AACnC,qBAAS,gBAAgB,MAAM,WAAW,wBAAwB;AAAA,UACpE,OAAO;AACL,qBAAS,gBAAgB,MAAM,eAAe,UAAU;AAAA,UAC1D;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,GAAG,CAAC,MAAM,YAAY,cAAc,CAAC;AAErC,QAAM,aAAa,YAAY,MAAM;AACnC,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,YAAY,CAAC;AAGjB,QAAM,gBAAqC;AAAA,IACzC,OAAO;AAAA,MACL,QAAQ,eAAe,SAAS,OAAO;AAAA,MACvC,mBAAmB,GAAG,eAAe;AAAA,MACrC,CAAC,mBAA6B,GAAG;AAAA,MACjC,SAAS,YAAY,IAAI;AAAA,MACzB,eAAe,YAAY,SAAS;AAAA,MACpC,iBAAiB;AAAA,IACnB;AAAA,IACA,CAAC,YAAY,iBAAiB,gBAAgB,cAAc,SAAS;AAAA,EACvE;AAEA,QAAM,cAAmC,QAAQ,MAAM;AACrD,UAAM,OAA4B;AAAA,MAChC,QAAQ,eAAe,SAAS,OAAO;AAAA,MACvC,iBAAiB;AAAA,MACjB,mBAAmB,GAAG,aAAa;AAAA,MACnC,SAAS;AAAA,MACT,eAAe;AAAA,MACf,UAAU;AAAA,MACV,GAAG;AAAA,IACL;AAEA,QAAI,SAAS,UAAU,SAAS,SAAS;AACvC,WAAK,QAAQ;AACb,WAAK,SAAS;AAAA,IAChB,OAAO;AACL,WAAK,SAAS;AACd,WAAK,QAAQ;AACb,WAAK,YAAY;AAAA,IACnB;AACA,WAAO;AAAA,EACT,GAAG,CAAC,YAAY,YAAY,eAAe,MAAM,OAAO,QAAQ,SAAS,CAAC;AAE1E,QAAM,qBAAqB,CAAC;AAC5B,MAAI,CAAC,mBAAoB,sBAAsB,SAAW,QAAO;AAEjE,QAAM,yBAAyB,YAAY,yBAAyB;AACpE,QAAM,uBAAuB,YAAY,GAAG,UAAU,kBAAkB,GAAG,UAAU;AAErF,QAAM,eACJ;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW,YAAY,IAAI,SAAS,oBAAoB,IAAI,SAAS,GAAG,KAAK;AAAA,MAC7E,OAAO;AAAA,MAEN;AAAA,kBAAU,QACT,gBAAAA,KAAC,SAAI,WAAW,CAAC,aAAa,aAAa,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,GAAG,OAAO,EAAE,YAAY,EAAE,GAC5F,kBACH;AAAA,QAGF,qBAAC,SAAI,WAAW,CAAC,WAAW,WAAW,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,GAAG,OAAO,EAAE,UAAU,GAAG,WAAW,OAAO,GACzG;AAAA;AAAA,UACA,CAAC,gBAAgB,gBAAAA,KAAC,wBAAqB,SAAS,YAAY;AAAA,WAC/D;AAAA,QAEC,UAAU,QACT,gBAAAA,KAAC,SAAI,WAAW,CAAC,aAAa,aAAa,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,GAAG,OAAO,EAAE,YAAY,EAAE,GAC5F,kBACH;AAAA;AAAA;AAAA,EAEJ;AAGF,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,cAAc,CAAC,qBAAqB,yBAAyB,EAAE;AAAA,MAC1E,OAAO,EAAE,SAAS,qBAAqB,SAAS,QAAQ;AAAA,MAEvD;AAAA,SAAC,sBACA,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW,eAAe,sBAAsB;AAAA,YAChD,OAAO;AAAA,YACP,SAAS,MAAM,CAAC,WAAW,WAAW;AAAA,YACtC,MAAK;AAAA,YACL,eAAY;AAAA;AAAA,QACd;AAAA,QAED;AAAA;AAAA;AAAA,EACH;AAGF,SAAO,aAAa,SAAS,eAAe;AAC9C;AAEA,WAAW,cAAc;","names":["jsx"]}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
.rsp-wrapper .rsp-overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.rsp-wrapper .rsp {
|
|
10
|
+
position: fixed;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.rsp-wrapper .rsp--right-side,
|
|
14
|
+
.rsp-wrapper .rsp--left-side {
|
|
15
|
+
top: 0;
|
|
16
|
+
height: 100%;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.rsp-wrapper .rsp--right-side {
|
|
20
|
+
right: 0;
|
|
21
|
+
left: unset;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.rsp-wrapper .rsp--left-side {
|
|
25
|
+
right: unset;
|
|
26
|
+
left: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.rsp-wrapper .rsp--bottom-side,
|
|
30
|
+
.rsp-wrapper .rsp--top-side {
|
|
31
|
+
left: 0;
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.rsp-wrapper .rsp--bottom-side {
|
|
36
|
+
bottom: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.rsp-wrapper .rsp--top-side {
|
|
40
|
+
top: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.rsp-wrapper .rsp__header,
|
|
44
|
+
.rsp-wrapper .rsp__body,
|
|
45
|
+
.rsp-wrapper .rsp__footer {
|
|
46
|
+
overflow: auto;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.rsp-wrapper .rsp__body {
|
|
50
|
+
position: relative;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* =========================================
|
|
54
|
+
OVERLAY ANIMATIONS
|
|
55
|
+
========================================= */
|
|
56
|
+
.rsp-wrapper .overlay-enter-active {
|
|
57
|
+
animation: rsp-overlay-in forwards;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.rsp-wrapper .overlay-leave-active {
|
|
61
|
+
animation: rsp-overlay-out forwards;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@keyframes rsp-overlay-in {
|
|
65
|
+
0% { opacity: 0; }
|
|
66
|
+
100% { opacity: var(--overlay-opacity, 0.5); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@keyframes rsp-overlay-out {
|
|
70
|
+
0% { opacity: var(--overlay-opacity, 0.5); }
|
|
71
|
+
100% { opacity: 0; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* =========================================
|
|
75
|
+
SLIDE RIGHT
|
|
76
|
+
========================================= */
|
|
77
|
+
.rsp-wrapper .slide-right-enter-active {
|
|
78
|
+
animation: rsp-slide-right-in forwards;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.rsp-wrapper .slide-right-leave-active {
|
|
82
|
+
animation: rsp-slide-right-out forwards;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@keyframes rsp-slide-right-in {
|
|
86
|
+
0% { transform: translateX(100%); opacity: 0; }
|
|
87
|
+
100% { transform: translateX(0); opacity: 1; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@keyframes rsp-slide-right-out {
|
|
91
|
+
0% { transform: translateX(0); opacity: 1; }
|
|
92
|
+
100% { transform: translateX(100%); opacity: 0; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* =========================================
|
|
96
|
+
SLIDE LEFT
|
|
97
|
+
========================================= */
|
|
98
|
+
.rsp-wrapper .slide-left-enter-active {
|
|
99
|
+
animation: rsp-slide-left-in forwards;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.rsp-wrapper .slide-left-leave-active {
|
|
103
|
+
animation: rsp-slide-left-out forwards;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@keyframes rsp-slide-left-in {
|
|
107
|
+
0% { transform: translateX(-100%); opacity: 0; }
|
|
108
|
+
100% { transform: translateX(0); opacity: 1; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@keyframes rsp-slide-left-out {
|
|
112
|
+
0% { transform: translateX(0); opacity: 1; }
|
|
113
|
+
100% { transform: translateX(-100%); opacity: 0; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* =========================================
|
|
117
|
+
SLIDE TOP
|
|
118
|
+
========================================= */
|
|
119
|
+
.rsp-wrapper .slide-top-enter-active {
|
|
120
|
+
animation: rsp-slide-top-in forwards;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.rsp-wrapper .slide-top-leave-active {
|
|
124
|
+
animation: rsp-slide-top-out forwards;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@keyframes rsp-slide-top-in {
|
|
128
|
+
0% { transform: translateY(-100%); opacity: 0; }
|
|
129
|
+
100% { transform: translateY(0); opacity: 1; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@keyframes rsp-slide-top-out {
|
|
133
|
+
0% { transform: translateY(0); opacity: 1; }
|
|
134
|
+
100% { transform: translateY(-100%); opacity: 0; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* =========================================
|
|
138
|
+
SLIDE BOTTOM
|
|
139
|
+
========================================= */
|
|
140
|
+
.rsp-wrapper .slide-bottom-enter-active {
|
|
141
|
+
animation: rsp-slide-bottom-in forwards;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.rsp-wrapper .slide-bottom-leave-active {
|
|
145
|
+
animation: rsp-slide-bottom-out forwards;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@keyframes rsp-slide-bottom-in {
|
|
149
|
+
0% { transform: translateY(100%); opacity: 0; }
|
|
150
|
+
100% { transform: translateY(0); opacity: 1; }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@keyframes rsp-slide-bottom-out {
|
|
154
|
+
0% { transform: translateY(0); opacity: 1; }
|
|
155
|
+
100% { transform: translateY(100%); opacity: 0; }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* =========================================
|
|
159
|
+
CLOSE BUTTON
|
|
160
|
+
========================================= */
|
|
161
|
+
.rsp-close {
|
|
162
|
+
position: absolute;
|
|
163
|
+
top: 24px;
|
|
164
|
+
right: 24px;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
justify-content: center;
|
|
168
|
+
width: 36px;
|
|
169
|
+
height: 36px;
|
|
170
|
+
padding: 0;
|
|
171
|
+
margin: 0;
|
|
172
|
+
font: inherit;
|
|
173
|
+
color: inherit;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
background: white;
|
|
176
|
+
border: none;
|
|
177
|
+
border-radius: 50%;
|
|
178
|
+
transition: background-color 0.1s;
|
|
179
|
+
-webkit-tap-highlight-color: transparent;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.rsp-close:hover {
|
|
183
|
+
background: #f7f7f7;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.rsp-close__x {
|
|
187
|
+
position: relative;
|
|
188
|
+
left: -1px;
|
|
189
|
+
display: block;
|
|
190
|
+
width: 20px;
|
|
191
|
+
height: 20px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.rsp-close__x::before,
|
|
195
|
+
.rsp-close__x::after {
|
|
196
|
+
position: absolute;
|
|
197
|
+
width: 2px;
|
|
198
|
+
height: 20px;
|
|
199
|
+
content: ' ';
|
|
200
|
+
background-color: #777;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.rsp-close__x::before {
|
|
204
|
+
transform: rotate(-45deg);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.rsp-close__x::after {
|
|
208
|
+
transform: rotate(45deg);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.rsp-close:active {
|
|
212
|
+
top: 23px;
|
|
213
|
+
right: 23px;
|
|
214
|
+
width: 38px;
|
|
215
|
+
height: 38px;
|
|
216
|
+
background: #f1f1f1;
|
|
217
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-slide-panel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Easy to use and flexible modal drawer/sidebar component for React (TypeScript)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"style": "dist/styles.css",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup && cp src/styles.css dist/",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
23
|
+
"demo": "cd demo && npm install && npm run dev",
|
|
24
|
+
"build:demo": "npm run build && cd demo && npm install && npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"react",
|
|
28
|
+
"modal",
|
|
29
|
+
"sidebar",
|
|
30
|
+
"panel",
|
|
31
|
+
"slide-panel",
|
|
32
|
+
"drawer"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/headmandev/react-slide-panel"
|
|
37
|
+
},
|
|
38
|
+
"author": "headmandev",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=16.8.0",
|
|
42
|
+
"react-dom": ">=16.8.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"body-scroll-lock": "^4.0.0-beta.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/body-scroll-lock": "^3.1.2",
|
|
49
|
+
"@types/react": "^18.2.0",
|
|
50
|
+
"@types/react-dom": "^18.2.0",
|
|
51
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
52
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
53
|
+
"eslint": "^8.0.0",
|
|
54
|
+
"eslint-plugin-react": "^7.34.0",
|
|
55
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
56
|
+
"react": "^18.2.0",
|
|
57
|
+
"react-dom": "^18.2.0",
|
|
58
|
+
"tsup": "^8.0.0",
|
|
59
|
+
"typescript": "^5.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|