@develemit/web-hooks 0.1.0 → 0.2.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 +16 -0
- package/README.md +166 -0
- package/dist/auto-grow.cjs +60 -0
- package/dist/auto-grow.d.cts +14 -0
- package/dist/auto-grow.d.ts +14 -0
- package/dist/auto-grow.js +9 -0
- package/dist/chunk-3CACI4OQ.js +68 -0
- package/dist/chunk-3UGRXQIA.js +39 -0
- package/dist/chunk-5CA5RIMH.js +34 -0
- package/dist/chunk-5KIH2XLV.js +136 -0
- package/dist/chunk-6KCRVSLS.js +80 -0
- package/dist/chunk-DNU57XEJ.js +82 -0
- package/dist/chunk-IWQDAJEP.js +69 -0
- package/dist/chunk-LZKAGDER.js +22 -0
- package/dist/chunk-NKLEB5TR.js +98 -0
- package/dist/chunk-OZ4PU4E3.js +115 -0
- package/dist/chunk-S4YKMGQE.js +23 -0
- package/dist/chunk-UPVNSGIK.js +72 -0
- package/dist/chunk-UZADAINO.js +61 -0
- package/dist/clipboard.cjs +5 -0
- package/dist/clipboard.js +4 -57
- package/dist/debounce.cjs +96 -0
- package/dist/debounce.d.cts +21 -0
- package/dist/debounce.d.ts +21 -0
- package/dist/debounce.js +11 -0
- package/dist/drawer-swipe.js +12 -101
- package/dist/escape.cjs +1 -0
- package/dist/escape.js +3 -17
- package/dist/focus-trap.cjs +3 -1
- package/dist/focus-trap.js +6 -72
- package/dist/hotkey.cjs +127 -0
- package/dist/hotkey.d.cts +31 -0
- package/dist/hotkey.d.ts +31 -0
- package/dist/hotkey.js +15 -0
- package/dist/index.cjs +613 -3
- package/dist/index.d.cts +14 -60
- package/dist/index.d.ts +14 -60
- package/dist/index.js +97 -247
- package/dist/media-query.cjs +90 -0
- package/dist/media-query.d.cts +12 -0
- package/dist/media-query.d.ts +12 -0
- package/dist/media-query.js +15 -0
- package/dist/outside-press.cjs +66 -0
- package/dist/outside-press.d.cts +15 -0
- package/dist/outside-press.d.ts +15 -0
- package/dist/outside-press.js +11 -0
- package/dist/scroll-lock.cjs +107 -0
- package/dist/scroll-lock.d.cts +8 -0
- package/dist/scroll-lock.d.ts +8 -0
- package/dist/scroll-lock.js +11 -0
- package/dist/storage-state.cjs +161 -0
- package/dist/storage-state.d.cts +14 -0
- package/dist/storage-state.d.ts +14 -0
- package/dist/storage-state.js +7 -0
- package/dist/unsaved-guard.cjs +48 -0
- package/dist/unsaved-guard.d.cts +5 -0
- package/dist/unsaved-guard.d.ts +5 -0
- package/dist/unsaved-guard.js +9 -0
- package/dist/visibility-polling.cjs +98 -0
- package/dist/visibility-polling.d.cts +14 -0
- package/dist/visibility-polling.d.ts +14 -0
- package/dist/visibility-polling.js +9 -0
- package/package.json +55 -2
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/focus-trap/focusables.ts
|
|
4
|
+
var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
5
|
+
function focusableWithin(container) {
|
|
6
|
+
return Array.from(
|
|
7
|
+
container.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
8
|
+
).filter((el) => !el.closest("[hidden], [inert]"));
|
|
9
|
+
}
|
|
10
|
+
function nextFocusTarget(items, current, shiftKey, container) {
|
|
11
|
+
const first = items[0];
|
|
12
|
+
const last = items[items.length - 1];
|
|
13
|
+
if (!first || !last) return null;
|
|
14
|
+
if (!current || !container.contains(current)) return shiftKey ? last : first;
|
|
15
|
+
if (shiftKey && (current === first || current === container)) return last;
|
|
16
|
+
if (!shiftKey && current === last) return first;
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/focus-trap/use-focus-trap.ts
|
|
21
|
+
import { useEffect, useRef } from "react";
|
|
22
|
+
var activeTraps = [];
|
|
23
|
+
function useFocusTrap(containerRef, {
|
|
24
|
+
active,
|
|
25
|
+
initialFocusRef,
|
|
26
|
+
returnFocusRef,
|
|
27
|
+
restoreFocus = true
|
|
28
|
+
}) {
|
|
29
|
+
const openerRef = useRef(null);
|
|
30
|
+
if (active && !openerRef.current && typeof document !== "undefined") {
|
|
31
|
+
openerRef.current = document.activeElement;
|
|
32
|
+
}
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!active) openerRef.current = null;
|
|
35
|
+
}, [active]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const container = containerRef.current;
|
|
38
|
+
if (!active || !container) return;
|
|
39
|
+
const token = {};
|
|
40
|
+
activeTraps.push(token);
|
|
41
|
+
if (!container.contains(document.activeElement)) {
|
|
42
|
+
const target = initialFocusRef?.current ?? focusableWithin(container)[0] ?? container;
|
|
43
|
+
if (target === container && !container.hasAttribute("tabindex")) {
|
|
44
|
+
container.setAttribute("tabindex", "-1");
|
|
45
|
+
}
|
|
46
|
+
target.focus();
|
|
47
|
+
}
|
|
48
|
+
function onKeyDown(event) {
|
|
49
|
+
if (event.key !== "Tab" || !container) return;
|
|
50
|
+
if (activeTraps[activeTraps.length - 1] !== token) return;
|
|
51
|
+
const items = focusableWithin(container);
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const target = nextFocusTarget(
|
|
57
|
+
items,
|
|
58
|
+
document.activeElement,
|
|
59
|
+
event.shiftKey,
|
|
60
|
+
container
|
|
61
|
+
);
|
|
62
|
+
if (target) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
target.focus();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
document.addEventListener("keydown", onKeyDown);
|
|
68
|
+
return () => {
|
|
69
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
70
|
+
activeTraps.splice(activeTraps.indexOf(token), 1);
|
|
71
|
+
const opener = returnFocusRef?.current ?? openerRef.current;
|
|
72
|
+
if (restoreFocus && opener?.isConnected) opener.focus();
|
|
73
|
+
};
|
|
74
|
+
}, [active, containerRef, initialFocusRef, returnFocusRef, restoreFocus]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
FOCUSABLE_SELECTOR,
|
|
79
|
+
focusableWithin,
|
|
80
|
+
nextFocusTarget,
|
|
81
|
+
useFocusTrap
|
|
82
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/debounce/debouncer.ts
|
|
4
|
+
function createDebouncer(getFn, getMs) {
|
|
5
|
+
let timer;
|
|
6
|
+
let pending;
|
|
7
|
+
function clear() {
|
|
8
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
9
|
+
timer = void 0;
|
|
10
|
+
pending = void 0;
|
|
11
|
+
}
|
|
12
|
+
function run() {
|
|
13
|
+
const args = pending;
|
|
14
|
+
clear();
|
|
15
|
+
if (args) getFn()(...args);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
call(...args) {
|
|
19
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
20
|
+
pending = args;
|
|
21
|
+
timer = setTimeout(run, getMs());
|
|
22
|
+
},
|
|
23
|
+
cancel: clear,
|
|
24
|
+
flush() {
|
|
25
|
+
if (pending) run();
|
|
26
|
+
},
|
|
27
|
+
isPending: () => pending !== void 0
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/debounce/use-debounced-value.ts
|
|
32
|
+
import { useEffect, useState } from "react";
|
|
33
|
+
function useDebouncedValue(value, ms) {
|
|
34
|
+
const [debounced, setDebounced] = useState(value);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const timer = setTimeout(() => setDebounced(value), ms);
|
|
37
|
+
return () => clearTimeout(timer);
|
|
38
|
+
}, [value, ms]);
|
|
39
|
+
return debounced;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/debounce/use-debounced-callback.ts
|
|
43
|
+
import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
44
|
+
function useDebouncedCallback(fn, ms) {
|
|
45
|
+
const fnRef = useRef(fn);
|
|
46
|
+
const msRef = useRef(ms);
|
|
47
|
+
useEffect2(() => {
|
|
48
|
+
fnRef.current = fn;
|
|
49
|
+
msRef.current = ms;
|
|
50
|
+
});
|
|
51
|
+
const [debounced] = useState2(() => {
|
|
52
|
+
const debouncer = createDebouncer(
|
|
53
|
+
() => fnRef.current,
|
|
54
|
+
() => msRef.current
|
|
55
|
+
);
|
|
56
|
+
return Object.assign((...args) => debouncer.call(...args), {
|
|
57
|
+
cancel: debouncer.cancel,
|
|
58
|
+
flush: debouncer.flush
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
useEffect2(() => () => debounced.cancel(), [debounced]);
|
|
62
|
+
return debounced;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
createDebouncer,
|
|
67
|
+
useDebouncedValue,
|
|
68
|
+
useDebouncedCallback
|
|
69
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/unsaved-guard/handle-before-unload.ts
|
|
4
|
+
function handleBeforeUnload(event) {
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
event.returnValue = "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/unsaved-guard/use-unsaved-changes-guard.ts
|
|
10
|
+
import { useEffect } from "react";
|
|
11
|
+
function useUnsavedChangesGuard(dirty) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!dirty) return;
|
|
14
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
15
|
+
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
16
|
+
}, [dirty]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
handleBeforeUnload,
|
|
21
|
+
useUnsavedChangesGuard
|
|
22
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/hotkey/parse-hotkey.ts
|
|
4
|
+
var KEY_ALIASES = {
|
|
5
|
+
esc: "escape",
|
|
6
|
+
space: " ",
|
|
7
|
+
plus: "+",
|
|
8
|
+
up: "arrowup",
|
|
9
|
+
down: "arrowdown",
|
|
10
|
+
left: "arrowleft",
|
|
11
|
+
right: "arrowright"
|
|
12
|
+
};
|
|
13
|
+
function parseHotkey(combo) {
|
|
14
|
+
const parsed = { key: "", mod: false, ctrl: false, meta: false, alt: false, shift: false };
|
|
15
|
+
const parts = combo.trim().toLowerCase().split(/\+(?=.)/);
|
|
16
|
+
for (const part of parts) {
|
|
17
|
+
const token = part.trim();
|
|
18
|
+
if (token === "mod" || token === "ctrl" || token === "meta" || token === "alt" || token === "shift") {
|
|
19
|
+
parsed[token] = true;
|
|
20
|
+
} else {
|
|
21
|
+
parsed.key = KEY_ALIASES[token] ?? token;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!parsed.key) throw new Error(`Invalid hotkey "${combo}": no key`);
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/hotkey/match-hotkey.ts
|
|
29
|
+
var CODE_FOR_SYMBOL = {
|
|
30
|
+
"/": "Slash",
|
|
31
|
+
".": "Period",
|
|
32
|
+
",": "Comma",
|
|
33
|
+
";": "Semicolon",
|
|
34
|
+
"'": "Quote",
|
|
35
|
+
"[": "BracketLeft",
|
|
36
|
+
"]": "BracketRight",
|
|
37
|
+
"\\": "Backslash",
|
|
38
|
+
"-": "Minus",
|
|
39
|
+
"=": "Equal",
|
|
40
|
+
"`": "Backquote"
|
|
41
|
+
};
|
|
42
|
+
function physicalCode(key) {
|
|
43
|
+
if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`;
|
|
44
|
+
if (/^[0-9]$/.test(key)) return `Digit${key}`;
|
|
45
|
+
return CODE_FOR_SYMBOL[key] ?? null;
|
|
46
|
+
}
|
|
47
|
+
function keyMatches(event, parsed) {
|
|
48
|
+
if (event.key.toLowerCase() === parsed.key) return true;
|
|
49
|
+
return parsed.shift && physicalCode(parsed.key) === event.code;
|
|
50
|
+
}
|
|
51
|
+
function matchHotkey(event, combo, isApple) {
|
|
52
|
+
const parsed = typeof combo === "string" ? parseHotkey(combo) : combo;
|
|
53
|
+
const wantMeta = parsed.meta || parsed.mod && isApple;
|
|
54
|
+
const wantCtrl = parsed.ctrl || parsed.mod && !isApple;
|
|
55
|
+
return event.metaKey === wantMeta && event.ctrlKey === wantCtrl && event.altKey === parsed.alt && event.shiftKey === parsed.shift && keyMatches(event, parsed);
|
|
56
|
+
}
|
|
57
|
+
function detectApple(nav) {
|
|
58
|
+
if (!nav) return false;
|
|
59
|
+
const platform = nav.userAgentData?.platform || nav.platform || "";
|
|
60
|
+
return /mac|iphone|ipad|ipod/i.test(platform);
|
|
61
|
+
}
|
|
62
|
+
function isEditableTarget(target) {
|
|
63
|
+
const el = target;
|
|
64
|
+
if (!el || typeof el.tagName !== "string") return false;
|
|
65
|
+
const tag = el.tagName.toLowerCase();
|
|
66
|
+
return tag === "input" || tag === "textarea" || tag === "select" || el.isContentEditable === true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/hotkey/use-hotkey.ts
|
|
70
|
+
import { useEffect, useRef } from "react";
|
|
71
|
+
function useHotkey(combo, onHotkey, options = {}) {
|
|
72
|
+
const { enabled = true, allowInInputs = false, preventDefault = true } = options;
|
|
73
|
+
const handlerRef = useRef(onHotkey);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
handlerRef.current = onHotkey;
|
|
76
|
+
});
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!enabled) return;
|
|
79
|
+
const parsed = parseHotkey(combo);
|
|
80
|
+
function onKeyDown(event) {
|
|
81
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
82
|
+
if (!allowInInputs && isEditableTarget(event.target)) return;
|
|
83
|
+
if (!matchHotkey(event, parsed, detectApple(navigator))) return;
|
|
84
|
+
if (preventDefault) event.preventDefault();
|
|
85
|
+
handlerRef.current(event);
|
|
86
|
+
}
|
|
87
|
+
document.addEventListener("keydown", onKeyDown);
|
|
88
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
89
|
+
}, [combo, enabled, allowInInputs, preventDefault]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
parseHotkey,
|
|
94
|
+
matchHotkey,
|
|
95
|
+
detectApple,
|
|
96
|
+
isEditableTarget,
|
|
97
|
+
useHotkey
|
|
98
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
|
|
104
|
+
export {
|
|
105
|
+
BACK_ZONE_PX,
|
|
106
|
+
FORWARD_ZONE_PX,
|
|
107
|
+
OPEN_DISTANCE_PX,
|
|
108
|
+
CLOSE_DISTANCE_PX,
|
|
109
|
+
VERTICAL_CANCEL_PX,
|
|
110
|
+
MAX_SWIPE_DURATION_MS,
|
|
111
|
+
isInOpenZone,
|
|
112
|
+
resolveSwipeIntent,
|
|
113
|
+
isInsideHorizontalScroller,
|
|
114
|
+
useDrawerSwipe
|
|
115
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/escape/use-escape.ts
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
function useEscape(onEscape, active = true) {
|
|
6
|
+
const handlerRef = useRef(onEscape);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
handlerRef.current = onEscape;
|
|
9
|
+
});
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!active) return;
|
|
12
|
+
function onKeyDown(event) {
|
|
13
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
14
|
+
if (event.key === "Escape") handlerRef.current();
|
|
15
|
+
}
|
|
16
|
+
document.addEventListener("keydown", onKeyDown);
|
|
17
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
18
|
+
}, [active]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
useEscape
|
|
23
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/visibility-polling/schedule.ts
|
|
4
|
+
function nextDelay(intervalMs, jitter, random) {
|
|
5
|
+
const fraction = Math.min(1, Math.max(0, jitter));
|
|
6
|
+
return intervalMs + random() * fraction * intervalMs;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/visibility-polling/use-visibility-polling.ts
|
|
10
|
+
import { useEffect, useRef } from "react";
|
|
11
|
+
var isHidden = () => document.visibilityState === "hidden";
|
|
12
|
+
function useVisibilityPolling(fn, intervalMs, { enabled = true, immediate = true, jitter = 0, runOnVisible = true } = {}) {
|
|
13
|
+
const fnRef = useRef(fn);
|
|
14
|
+
const optionsRef = useRef({ immediate, jitter, runOnVisible });
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
fnRef.current = fn;
|
|
17
|
+
optionsRef.current = { immediate, jitter, runOnVisible };
|
|
18
|
+
});
|
|
19
|
+
const runningRef = useRef(false);
|
|
20
|
+
const scheduleRef = useRef(null);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!enabled) return;
|
|
23
|
+
let active = true;
|
|
24
|
+
let timer = null;
|
|
25
|
+
function clearTimer() {
|
|
26
|
+
if (timer !== null) clearTimeout(timer);
|
|
27
|
+
timer = null;
|
|
28
|
+
}
|
|
29
|
+
function schedule() {
|
|
30
|
+
clearTimer();
|
|
31
|
+
if (!active || isHidden()) return;
|
|
32
|
+
timer = setTimeout(run, nextDelay(intervalMs, optionsRef.current.jitter, Math.random));
|
|
33
|
+
}
|
|
34
|
+
async function run() {
|
|
35
|
+
timer = null;
|
|
36
|
+
if (!active || runningRef.current || isHidden()) return;
|
|
37
|
+
runningRef.current = true;
|
|
38
|
+
try {
|
|
39
|
+
await fnRef.current();
|
|
40
|
+
} catch {
|
|
41
|
+
} finally {
|
|
42
|
+
runningRef.current = false;
|
|
43
|
+
}
|
|
44
|
+
scheduleRef.current?.();
|
|
45
|
+
}
|
|
46
|
+
function onVisibilityChange() {
|
|
47
|
+
if (isHidden()) {
|
|
48
|
+
clearTimer();
|
|
49
|
+
} else if (optionsRef.current.runOnVisible) {
|
|
50
|
+
clearTimer();
|
|
51
|
+
void run();
|
|
52
|
+
} else if (!runningRef.current) {
|
|
53
|
+
schedule();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
scheduleRef.current = schedule;
|
|
57
|
+
if (optionsRef.current.immediate && !isHidden()) void run();
|
|
58
|
+
else schedule();
|
|
59
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
60
|
+
return () => {
|
|
61
|
+
active = false;
|
|
62
|
+
scheduleRef.current = null;
|
|
63
|
+
clearTimer();
|
|
64
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
65
|
+
};
|
|
66
|
+
}, [enabled, intervalMs]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
nextDelay,
|
|
71
|
+
useVisibilityPolling
|
|
72
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/media-query/use-media-query.ts
|
|
4
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
5
|
+
|
|
6
|
+
// src/media-query/media-query.ts
|
|
7
|
+
var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
8
|
+
var COARSE_POINTER_QUERY = "(pointer: coarse)";
|
|
9
|
+
var lists = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
function getList(query) {
|
|
11
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return null;
|
|
12
|
+
const owner = window.matchMedia;
|
|
13
|
+
let byQuery = lists.get(owner);
|
|
14
|
+
if (!byQuery) {
|
|
15
|
+
byQuery = /* @__PURE__ */ new Map();
|
|
16
|
+
lists.set(owner, byQuery);
|
|
17
|
+
}
|
|
18
|
+
let list = byQuery.get(query);
|
|
19
|
+
if (!list) {
|
|
20
|
+
list = owner.call(window, query);
|
|
21
|
+
byQuery.set(query, list);
|
|
22
|
+
}
|
|
23
|
+
return list;
|
|
24
|
+
}
|
|
25
|
+
function getMatches(query) {
|
|
26
|
+
return getList(query)?.matches ?? null;
|
|
27
|
+
}
|
|
28
|
+
function subscribe(query, onChange) {
|
|
29
|
+
const list = getList(query);
|
|
30
|
+
if (!list) return () => {
|
|
31
|
+
};
|
|
32
|
+
if (typeof list.addEventListener === "function") {
|
|
33
|
+
list.addEventListener("change", onChange);
|
|
34
|
+
return () => list.removeEventListener("change", onChange);
|
|
35
|
+
}
|
|
36
|
+
list.addListener(onChange);
|
|
37
|
+
return () => list.removeListener(onChange);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/media-query/use-media-query.ts
|
|
41
|
+
function useMediaQuery(query, options = {}) {
|
|
42
|
+
const serverDefault = options.serverDefault ?? false;
|
|
43
|
+
const subscribeToQuery = useCallback((onChange) => subscribe(query, onChange), [query]);
|
|
44
|
+
const getSnapshot = useCallback(() => getMatches(query) ?? serverDefault, [query, serverDefault]);
|
|
45
|
+
const getServerSnapshot = useCallback(() => serverDefault, [serverDefault]);
|
|
46
|
+
return useSyncExternalStore(subscribeToQuery, getSnapshot, getServerSnapshot);
|
|
47
|
+
}
|
|
48
|
+
function useReducedMotion(options) {
|
|
49
|
+
return useMediaQuery(REDUCED_MOTION_QUERY, options);
|
|
50
|
+
}
|
|
51
|
+
function useCoarsePointer(options) {
|
|
52
|
+
return useMediaQuery(COARSE_POINTER_QUERY, options);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
REDUCED_MOTION_QUERY,
|
|
57
|
+
COARSE_POINTER_QUERY,
|
|
58
|
+
useMediaQuery,
|
|
59
|
+
useReducedMotion,
|
|
60
|
+
useCoarsePointer
|
|
61
|
+
};
|
package/dist/clipboard.cjs
CHANGED
|
@@ -48,8 +48,10 @@ function execCommandCopy(text) {
|
|
|
48
48
|
textarea.style.top = "-1000px";
|
|
49
49
|
textarea.style.left = "-1000px";
|
|
50
50
|
textarea.style.fontSize = "16px";
|
|
51
|
+
const previouslyFocused = document.activeElement;
|
|
51
52
|
document.body.appendChild(textarea);
|
|
52
53
|
try {
|
|
54
|
+
textarea.focus({ preventScroll: true });
|
|
53
55
|
textarea.select();
|
|
54
56
|
textarea.setSelectionRange(0, text.length);
|
|
55
57
|
return document.execCommand("copy");
|
|
@@ -57,6 +59,9 @@ function execCommandCopy(text) {
|
|
|
57
59
|
return false;
|
|
58
60
|
} finally {
|
|
59
61
|
document.body.removeChild(textarea);
|
|
62
|
+
if (previouslyFocused instanceof HTMLElement && previouslyFocused !== document.body) {
|
|
63
|
+
previouslyFocused.focus({ preventScroll: true });
|
|
64
|
+
}
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
|
package/dist/clipboard.js
CHANGED
|
@@ -1,61 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
}
|
|
2
|
+
import {
|
|
3
|
+
copyText,
|
|
4
|
+
useCopyFeedback
|
|
5
|
+
} from "./chunk-3CACI4OQ.js";
|
|
59
6
|
export {
|
|
60
7
|
copyText,
|
|
61
8
|
useCopyFeedback
|