@develemit/web-hooks 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - `drawer-swipe`: `useDrawerSwipe` and the pure `resolveSwipeIntent`. Swipe right from mid-screen opens a mobile nav drawer, left closes it. The left 60px band is left to the browser's back gesture.
6
+ - `focus-trap`: `useFocusTrap` and the pure `focusableWithin` / `nextFocusTarget`. Reconciles the emit-vision, emit-social, develemit-commerce and centraflow copies: initial focus, Tab wrapping, `returnFocusRef` for Safari, opener captured during render, hidden/disabled/inert items skipped.
7
+ - `escape`: `useEscape`. The callback is read through a ref, so changing its identity doesn't re-bind the listener.
8
+ - `clipboard`: `copyText` and `useCopyFeedback`. Copy works on plain-http PWAs where `navigator.clipboard` is undefined (falls back to `execCommand`) and returns a typed `CopyResult` instead of rejecting. The hook tracks `idle | copied | failed` and clears its timer on unmount and on each new copy.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emit Dutcher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,84 @@
1
- # Temporary Holding Version
1
+ # @develemit/web-hooks
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared React behavior hooks for develemit web apps: the interaction plumbing
4
+ that every app ends up hand-copying from the last one.
5
+
6
+ | Subpath | What it does |
7
+ |---|---|
8
+ | `@develemit/web-hooks/drawer-swipe` | Swipe right from mid-screen to open a mobile nav drawer, left to close it |
9
+ | `@develemit/web-hooks/focus-trap` | Keep Tab focus inside an open dialog or drawer, restore it on close |
10
+ | `@develemit/web-hooks/escape` | Run a callback when Escape is pressed |
11
+ | `@develemit/web-hooks/clipboard` | Copy text, with the fallback that works on plain-http PWAs |
12
+
13
+ Behavior only. No styling and no components: each app keeps its own look.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pnpm add @develemit/web-hooks
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```tsx
24
+ import { useDrawerSwipe } from '@develemit/web-hooks/drawer-swipe';
25
+
26
+ useDrawerSwipe({
27
+ drawerOpen,
28
+ onOpen: () => setDrawerOpen(true),
29
+ onClose: () => setDrawerOpen(false),
30
+ enabled: true, // optional, default true
31
+ maxViewportWidth: 768, // optional; no-op at or above this width
32
+ });
33
+ ```
34
+
35
+ The left 60px band is left to the browser's back gesture, and swipes that start
36
+ inside a horizontally scrolling container stay with that container.
37
+
38
+ ### Drawer with focus trap and Escape
39
+
40
+ ```tsx
41
+ import { useRef } from 'react';
42
+ import { useFocusTrap } from '@develemit/web-hooks/focus-trap';
43
+ import { useEscape } from '@develemit/web-hooks/escape';
44
+
45
+ const drawerRef = useRef<HTMLElement>(null);
46
+ const triggerRef = useRef<HTMLButtonElement>(null);
47
+
48
+ useFocusTrap(drawerRef, {
49
+ active: open,
50
+ initialFocusRef, // optional; default is the first focusable item
51
+ returnFocusRef: triggerRef, // optional; wins over the element focused at open
52
+ restoreFocus: true, // optional, default true
53
+ });
54
+ useEscape(() => setOpen(false), open);
55
+ ```
56
+
57
+ Pass `returnFocusRef` when the opener is a button: Safari doesn't focus a
58
+ button on tap, so the element focused at open can be `<body>`. If nothing
59
+ inside is focusable the container itself gets focus (`tabindex="-1"` is added).
60
+ `useEscape` reads the callback through a ref, so it doesn't need `useCallback`.
61
+ When traps stack, only the most recently activated one handles Tab.
62
+
63
+ ### Copy to clipboard
64
+
65
+ ```tsx
66
+ import { useCopyFeedback } from '@develemit/web-hooks/clipboard';
67
+
68
+ const { copy, status } = useCopyFeedback({ resetMs: 1500 });
69
+ <button onClick={() => copy(token)}>{status === 'copied' ? 'Copied!' : 'Copy'}</button>;
70
+ ```
71
+
72
+ `copyText(text)` is the plain function underneath and resolves to
73
+ `{ ok: true }` or `{ ok: false, reason: 'unavailable' | 'denied' }`; it never
74
+ rejects. On plain-http origins (a PWA over a Tailscale address)
75
+ `navigator.clipboard` doesn't exist, so it falls back to `execCommand('copy')`.
76
+
77
+ ### Migrating from the per-app copies
78
+
79
+ | App | Old | New |
80
+ |---|---|---|
81
+ | emit-vision | `useFocusTrap(ref, { active, initialFocusRef, restoreFocus })` | Same signature. |
82
+ | emit-social | `useFocusTrap(ref, { active, returnFocusRef })` plus `useEscape(fn, active)` | Same signatures. |
83
+ | develemit-commerce | `const onKeyDown = useFocusTrap(ref, active, onEscape)` | `useFocusTrap(ref, { active })` and `useEscape(onEscape, active)`. Drop the returned `onKeyDown` handler. |
84
+ | centraflow | `const ref = useFocusTrap(active)` | `const ref = useRef<HTMLElement>(null)`, then `useFocusTrap(ref, { active })`. |
@@ -0,0 +1,89 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/clipboard/index.ts
22
+ var clipboard_exports = {};
23
+ __export(clipboard_exports, {
24
+ copyText: () => copyText,
25
+ useCopyFeedback: () => useCopyFeedback
26
+ });
27
+ module.exports = __toCommonJS(clipboard_exports);
28
+
29
+ // src/clipboard/copy-text.ts
30
+ async function copyText(text) {
31
+ const hasClipboardApi = typeof navigator !== "undefined" && !!navigator.clipboard?.writeText && typeof window !== "undefined" && window.isSecureContext;
32
+ if (hasClipboardApi) {
33
+ try {
34
+ await navigator.clipboard.writeText(text);
35
+ return { ok: true };
36
+ } catch {
37
+ }
38
+ }
39
+ if (execCommandCopy(text)) return { ok: true };
40
+ return { ok: false, reason: hasClipboardApi ? "denied" : "unavailable" };
41
+ }
42
+ function execCommandCopy(text) {
43
+ if (typeof document === "undefined") return false;
44
+ const textarea = document.createElement("textarea");
45
+ textarea.value = text;
46
+ textarea.readOnly = true;
47
+ textarea.style.position = "fixed";
48
+ textarea.style.top = "-1000px";
49
+ textarea.style.left = "-1000px";
50
+ textarea.style.fontSize = "16px";
51
+ document.body.appendChild(textarea);
52
+ try {
53
+ textarea.select();
54
+ textarea.setSelectionRange(0, text.length);
55
+ return document.execCommand("copy");
56
+ } catch {
57
+ return false;
58
+ } finally {
59
+ document.body.removeChild(textarea);
60
+ }
61
+ }
62
+
63
+ // src/clipboard/use-copy-feedback.ts
64
+ var import_react = require("react");
65
+ function useCopyFeedback({ resetMs = 1500 } = {}) {
66
+ const [status, setStatus] = (0, import_react.useState)("idle");
67
+ const timerRef = (0, import_react.useRef)(void 0);
68
+ const clearTimer = (0, import_react.useCallback)(() => {
69
+ if (timerRef.current !== void 0) clearTimeout(timerRef.current);
70
+ timerRef.current = void 0;
71
+ }, []);
72
+ (0, import_react.useEffect)(() => clearTimer, [clearTimer]);
73
+ const copy = (0, import_react.useCallback)(
74
+ async (text) => {
75
+ const result = await copyText(text);
76
+ clearTimer();
77
+ setStatus(result.ok ? "copied" : "failed");
78
+ timerRef.current = setTimeout(() => setStatus("idle"), resetMs);
79
+ return result;
80
+ },
81
+ [clearTimer, resetMs]
82
+ );
83
+ return { copy, status };
84
+ }
85
+ // Annotate the CommonJS export names for ESM import in node:
86
+ 0 && (module.exports = {
87
+ copyText,
88
+ useCopyFeedback
89
+ });
@@ -0,0 +1,17 @@
1
+ type CopyResult = {
2
+ ok: true;
3
+ } | {
4
+ ok: false;
5
+ reason: 'unavailable' | 'denied';
6
+ };
7
+ declare function copyText(text: string): Promise<CopyResult>;
8
+
9
+ type CopyStatus = 'idle' | 'copied' | 'failed';
10
+ declare function useCopyFeedback({ resetMs }?: {
11
+ resetMs?: number;
12
+ }): {
13
+ copy: (text: string) => Promise<CopyResult>;
14
+ status: CopyStatus;
15
+ };
16
+
17
+ export { type CopyResult, type CopyStatus, copyText, useCopyFeedback };
@@ -0,0 +1,17 @@
1
+ type CopyResult = {
2
+ ok: true;
3
+ } | {
4
+ ok: false;
5
+ reason: 'unavailable' | 'denied';
6
+ };
7
+ declare function copyText(text: string): Promise<CopyResult>;
8
+
9
+ type CopyStatus = 'idle' | 'copied' | 'failed';
10
+ declare function useCopyFeedback({ resetMs }?: {
11
+ resetMs?: number;
12
+ }): {
13
+ copy: (text: string) => Promise<CopyResult>;
14
+ status: CopyStatus;
15
+ };
16
+
17
+ export { type CopyResult, type CopyStatus, copyText, useCopyFeedback };
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ // src/clipboard/copy-text.ts
4
+ async function copyText(text) {
5
+ const hasClipboardApi = typeof navigator !== "undefined" && !!navigator.clipboard?.writeText && typeof window !== "undefined" && window.isSecureContext;
6
+ if (hasClipboardApi) {
7
+ try {
8
+ await navigator.clipboard.writeText(text);
9
+ return { ok: true };
10
+ } catch {
11
+ }
12
+ }
13
+ if (execCommandCopy(text)) return { ok: true };
14
+ return { ok: false, reason: hasClipboardApi ? "denied" : "unavailable" };
15
+ }
16
+ function execCommandCopy(text) {
17
+ if (typeof document === "undefined") return false;
18
+ const textarea = document.createElement("textarea");
19
+ textarea.value = text;
20
+ textarea.readOnly = true;
21
+ textarea.style.position = "fixed";
22
+ textarea.style.top = "-1000px";
23
+ textarea.style.left = "-1000px";
24
+ textarea.style.fontSize = "16px";
25
+ document.body.appendChild(textarea);
26
+ try {
27
+ textarea.select();
28
+ textarea.setSelectionRange(0, text.length);
29
+ return document.execCommand("copy");
30
+ } catch {
31
+ return false;
32
+ } finally {
33
+ document.body.removeChild(textarea);
34
+ }
35
+ }
36
+
37
+ // src/clipboard/use-copy-feedback.ts
38
+ import { useCallback, useEffect, useRef, useState } from "react";
39
+ function useCopyFeedback({ resetMs = 1500 } = {}) {
40
+ const [status, setStatus] = useState("idle");
41
+ const timerRef = useRef(void 0);
42
+ const clearTimer = useCallback(() => {
43
+ if (timerRef.current !== void 0) clearTimeout(timerRef.current);
44
+ timerRef.current = void 0;
45
+ }, []);
46
+ useEffect(() => clearTimer, [clearTimer]);
47
+ const copy = useCallback(
48
+ async (text) => {
49
+ const result = await copyText(text);
50
+ clearTimer();
51
+ setStatus(result.ok ? "copied" : "failed");
52
+ timerRef.current = setTimeout(() => setStatus("idle"), resetMs);
53
+ return result;
54
+ },
55
+ [clearTimer, resetMs]
56
+ );
57
+ return { copy, status };
58
+ }
59
+ export {
60
+ copyText,
61
+ useCopyFeedback
62
+ };
@@ -0,0 +1,149 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/drawer-swipe/index.ts
22
+ var drawer_swipe_exports = {};
23
+ __export(drawer_swipe_exports, {
24
+ BACK_ZONE_PX: () => BACK_ZONE_PX,
25
+ CLOSE_DISTANCE_PX: () => CLOSE_DISTANCE_PX,
26
+ FORWARD_ZONE_PX: () => FORWARD_ZONE_PX,
27
+ MAX_SWIPE_DURATION_MS: () => MAX_SWIPE_DURATION_MS,
28
+ OPEN_DISTANCE_PX: () => OPEN_DISTANCE_PX,
29
+ VERTICAL_CANCEL_PX: () => VERTICAL_CANCEL_PX,
30
+ isInOpenZone: () => isInOpenZone,
31
+ isInsideHorizontalScroller: () => isInsideHorizontalScroller,
32
+ resolveSwipeIntent: () => resolveSwipeIntent,
33
+ useDrawerSwipe: () => useDrawerSwipe
34
+ });
35
+ module.exports = __toCommonJS(drawer_swipe_exports);
36
+
37
+ // src/drawer-swipe/swipe-intent.ts
38
+ var BACK_ZONE_PX = 60;
39
+ var FORWARD_ZONE_PX = 30;
40
+ var OPEN_DISTANCE_PX = 80;
41
+ var CLOSE_DISTANCE_PX = 60;
42
+ var VERTICAL_CANCEL_PX = 40;
43
+ var MAX_SWIPE_DURATION_MS = 450;
44
+ var HORIZONTAL_DOMINANCE = 1.5;
45
+ function isInOpenZone(x, viewportWidth) {
46
+ return x > BACK_ZONE_PX && x < viewportWidth - FORWARD_ZONE_PX;
47
+ }
48
+ function resolveSwipeIntent(start, end, drawerOpen, viewportWidth) {
49
+ const dx = end.x - start.x;
50
+ const dy = end.y - start.y;
51
+ if (end.time - start.time > MAX_SWIPE_DURATION_MS) return null;
52
+ if (Math.abs(dx) < Math.abs(dy) * HORIZONTAL_DOMINANCE) return null;
53
+ if (drawerOpen) {
54
+ return dx <= -CLOSE_DISTANCE_PX ? "close" : null;
55
+ }
56
+ if (dx < OPEN_DISTANCE_PX) return null;
57
+ return isInOpenZone(start.x, viewportWidth) ? "open" : null;
58
+ }
59
+ function isInsideHorizontalScroller(target) {
60
+ let node = target instanceof Element ? target : null;
61
+ while (node && node !== document.body) {
62
+ const { overflowX } = window.getComputedStyle(node);
63
+ if ((overflowX === "auto" || overflowX === "scroll") && node.scrollWidth > node.clientWidth) {
64
+ return true;
65
+ }
66
+ node = node.parentElement;
67
+ }
68
+ return false;
69
+ }
70
+
71
+ // src/drawer-swipe/use-drawer-swipe.ts
72
+ var import_react = require("react");
73
+ function isWideViewport(maxViewportWidth) {
74
+ return window.matchMedia(`(min-width: ${maxViewportWidth}px)`).matches;
75
+ }
76
+ function useDrawerSwipe({
77
+ drawerOpen,
78
+ onOpen,
79
+ onClose,
80
+ enabled = true,
81
+ maxViewportWidth = 768
82
+ }) {
83
+ const start = (0, import_react.useRef)(null);
84
+ const handlers = (0, import_react.useRef)({ drawerOpen, onOpen, onClose, maxViewportWidth });
85
+ handlers.current = { drawerOpen, onOpen, onClose, maxViewportWidth };
86
+ (0, import_react.useEffect)(() => {
87
+ if (!enabled) return;
88
+ function handleStart(event) {
89
+ start.current = null;
90
+ if (event.touches.length !== 1) return;
91
+ if (isWideViewport(handlers.current.maxViewportWidth)) return;
92
+ if (!handlers.current.drawerOpen && isInsideHorizontalScroller(event.target)) {
93
+ return;
94
+ }
95
+ const touch = event.touches[0];
96
+ if (!touch) return;
97
+ start.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
98
+ }
99
+ function handleMove(event) {
100
+ const origin = start.current;
101
+ const touch = event.touches[0];
102
+ if (!origin || !touch) return;
103
+ if (Math.abs(touch.clientY - origin.y) > VERTICAL_CANCEL_PX) {
104
+ start.current = null;
105
+ }
106
+ }
107
+ function handleEnd(event) {
108
+ const origin = start.current;
109
+ start.current = null;
110
+ const touch = event.changedTouches[0];
111
+ if (!origin || !touch) return;
112
+ const { drawerOpen: drawerOpen2, onOpen: onOpen2, onClose: onClose2 } = handlers.current;
113
+ const intent = resolveSwipeIntent(
114
+ origin,
115
+ { x: touch.clientX, y: touch.clientY, time: Date.now() },
116
+ drawerOpen2,
117
+ window.innerWidth
118
+ );
119
+ if (intent === "open") onOpen2();
120
+ if (intent === "close") onClose2();
121
+ }
122
+ function handleCancel() {
123
+ start.current = null;
124
+ }
125
+ document.addEventListener("touchstart", handleStart, { passive: true });
126
+ document.addEventListener("touchmove", handleMove, { passive: true });
127
+ document.addEventListener("touchend", handleEnd, { passive: true });
128
+ document.addEventListener("touchcancel", handleCancel, { passive: true });
129
+ return () => {
130
+ document.removeEventListener("touchstart", handleStart);
131
+ document.removeEventListener("touchmove", handleMove);
132
+ document.removeEventListener("touchend", handleEnd);
133
+ document.removeEventListener("touchcancel", handleCancel);
134
+ };
135
+ }, [enabled]);
136
+ }
137
+ // Annotate the CommonJS export names for ESM import in node:
138
+ 0 && (module.exports = {
139
+ BACK_ZONE_PX,
140
+ CLOSE_DISTANCE_PX,
141
+ FORWARD_ZONE_PX,
142
+ MAX_SWIPE_DURATION_MS,
143
+ OPEN_DISTANCE_PX,
144
+ VERTICAL_CANCEL_PX,
145
+ isInOpenZone,
146
+ isInsideHorizontalScroller,
147
+ resolveSwipeIntent,
148
+ useDrawerSwipe
149
+ });
@@ -0,0 +1,26 @@
1
+ type SwipePoint = {
2
+ x: number;
3
+ y: number;
4
+ time: number;
5
+ };
6
+ declare const BACK_ZONE_PX = 60;
7
+ declare const FORWARD_ZONE_PX = 30;
8
+ declare const OPEN_DISTANCE_PX = 80;
9
+ declare const CLOSE_DISTANCE_PX = 60;
10
+ declare const VERTICAL_CANCEL_PX = 40;
11
+ declare const MAX_SWIPE_DURATION_MS = 450;
12
+ type SwipeIntent = 'open' | 'close' | null;
13
+ declare function isInOpenZone(x: number, viewportWidth: number): boolean;
14
+ declare function resolveSwipeIntent(start: SwipePoint, end: SwipePoint, drawerOpen: boolean, viewportWidth: number): SwipeIntent;
15
+ declare function isInsideHorizontalScroller(target: EventTarget | null): boolean;
16
+
17
+ type UseDrawerSwipeOptions = {
18
+ drawerOpen: boolean;
19
+ onOpen: () => void;
20
+ onClose: () => void;
21
+ enabled?: boolean;
22
+ maxViewportWidth?: number;
23
+ };
24
+ declare function useDrawerSwipe({ drawerOpen, onOpen, onClose, enabled, maxViewportWidth, }: UseDrawerSwipeOptions): void;
25
+
26
+ export { BACK_ZONE_PX, CLOSE_DISTANCE_PX, FORWARD_ZONE_PX, MAX_SWIPE_DURATION_MS, OPEN_DISTANCE_PX, type SwipeIntent, type SwipePoint, type UseDrawerSwipeOptions, VERTICAL_CANCEL_PX, isInOpenZone, isInsideHorizontalScroller, resolveSwipeIntent, useDrawerSwipe };
@@ -0,0 +1,26 @@
1
+ type SwipePoint = {
2
+ x: number;
3
+ y: number;
4
+ time: number;
5
+ };
6
+ declare const BACK_ZONE_PX = 60;
7
+ declare const FORWARD_ZONE_PX = 30;
8
+ declare const OPEN_DISTANCE_PX = 80;
9
+ declare const CLOSE_DISTANCE_PX = 60;
10
+ declare const VERTICAL_CANCEL_PX = 40;
11
+ declare const MAX_SWIPE_DURATION_MS = 450;
12
+ type SwipeIntent = 'open' | 'close' | null;
13
+ declare function isInOpenZone(x: number, viewportWidth: number): boolean;
14
+ declare function resolveSwipeIntent(start: SwipePoint, end: SwipePoint, drawerOpen: boolean, viewportWidth: number): SwipeIntent;
15
+ declare function isInsideHorizontalScroller(target: EventTarget | null): boolean;
16
+
17
+ type UseDrawerSwipeOptions = {
18
+ drawerOpen: boolean;
19
+ onOpen: () => void;
20
+ onClose: () => void;
21
+ enabled?: boolean;
22
+ maxViewportWidth?: number;
23
+ };
24
+ declare function useDrawerSwipe({ drawerOpen, onOpen, onClose, enabled, maxViewportWidth, }: UseDrawerSwipeOptions): void;
25
+
26
+ export { BACK_ZONE_PX, CLOSE_DISTANCE_PX, FORWARD_ZONE_PX, MAX_SWIPE_DURATION_MS, OPEN_DISTANCE_PX, type SwipeIntent, type SwipePoint, type UseDrawerSwipeOptions, VERTICAL_CANCEL_PX, isInOpenZone, isInsideHorizontalScroller, resolveSwipeIntent, useDrawerSwipe };
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ // src/drawer-swipe/swipe-intent.ts
4
+ var BACK_ZONE_PX = 60;
5
+ var FORWARD_ZONE_PX = 30;
6
+ var OPEN_DISTANCE_PX = 80;
7
+ var CLOSE_DISTANCE_PX = 60;
8
+ var VERTICAL_CANCEL_PX = 40;
9
+ var MAX_SWIPE_DURATION_MS = 450;
10
+ var HORIZONTAL_DOMINANCE = 1.5;
11
+ function isInOpenZone(x, viewportWidth) {
12
+ return x > BACK_ZONE_PX && x < viewportWidth - FORWARD_ZONE_PX;
13
+ }
14
+ function resolveSwipeIntent(start, end, drawerOpen, viewportWidth) {
15
+ const dx = end.x - start.x;
16
+ const dy = end.y - start.y;
17
+ if (end.time - start.time > MAX_SWIPE_DURATION_MS) return null;
18
+ if (Math.abs(dx) < Math.abs(dy) * HORIZONTAL_DOMINANCE) return null;
19
+ if (drawerOpen) {
20
+ return dx <= -CLOSE_DISTANCE_PX ? "close" : null;
21
+ }
22
+ if (dx < OPEN_DISTANCE_PX) return null;
23
+ return isInOpenZone(start.x, viewportWidth) ? "open" : null;
24
+ }
25
+ function isInsideHorizontalScroller(target) {
26
+ let node = target instanceof Element ? target : null;
27
+ while (node && node !== document.body) {
28
+ const { overflowX } = window.getComputedStyle(node);
29
+ if ((overflowX === "auto" || overflowX === "scroll") && node.scrollWidth > node.clientWidth) {
30
+ return true;
31
+ }
32
+ node = node.parentElement;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ // src/drawer-swipe/use-drawer-swipe.ts
38
+ import { useEffect, useRef } from "react";
39
+ function isWideViewport(maxViewportWidth) {
40
+ return window.matchMedia(`(min-width: ${maxViewportWidth}px)`).matches;
41
+ }
42
+ function useDrawerSwipe({
43
+ drawerOpen,
44
+ onOpen,
45
+ onClose,
46
+ enabled = true,
47
+ maxViewportWidth = 768
48
+ }) {
49
+ const start = useRef(null);
50
+ const handlers = useRef({ drawerOpen, onOpen, onClose, maxViewportWidth });
51
+ handlers.current = { drawerOpen, onOpen, onClose, maxViewportWidth };
52
+ useEffect(() => {
53
+ if (!enabled) return;
54
+ function handleStart(event) {
55
+ start.current = null;
56
+ if (event.touches.length !== 1) return;
57
+ if (isWideViewport(handlers.current.maxViewportWidth)) return;
58
+ if (!handlers.current.drawerOpen && isInsideHorizontalScroller(event.target)) {
59
+ return;
60
+ }
61
+ const touch = event.touches[0];
62
+ if (!touch) return;
63
+ start.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
64
+ }
65
+ function handleMove(event) {
66
+ const origin = start.current;
67
+ const touch = event.touches[0];
68
+ if (!origin || !touch) return;
69
+ if (Math.abs(touch.clientY - origin.y) > VERTICAL_CANCEL_PX) {
70
+ start.current = null;
71
+ }
72
+ }
73
+ function handleEnd(event) {
74
+ const origin = start.current;
75
+ start.current = null;
76
+ const touch = event.changedTouches[0];
77
+ if (!origin || !touch) return;
78
+ const { drawerOpen: drawerOpen2, onOpen: onOpen2, onClose: onClose2 } = handlers.current;
79
+ const intent = resolveSwipeIntent(
80
+ origin,
81
+ { x: touch.clientX, y: touch.clientY, time: Date.now() },
82
+ drawerOpen2,
83
+ window.innerWidth
84
+ );
85
+ if (intent === "open") onOpen2();
86
+ if (intent === "close") onClose2();
87
+ }
88
+ function handleCancel() {
89
+ start.current = null;
90
+ }
91
+ document.addEventListener("touchstart", handleStart, { passive: true });
92
+ document.addEventListener("touchmove", handleMove, { passive: true });
93
+ document.addEventListener("touchend", handleEnd, { passive: true });
94
+ document.addEventListener("touchcancel", handleCancel, { passive: true });
95
+ return () => {
96
+ document.removeEventListener("touchstart", handleStart);
97
+ document.removeEventListener("touchmove", handleMove);
98
+ document.removeEventListener("touchend", handleEnd);
99
+ document.removeEventListener("touchcancel", handleCancel);
100
+ };
101
+ }, [enabled]);
102
+ }
103
+ export {
104
+ BACK_ZONE_PX,
105
+ CLOSE_DISTANCE_PX,
106
+ FORWARD_ZONE_PX,
107
+ MAX_SWIPE_DURATION_MS,
108
+ OPEN_DISTANCE_PX,
109
+ VERTICAL_CANCEL_PX,
110
+ isInOpenZone,
111
+ isInsideHorizontalScroller,
112
+ resolveSwipeIntent,
113
+ useDrawerSwipe
114
+ };
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/escape/index.ts
22
+ var escape_exports = {};
23
+ __export(escape_exports, {
24
+ useEscape: () => useEscape
25
+ });
26
+ module.exports = __toCommonJS(escape_exports);
27
+
28
+ // src/escape/use-escape.ts
29
+ var import_react = require("react");
30
+ function useEscape(onEscape, active = true) {
31
+ const handlerRef = (0, import_react.useRef)(onEscape);
32
+ (0, import_react.useEffect)(() => {
33
+ handlerRef.current = onEscape;
34
+ });
35
+ (0, import_react.useEffect)(() => {
36
+ if (!active) return;
37
+ function onKeyDown(event) {
38
+ if (event.key === "Escape") handlerRef.current();
39
+ }
40
+ document.addEventListener("keydown", onKeyDown);
41
+ return () => document.removeEventListener("keydown", onKeyDown);
42
+ }, [active]);
43
+ }
44
+ // Annotate the CommonJS export names for ESM import in node:
45
+ 0 && (module.exports = {
46
+ useEscape
47
+ });
@@ -0,0 +1,3 @@
1
+ declare function useEscape(onEscape: () => void, active?: boolean): void;
2
+
3
+ export { useEscape };
@@ -0,0 +1,3 @@
1
+ declare function useEscape(onEscape: () => void, active?: boolean): void;
2
+
3
+ export { useEscape };