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 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.
@@ -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,qBAkF5C"}
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 */
@@ -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;AA4IjB;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,GAAE,cAAmB,GAAG,iBAAiB,CAiT1E"}
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"}
@@ -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
- const minDistance = Math.min(distanceToLeft, distanceToRight, distanceToTop, distanceToBottom);
11
- if (minDistance === distanceToLeft)
12
- return 'left';
13
- if (minDistance === distanceToRight)
14
- return 'right';
15
- if (minDistance === distanceToTop)
16
- return 'top';
17
- return 'bottom';
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
- // Internal state
116
- const [position, setPositionInternal] = useState(controlledPosition || { x: window.innerWidth - 60, y: window.innerHeight - 60 });
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 }, { width: window.innerWidth, height: window.innerHeight }, popupGap);
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 = { width: window.innerWidth, height: window.innerHeight };
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 = { width: window.innerWidth, height: window.innerHeight };
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 = { width: window.innerWidth, height: window.innerHeight };
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.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",