react-edge-dock 1.0.0 → 1.0.3
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 +71 -0
- package/dist/EdgeDock.d.ts.map +1 -1
- package/dist/EdgeDock.js +3 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/useEdgeDock.d.ts.map +1 -1
- package/dist/useEdgeDock.js +78 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ A zero-dependency React + TypeScript library for customizable draggable edge-doc
|
|
|
12
12
|
- 🎮 Multiple docking modes (free, auto, manual)
|
|
13
13
|
- 💡 Smart popup positioning
|
|
14
14
|
- ⚡ Performance optimized with transform: translate3d
|
|
15
|
+
- 🌐 **SSR compatible (Next.js, Remix, etc.)**
|
|
16
|
+
- 📏 **Configurable edge offset/margin**
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -29,6 +31,7 @@ function App() {
|
|
|
29
31
|
<EdgeDock
|
|
30
32
|
dockMode="auto"
|
|
31
33
|
animation={true}
|
|
34
|
+
edgeOffset={16} // 16px gap from screen edge
|
|
32
35
|
button={<button>🚀</button>}
|
|
33
36
|
popup={<div>Your content here</div>}
|
|
34
37
|
/>
|
|
@@ -36,6 +39,74 @@ function App() {
|
|
|
36
39
|
}
|
|
37
40
|
```
|
|
38
41
|
|
|
42
|
+
## Usage in Next.js
|
|
43
|
+
|
|
44
|
+
Works out of the box with Next.js App Router and Pages Router:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
'use client'; // For App Router
|
|
48
|
+
|
|
49
|
+
import { EdgeDock } from 'react-edge-dock';
|
|
50
|
+
|
|
51
|
+
export default function MyComponent() {
|
|
52
|
+
return (
|
|
53
|
+
<EdgeDock
|
|
54
|
+
dockMode="auto"
|
|
55
|
+
edgeOffset={20}
|
|
56
|
+
button={<button>Menu</button>}
|
|
57
|
+
popup={<div>Navigation</div>}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
- `dockMode`: `"free"` | `"auto"` | `"manual"` - Docking behavior
|
|
66
|
+
- `dockEdge`: `"left"` | `"right"` | `"top"` | `"bottom"` - Fixed edge (manual mode)
|
|
67
|
+
- `allowedEdges`: `DockEdge[]` - Restrict docking to specific edges (e.g., `['left', 'right']` for horizontal only)
|
|
68
|
+
- `edgeOffset`: `number` - Gap from screen edge in pixels (default: 0)
|
|
69
|
+
- `animation`: `boolean` - Enable snap animations
|
|
70
|
+
- `popupGap`: `number` - Gap between button and popup
|
|
71
|
+
- `position`: `{ x: number; y: number }` - Initial/controlled position
|
|
72
|
+
- `zIndex`: `number` - z-index for the dock
|
|
73
|
+
- `onDockChange`: Callback when dock state changes
|
|
74
|
+
- `isPopupOpen` / `onPopupChange`: Controlled popup state
|
|
75
|
+
|
|
76
|
+
## Examples
|
|
77
|
+
|
|
78
|
+
### Restrict to horizontal edges only (left/right)
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<EdgeDock
|
|
82
|
+
dockMode="auto"
|
|
83
|
+
allowedEdges={['left', 'right']}
|
|
84
|
+
edgeOffset={16}
|
|
85
|
+
button={<button>📱</button>}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Restrict to vertical edges only (top/bottom)
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<EdgeDock
|
|
93
|
+
dockMode="auto"
|
|
94
|
+
allowedEdges={['top', 'bottom']}
|
|
95
|
+
button={<button>⬆️</button>}
|
|
96
|
+
/>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Manual edge with offset
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<EdgeDock
|
|
103
|
+
dockMode="manual"
|
|
104
|
+
dockEdge="right"
|
|
105
|
+
edgeOffset={20}
|
|
106
|
+
button={<button>➡️</button>}
|
|
107
|
+
/>
|
|
108
|
+
```
|
|
109
|
+
|
|
39
110
|
## API
|
|
40
111
|
|
|
41
112
|
See the [example.tsx](./example.tsx) file for more detailed usage examples.
|
package/dist/EdgeDock.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EdgeDock.d.ts","sourceRoot":"","sources":["../src/EdgeDock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"EdgeDock.d.ts","sourceRoot":"","sources":["../src/EdgeDock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,qBAsF5C"}
|
package/dist/EdgeDock.js
CHANGED
|
@@ -14,13 +14,15 @@ import { useEdgeDock } from './useEdgeDock';
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
export function EdgeDock(props) {
|
|
17
|
-
const { button, popup, className, style, dockMode, dockEdge, position, animation, popupGap, zIndex, onDockChange, isPopupOpen, onPopupChange, } = props;
|
|
17
|
+
const { button, popup, className, style, dockMode, dockEdge, allowedEdges, position, animation, popupGap, edgeOffset, zIndex, onDockChange, isPopupOpen, onPopupChange, } = props;
|
|
18
18
|
const { state, buttonRef, popupRef, closePopup, buttonStyles, popupStyles, buttonProps, } = useEdgeDock({
|
|
19
19
|
dockMode,
|
|
20
20
|
dockEdge,
|
|
21
|
+
allowedEdges,
|
|
21
22
|
position,
|
|
22
23
|
animation,
|
|
23
24
|
popupGap,
|
|
25
|
+
edgeOffset,
|
|
24
26
|
zIndex,
|
|
25
27
|
onDockChange,
|
|
26
28
|
isPopupOpen,
|
package/dist/types.d.ts
CHANGED
|
@@ -35,12 +35,16 @@ export interface EdgeDockConfig {
|
|
|
35
35
|
dockMode?: DockMode;
|
|
36
36
|
/** Fixed edge for manual dock mode */
|
|
37
37
|
dockEdge?: DockEdge;
|
|
38
|
+
/** Restrict which edges are allowed for docking (only works in auto mode) */
|
|
39
|
+
allowedEdges?: DockEdge[];
|
|
38
40
|
/** Initial or controlled position */
|
|
39
41
|
position?: Position;
|
|
40
42
|
/** Enable snap animations */
|
|
41
43
|
animation?: boolean;
|
|
42
44
|
/** Gap between button and popup in pixels */
|
|
43
45
|
popupGap?: number;
|
|
46
|
+
/** Offset from edge when docked (in pixels) */
|
|
47
|
+
edgeOffset?: number;
|
|
44
48
|
/** z-index for the dock container */
|
|
45
49
|
zIndex?: number;
|
|
46
50
|
/** Callback when dock state changes */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEdgeDock.d.ts","sourceRoot":"","sources":["../src/useEdgeDock.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EAId,iBAAiB,EAGlB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"useEdgeDock.d.ts","sourceRoot":"","sources":["../src/useEdgeDock.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EAId,iBAAiB,EAGlB,MAAM,SAAS,CAAC;AAwKjB;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,GAAE,cAAmB,GAAG,iBAAiB,CAgV1E"}
|
package/dist/useEdgeDock.js
CHANGED
|
@@ -1,47 +1,66 @@
|
|
|
1
1
|
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Check if code is running in browser (SSR-safe)
|
|
4
|
+
*/
|
|
5
|
+
const isBrowser = typeof window !== 'undefined';
|
|
6
|
+
/**
|
|
7
|
+
* Get viewport dimensions safely (SSR-compatible)
|
|
8
|
+
*/
|
|
9
|
+
function getViewport() {
|
|
10
|
+
if (!isBrowser) {
|
|
11
|
+
return { width: 1920, height: 1080 }; // Default fallback for SSR
|
|
12
|
+
}
|
|
13
|
+
return { width: window.innerWidth, height: window.innerHeight };
|
|
14
|
+
}
|
|
2
15
|
/**
|
|
3
16
|
* Calculate which edge is closest to the given position
|
|
4
17
|
*/
|
|
5
|
-
function getClosestEdge(pos, viewport) {
|
|
18
|
+
function getClosestEdge(pos, viewport, allowedEdges) {
|
|
6
19
|
const distanceToLeft = pos.x;
|
|
7
20
|
const distanceToRight = viewport.width - pos.x;
|
|
8
21
|
const distanceToTop = pos.y;
|
|
9
22
|
const distanceToBottom = viewport.height - pos.y;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
// Create map of edges with their distances
|
|
24
|
+
const edgeDistances = [
|
|
25
|
+
{ edge: 'left', distance: distanceToLeft },
|
|
26
|
+
{ edge: 'right', distance: distanceToRight },
|
|
27
|
+
{ edge: 'top', distance: distanceToTop },
|
|
28
|
+
{ edge: 'bottom', distance: distanceToBottom },
|
|
29
|
+
];
|
|
30
|
+
// Filter by allowed edges if specified
|
|
31
|
+
const validEdges = allowedEdges
|
|
32
|
+
? edgeDistances.filter(({ edge }) => allowedEdges.includes(edge))
|
|
33
|
+
: edgeDistances;
|
|
34
|
+
// Find the closest edge
|
|
35
|
+
const closest = validEdges.reduce((min, current) => current.distance < min.distance ? current : min);
|
|
36
|
+
return closest.edge;
|
|
18
37
|
}
|
|
19
38
|
/**
|
|
20
39
|
* Snap position to edge based on dock mode
|
|
21
40
|
*/
|
|
22
|
-
function snapToEdge(pos, edge, viewport, buttonDimensions) {
|
|
41
|
+
function snapToEdge(pos, edge, viewport, buttonDimensions, offset = 0) {
|
|
23
42
|
const halfWidth = buttonDimensions.width / 2;
|
|
24
43
|
const halfHeight = buttonDimensions.height / 2;
|
|
25
44
|
switch (edge) {
|
|
26
45
|
case 'left':
|
|
27
46
|
return {
|
|
28
|
-
x: halfWidth,
|
|
47
|
+
x: halfWidth + offset,
|
|
29
48
|
y: Math.max(halfHeight, Math.min(viewport.height - halfHeight, pos.y)),
|
|
30
49
|
};
|
|
31
50
|
case 'right':
|
|
32
51
|
return {
|
|
33
|
-
x: viewport.width - halfWidth,
|
|
52
|
+
x: viewport.width - halfWidth - offset,
|
|
34
53
|
y: Math.max(halfHeight, Math.min(viewport.height - halfHeight, pos.y)),
|
|
35
54
|
};
|
|
36
55
|
case 'top':
|
|
37
56
|
return {
|
|
38
57
|
x: Math.max(halfWidth, Math.min(viewport.width - halfWidth, pos.x)),
|
|
39
|
-
y: halfHeight,
|
|
58
|
+
y: halfHeight + offset,
|
|
40
59
|
};
|
|
41
60
|
case 'bottom':
|
|
42
61
|
return {
|
|
43
62
|
x: Math.max(halfWidth, Math.min(viewport.width - halfWidth, pos.x)),
|
|
44
|
-
y: viewport.height - halfHeight,
|
|
63
|
+
y: viewport.height - halfHeight - offset,
|
|
45
64
|
};
|
|
46
65
|
}
|
|
47
66
|
}
|
|
@@ -109,11 +128,13 @@ function calculatePopupPosition(buttonPos, buttonDimensions, popupDimensions, vi
|
|
|
109
128
|
* Main hook for edge dock functionality
|
|
110
129
|
*/
|
|
111
130
|
export function useEdgeDock(config = {}) {
|
|
112
|
-
const { dockMode = 'auto', dockEdge, position: controlledPosition, animation = true, popupGap = 12, zIndex = 9999, onDockChange, isPopupOpen: controlledPopupOpen, onPopupChange, } = config;
|
|
131
|
+
const { dockMode = 'auto', dockEdge, allowedEdges, position: controlledPosition, animation = true, popupGap = 12, edgeOffset = 0, zIndex = 9999, onDockChange, isPopupOpen: controlledPopupOpen, onPopupChange, } = config;
|
|
113
132
|
const buttonRef = useRef(null);
|
|
114
133
|
const popupRef = useRef(null);
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
const isMountedRef = useRef(false);
|
|
135
|
+
// Use a fixed initial position to avoid SSR hydration mismatch
|
|
136
|
+
// This will be updated after mount on the client
|
|
137
|
+
const [position, setPositionInternal] = useState(controlledPosition || { x: 100, y: 100 });
|
|
117
138
|
const [dockedEdge, setDockedEdge] = useState(null);
|
|
118
139
|
const [isDragging, setIsDragging] = useState(false);
|
|
119
140
|
const [isPopupOpenInternal, setIsPopupOpenInternal] = useState(false);
|
|
@@ -137,6 +158,28 @@ export function useEdgeDock(config = {}) {
|
|
|
137
158
|
isDragging,
|
|
138
159
|
isPopupOpen,
|
|
139
160
|
};
|
|
161
|
+
// Initialize position after mount (client-side only) to avoid hydration mismatch
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!isMountedRef.current && !controlledPosition && buttonRef.current) {
|
|
164
|
+
isMountedRef.current = true;
|
|
165
|
+
const viewport = getViewport();
|
|
166
|
+
const buttonRect = buttonRef.current.getBoundingClientRect();
|
|
167
|
+
const buttonDimensions = { width: buttonRect.width, height: buttonRect.height };
|
|
168
|
+
// Set initial position on client after mount
|
|
169
|
+
let initialPos = { x: viewport.width - 60, y: viewport.height - 60 };
|
|
170
|
+
initialPos = constrainToViewport(initialPos, viewport, buttonDimensions);
|
|
171
|
+
if (dockMode === 'auto') {
|
|
172
|
+
const edge = getClosestEdge(initialPos, viewport, allowedEdges);
|
|
173
|
+
initialPos = snapToEdge(initialPos, edge, viewport, buttonDimensions, edgeOffset);
|
|
174
|
+
setDockedEdge(edge);
|
|
175
|
+
}
|
|
176
|
+
else if (dockMode === 'manual' && dockEdge) {
|
|
177
|
+
initialPos = snapToEdge(initialPos, dockEdge, viewport, buttonDimensions, edgeOffset);
|
|
178
|
+
setDockedEdge(dockEdge);
|
|
179
|
+
}
|
|
180
|
+
setPositionInternal(initialPos);
|
|
181
|
+
}
|
|
182
|
+
}, [controlledPosition, dockMode, dockEdge, allowedEdges, edgeOffset]);
|
|
140
183
|
// Update controlled position
|
|
141
184
|
useEffect(() => {
|
|
142
185
|
if (controlledPosition) {
|
|
@@ -148,7 +191,7 @@ export function useEdgeDock(config = {}) {
|
|
|
148
191
|
if (isPopupOpen && buttonRef.current && popupRef.current) {
|
|
149
192
|
const buttonRect = buttonRef.current.getBoundingClientRect();
|
|
150
193
|
const popupRect = popupRef.current.getBoundingClientRect();
|
|
151
|
-
const result = calculatePopupPosition(position, { width: buttonRect.width, height: buttonRect.height }, { width: popupRect.width, height: popupRect.height },
|
|
194
|
+
const result = calculatePopupPosition(position, { width: buttonRect.width, height: buttonRect.height }, { width: popupRect.width, height: popupRect.height }, getViewport(), popupGap);
|
|
152
195
|
setPopupPosition(result.position);
|
|
153
196
|
setPopupOrigin(result.origin);
|
|
154
197
|
}
|
|
@@ -164,23 +207,23 @@ export function useEdgeDock(config = {}) {
|
|
|
164
207
|
if (!buttonRef.current)
|
|
165
208
|
return;
|
|
166
209
|
const buttonRect = buttonRef.current.getBoundingClientRect();
|
|
167
|
-
const viewport =
|
|
210
|
+
const viewport = getViewport();
|
|
168
211
|
const buttonDimensions = { width: buttonRect.width, height: buttonRect.height };
|
|
169
212
|
let finalPos = constrainToViewport(newPos, viewport, buttonDimensions);
|
|
170
213
|
if (dockMode === 'auto') {
|
|
171
|
-
const edge = getClosestEdge(finalPos, viewport);
|
|
172
|
-
finalPos = snapToEdge(finalPos, edge, viewport, buttonDimensions);
|
|
214
|
+
const edge = getClosestEdge(finalPos, viewport, allowedEdges);
|
|
215
|
+
finalPos = snapToEdge(finalPos, edge, viewport, buttonDimensions, edgeOffset);
|
|
173
216
|
setDockedEdge(edge);
|
|
174
217
|
}
|
|
175
218
|
else if (dockMode === 'manual' && dockEdge) {
|
|
176
|
-
finalPos = snapToEdge(finalPos, dockEdge, viewport, buttonDimensions);
|
|
219
|
+
finalPos = snapToEdge(finalPos, dockEdge, viewport, buttonDimensions, edgeOffset);
|
|
177
220
|
setDockedEdge(dockEdge);
|
|
178
221
|
}
|
|
179
222
|
else {
|
|
180
223
|
setDockedEdge(null);
|
|
181
224
|
}
|
|
182
225
|
setPositionInternal(finalPos);
|
|
183
|
-
}, [dockMode, dockEdge]);
|
|
226
|
+
}, [dockMode, dockEdge, allowedEdges, edgeOffset]);
|
|
184
227
|
// Toggle popup
|
|
185
228
|
const togglePopup = useCallback(() => {
|
|
186
229
|
const newState = !isPopupOpen;
|
|
@@ -258,16 +301,16 @@ export function useEdgeDock(config = {}) {
|
|
|
258
301
|
// Snap to edge if needed
|
|
259
302
|
if (buttonRef.current) {
|
|
260
303
|
const buttonRect = buttonRef.current.getBoundingClientRect();
|
|
261
|
-
const viewport =
|
|
304
|
+
const viewport = getViewport();
|
|
262
305
|
const buttonDimensions = { width: buttonRect.width, height: buttonRect.height };
|
|
263
306
|
let finalPos = position;
|
|
264
307
|
if (dockMode === 'auto') {
|
|
265
|
-
const edge = getClosestEdge(position, viewport);
|
|
266
|
-
finalPos = snapToEdge(position, edge, viewport, buttonDimensions);
|
|
308
|
+
const edge = getClosestEdge(position, viewport, allowedEdges);
|
|
309
|
+
finalPos = snapToEdge(position, edge, viewport, buttonDimensions, edgeOffset);
|
|
267
310
|
setDockedEdge(edge);
|
|
268
311
|
}
|
|
269
312
|
else if (dockMode === 'manual' && dockEdge) {
|
|
270
|
-
finalPos = snapToEdge(position, dockEdge, viewport, buttonDimensions);
|
|
313
|
+
finalPos = snapToEdge(position, dockEdge, viewport, buttonDimensions, edgeOffset);
|
|
271
314
|
setDockedEdge(dockEdge);
|
|
272
315
|
}
|
|
273
316
|
setPositionInternal(finalPos);
|
|
@@ -279,7 +322,7 @@ export function useEdgeDock(config = {}) {
|
|
|
279
322
|
document.removeEventListener('pointermove', handlePointerMove);
|
|
280
323
|
document.removeEventListener('pointerup', handlePointerUp);
|
|
281
324
|
};
|
|
282
|
-
}, [position, dockMode, dockEdge, animation]);
|
|
325
|
+
}, [position, dockMode, dockEdge, animation, edgeOffset, allowedEdges]);
|
|
283
326
|
// Click handler (only trigger if not dragged)
|
|
284
327
|
const handleClick = useCallback((e) => {
|
|
285
328
|
if (dragStateRef.current.hasMoved) {
|
|
@@ -291,26 +334,28 @@ export function useEdgeDock(config = {}) {
|
|
|
291
334
|
}, [togglePopup]);
|
|
292
335
|
// Handle window resize
|
|
293
336
|
useEffect(() => {
|
|
337
|
+
if (!isBrowser)
|
|
338
|
+
return;
|
|
294
339
|
const handleResize = () => {
|
|
295
340
|
if (buttonRef.current) {
|
|
296
341
|
const buttonRect = buttonRef.current.getBoundingClientRect();
|
|
297
|
-
const viewport =
|
|
342
|
+
const viewport = getViewport();
|
|
298
343
|
const buttonDimensions = { width: buttonRect.width, height: buttonRect.height };
|
|
299
344
|
let newPos = constrainToViewport(position, viewport, buttonDimensions);
|
|
300
345
|
if (dockMode === 'auto') {
|
|
301
|
-
const edge = getClosestEdge(newPos, viewport);
|
|
302
|
-
newPos = snapToEdge(newPos, edge, viewport, buttonDimensions);
|
|
346
|
+
const edge = getClosestEdge(newPos, viewport, allowedEdges);
|
|
347
|
+
newPos = snapToEdge(newPos, edge, viewport, buttonDimensions, edgeOffset);
|
|
303
348
|
setDockedEdge(edge);
|
|
304
349
|
}
|
|
305
350
|
else if (dockMode === 'manual' && dockEdge) {
|
|
306
|
-
newPos = snapToEdge(newPos, dockEdge, viewport, buttonDimensions);
|
|
351
|
+
newPos = snapToEdge(newPos, dockEdge, viewport, buttonDimensions, edgeOffset);
|
|
307
352
|
}
|
|
308
353
|
setPositionInternal(newPos);
|
|
309
354
|
}
|
|
310
355
|
};
|
|
311
356
|
window.addEventListener('resize', handleResize);
|
|
312
357
|
return () => window.removeEventListener('resize', handleResize);
|
|
313
|
-
}, [position, dockMode, dockEdge]);
|
|
358
|
+
}, [position, dockMode, dockEdge, edgeOffset, allowedEdges]);
|
|
314
359
|
// Button styles
|
|
315
360
|
const buttonStyles = {
|
|
316
361
|
position: 'fixed',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-edge-dock",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A zero-dependency React TypeScript library for customizable draggable edge-docked floating buttons with popup support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|